refactor(web): use shared ui primitives
This commit is contained in:
parent
f692a94d25
commit
d26a4ae9de
@ -3,6 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { Badge, Button, Card } from "@/components/ui/Primitives";
|
||||
import { chatOverWorkspace, listWorkspaceSummaries } from "@/lib/notes-client";
|
||||
import type { WorkspaceSummary } from "@/lib/types";
|
||||
import { toast } from "@/lib/toast";
|
||||
@ -42,9 +43,9 @@ export default function ChatPage() {
|
||||
<AppShell
|
||||
title="Workspace chat"
|
||||
description="Retrieval over your notes (citations below). This is not a general-purpose LLM — answers are assembled from indexed note text."
|
||||
actions={<span className="badge">Feature-flagged on backend (chat.rag_enabled)</span>}
|
||||
actions={<Badge>Feature-flagged on backend (chat.rag_enabled)</Badge>}
|
||||
>
|
||||
<section className="surface-card" style={{ padding: "var(--nl-space-6)", display: "grid", gap: "var(--nl-space-4)", maxWidth: 720 }}>
|
||||
<Card style={{ padding: "var(--nl-space-6)", display: "grid", gap: "var(--nl-space-4)", maxWidth: 720 }}>
|
||||
<label style={{ display: "grid", gap: 8 }}>
|
||||
<span style={{ fontWeight: 600 }}>Workspace</span>
|
||||
{workspaces.length === 0 ? (
|
||||
@ -71,14 +72,14 @@ export default function ChatPage() {
|
||||
placeholder="What did we decide about launch?"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={loading || !workspaceId.trim() || workspaces.length === 0}
|
||||
loading={loading}
|
||||
onClick={() => void handleAsk()}
|
||||
>
|
||||
{loading ? "Thinking…" : "Ask"}
|
||||
</button>
|
||||
Ask
|
||||
</Button>
|
||||
{answer ? (
|
||||
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
<strong>Answer</strong>
|
||||
@ -98,7 +99,7 @@ export default function ChatPage() {
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
</Card>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { CreateNoteModal } from "@/components/CreateNoteModal";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { listNoteSummaries, listWorkspaceSummaries, seedOnboardingWorkspace } from "@/lib/notes-client";
|
||||
import { listApprovalQueue } from "@/lib/review-client";
|
||||
import { listSavedViews, type SavedView } from "@/lib/saved-views-client";
|
||||
@ -146,9 +147,9 @@ function DashboardContent() {
|
||||
title="Dashboard"
|
||||
description="Operational entry point for recent notes, active workspaces, and agent-relevant follow-ups."
|
||||
actions={
|
||||
<button className="btn btn-primary" onClick={() => setShowCreateNote(true)}>
|
||||
<Button onClick={() => setShowCreateNote(true)}>
|
||||
New Note
|
||||
</button>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<IntakeUrlBar
|
||||
@ -158,10 +159,12 @@ function DashboardContent() {
|
||||
|
||||
<section style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", gap: "var(--nl-space-4)" }}>
|
||||
{summaryCards.map((card) => (
|
||||
<Link key={card.id} href={card.href} className="surface-card" style={{ padding: "var(--nl-space-5)" }}>
|
||||
<Card key={card.id} padding="none">
|
||||
<Link href={card.href} style={{ display: "block", padding: "var(--nl-space-5)" }}>
|
||||
<div style={{ color: "var(--nl-text-secondary)" }}>{card.label}</div>
|
||||
<div style={{ fontSize: "var(--nl-fs-3xl)", fontWeight: 700, marginTop: "var(--nl-space-2)" }}>{card.value}</div>
|
||||
</Link>
|
||||
</Card>
|
||||
))}
|
||||
</section>
|
||||
|
||||
@ -172,10 +175,10 @@ function DashboardContent() {
|
||||
Seeds a "Getting started" workspace with sample notes and one agent item in the review queue (backend flag{" "}
|
||||
<code style={{ fontSize: "var(--nl-fs-sm)" }}>onboarding.seed_enabled</code>).
|
||||
</p>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={seeding}
|
||||
loading={seeding}
|
||||
onClick={() => {
|
||||
setSeeding(true);
|
||||
void seedOnboardingWorkspace()
|
||||
@ -187,8 +190,8 @@ function DashboardContent() {
|
||||
.finally(() => setSeeding(false));
|
||||
}}
|
||||
>
|
||||
{seeding ? "Creating…" : "Seed sample workspace"}
|
||||
</button>
|
||||
Seed sample workspace
|
||||
</Button>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ import { ArtifactPanel } from "@/components/ArtifactPanel";
|
||||
import { AgentTimeline } from "@/components/AgentTimeline";
|
||||
import { SmartActionsPanel } from "@/components/SmartActionsPanel";
|
||||
import { LinkNoteModal } from "@/components/LinkNoteModal";
|
||||
import { Badge, Button } from "@/components/ui/Primitives";
|
||||
import {
|
||||
archiveNote,
|
||||
createNoteArtifact,
|
||||
@ -229,31 +230,31 @@ export default function NoteDetailPage() {
|
||||
actions={
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-2)", alignItems: "center" }}>
|
||||
{isSaving ? (
|
||||
<div className="badge">Saving</div>
|
||||
<Badge>Saving</Badge>
|
||||
) : (
|
||||
<Link href="/reviews" className="badge">
|
||||
{`Review: ${note.metadata.reviewState}`}
|
||||
</Link>
|
||||
)}
|
||||
<button className="btn btn-secondary" onClick={handleSummarize}>
|
||||
<Button variant="secondary" onClick={handleSummarize}>
|
||||
Summarize
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary" onClick={() => void handleSuggestTitle()}>
|
||||
</Button>
|
||||
<Button type="button" variant="secondary" onClick={() => void handleSuggestTitle()}>
|
||||
Suggest title
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary" onClick={() => void handleCopyContextPack()}>
|
||||
</Button>
|
||||
<Button type="button" variant="secondary" onClick={() => void handleCopyContextPack()}>
|
||||
Copy context pack
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary" onClick={() => void handleCreateShareLink()}>
|
||||
</Button>
|
||||
<Button type="button" variant="secondary" onClick={() => void handleCreateShareLink()}>
|
||||
Copy share link
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => setShowLinkNote(true)}>
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => setShowLinkNote(true)}>
|
||||
Link Note
|
||||
</button>
|
||||
</Button>
|
||||
{note.status === "archived" ? (
|
||||
<button className="btn btn-primary" onClick={handleRestore}>Restore</button>
|
||||
<Button onClick={handleRestore}>Restore</Button>
|
||||
) : (
|
||||
<button className="btn btn-secondary" onClick={handleArchive}>Archive</button>
|
||||
<Button variant="secondary" onClick={handleArchive}>Archive</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { Button } from "@/components/ui/Primitives";
|
||||
import { PalacePanel } from "@/components/PalacePanel";
|
||||
import { PalaceStats } from "@/components/PalaceStats";
|
||||
import { KnowledgeGraphView } from "@/components/KnowledgeGraphView";
|
||||
@ -63,14 +64,14 @@ export default function PalacePage() {
|
||||
|
||||
<nav style={{ display: "flex", gap: "var(--nl-space-2)" }} aria-label="Palace tabs">
|
||||
{tabs.map((t) => (
|
||||
<button
|
||||
<Button
|
||||
key={t.key}
|
||||
onClick={() => setTab(t.key)}
|
||||
className={tab === t.key ? "btn btn-primary" : "btn"}
|
||||
variant={tab === t.key ? "primary" : "secondary"}
|
||||
aria-current={tab === t.key ? "page" : undefined}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Sparkles, FileText, Image, Layers, Trash2 } from "lucide-react";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { Badge, Button, Card } from "@/components/ui/Primitives";
|
||||
import { listPromptTemplates, deletePromptTemplate } from "@/lib/prompt-client";
|
||||
import { toast } from "@/lib/toast";
|
||||
import type { PromptTemplate, PromptCategory } from "@/lib/types";
|
||||
@ -66,29 +67,31 @@ export default function PromptsPage() {
|
||||
title="Prompt Templates"
|
||||
description="Browse built-in and custom Smart Action templates."
|
||||
actions={
|
||||
<div className="badge">
|
||||
<Badge>
|
||||
<Sparkles size={14} /> {templates.length} templates
|
||||
</div>
|
||||
</Badge>
|
||||
}
|
||||
>
|
||||
{/* Category filter */}
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap", marginBottom: "var(--nl-space-4)" }}>
|
||||
<button
|
||||
className={`badge ${activeCategory === "all" ? "" : "surface-muted"}`}
|
||||
<Button
|
||||
variant={activeCategory === "all" ? "primary" : "secondary"}
|
||||
size="sm"
|
||||
onClick={() => setActiveCategory("all")}
|
||||
aria-label="All categories"
|
||||
>
|
||||
All ({templates.length})
|
||||
</button>
|
||||
</Button>
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
<Button
|
||||
key={cat}
|
||||
className={`badge ${activeCategory === cat ? "" : "surface-muted"}`}
|
||||
variant={activeCategory === cat ? "primary" : "secondary"}
|
||||
size="sm"
|
||||
onClick={() => setActiveCategory(cat)}
|
||||
aria-label={`Filter: ${CATEGORY_LABELS[cat]}`}
|
||||
>
|
||||
{CATEGORY_LABELS[cat]} ({templates.filter((t) => t.category === cat).length})
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -106,13 +109,13 @@ export default function PromptsPage() {
|
||||
{builtIn.map((t) => {
|
||||
const Icon = INPUT_ICONS[t.inputType] ?? FileText;
|
||||
return (
|
||||
<div key={t.id} className="surface-card" style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}>
|
||||
<Card key={t.id} style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<span style={{ fontWeight: 600, display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<Icon size={14} />
|
||||
{t.name}
|
||||
</span>
|
||||
<span className="badge" style={{ fontSize: "var(--nl-fs-xs)" }}>{CATEGORY_LABELS[t.category]}</span>
|
||||
<Badge>{CATEGORY_LABELS[t.category]}</Badge>
|
||||
</div>
|
||||
<p style={{ fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)", margin: 0 }}>{t.description}</p>
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-2)", fontSize: "var(--nl-fs-xs)", color: "var(--nl-text-secondary)" }}>
|
||||
@ -120,7 +123,7 @@ export default function PromptsPage() {
|
||||
<span>·</span>
|
||||
<span>Output: {t.outputType}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@ -135,20 +138,20 @@ export default function PromptsPage() {
|
||||
{custom.map((t) => {
|
||||
const Icon = INPUT_ICONS[t.inputType] ?? FileText;
|
||||
return (
|
||||
<div key={t.id} className="surface-card" style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}>
|
||||
<Card key={t.id} style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<span style={{ fontWeight: 600, display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<Icon size={14} />
|
||||
{t.name}
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
style={{ fontSize: "var(--nl-fs-xs)", padding: "2px 6px" }}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => void handleDelete(t.id)}
|
||||
aria-label={`Delete template: ${t.name}`}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<p style={{ fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)", margin: 0 }}>{t.description}</p>
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-2)", fontSize: "var(--nl-fs-xs)", color: "var(--nl-text-secondary)" }}>
|
||||
@ -156,7 +159,7 @@ export default function PromptsPage() {
|
||||
<span>·</span>
|
||||
<span>Output: {t.outputType}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
import { useTheme } from "@/lib/use-theme";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { getFeedbackClient } from "@/lib/feedback-client";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { NOTES_API_URL, PLATFORM_SERVICE_URL, MCP_SERVER_URL, PRODUCT_ID } from "@/lib/product-config";
|
||||
@ -64,57 +65,57 @@ export default function SettingsPage() {
|
||||
title="Settings"
|
||||
description="Account, preferences, feedback, and session management."
|
||||
actions={
|
||||
<button onClick={logout} style={{ padding: "6px 14px", background: "var(--nl-danger-muted)", color: "var(--nl-danger)", border: "none", borderRadius: "var(--nl-radius-sm)", fontSize: "var(--nl-fs-sm)" }}>
|
||||
<Button onClick={logout} variant="secondary" size="sm" className="bg-[var(--nl-danger-muted)] text-[var(--nl-danger)] border-transparent hover:bg-[var(--nl-danger-muted)]">
|
||||
Sign out
|
||||
</button>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<section style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(320px, 1fr))", gap: "var(--nl-space-4)" }}>
|
||||
{/* Profile */}
|
||||
<article className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
<Card style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
<strong>Profile</strong>
|
||||
<div style={{ color: "var(--nl-text-secondary)" }}>
|
||||
{user?.name ?? "—"} · {user?.email ?? "—"} · {user?.role ?? "—"}
|
||||
</div>
|
||||
</article>
|
||||
</Card>
|
||||
|
||||
{/* Appearance */}
|
||||
<article className="surface-card" style={{ padding: "var(--nl-space-5)", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Card style={{ padding: "var(--nl-space-5)", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<div style={{ display: "grid", gap: "var(--nl-space-2)" }}>
|
||||
<strong>Appearance</strong>
|
||||
<div style={{ color: "var(--nl-text-secondary)" }}>Switch between dark and light mode</div>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
onClick={toggle}
|
||||
aria-label={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
|
||||
className="surface-muted"
|
||||
style={{ padding: "6px 14px", border: "none", fontSize: "var(--nl-fs-sm)" }}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
{theme === "dark" ? "Light" : "Dark"}
|
||||
</button>
|
||||
</article>
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
{/* Change password */}
|
||||
<article className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
<Card style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
<strong>Change password</strong>
|
||||
{error && <div style={{ color: "var(--nl-danger)", fontSize: "var(--nl-fs-sm)" }}>{error}</div>}
|
||||
{success && <div style={{ color: "var(--nl-status-success)", fontSize: "var(--nl-fs-sm)" }}>{success}</div>}
|
||||
<form onSubmit={handleChangePassword} style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
<input className="input-shell" type="password" placeholder="Current password" aria-label="Current password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} required />
|
||||
<input className="input-shell" type="password" placeholder="New password" aria-label="New password" minLength={8} value={newPassword} onChange={(e) => setNewPassword(e.target.value)} required />
|
||||
<button type="submit" disabled={isLoading} style={{ padding: "8px 16px", background: "var(--nl-accent-primary)", color: "var(--nl-on-accent)", border: "none", borderRadius: "var(--nl-radius-sm)", fontWeight: 600, justifySelf: "start" }}>
|
||||
{isLoading ? "Updating…" : "Update password"}
|
||||
</button>
|
||||
<Button type="submit" disabled={isLoading} loading={isLoading} style={{ justifySelf: "start" }}>
|
||||
Update password
|
||||
</Button>
|
||||
</form>
|
||||
</article>
|
||||
</Card>
|
||||
|
||||
{/* Danger zone */}
|
||||
<article className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
<Card style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
<strong>Danger zone</strong>
|
||||
<button onClick={handleDeleteAccount} style={{ padding: "8px 16px", background: "var(--nl-danger-muted)", color: "var(--nl-danger)", border: "none", borderRadius: "var(--nl-radius-sm)", fontWeight: 600, justifySelf: "start" }}>
|
||||
<Button onClick={handleDeleteAccount} variant="secondary" className="bg-[var(--nl-danger-muted)] text-[var(--nl-danger)] border-transparent hover:bg-[var(--nl-danger-muted)]" style={{ justifySelf: "start" }}>
|
||||
Delete account
|
||||
</button>
|
||||
</article>
|
||||
</Button>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)", marginTop: "var(--nl-space-4)" }}>
|
||||
@ -155,9 +156,9 @@ MCP API base: ${MCP_SERVER_URL}
|
||||
Failed writes are retried from local storage via <code>@bytelyst/offline-queue</code> (storage key{" "}
|
||||
<code>{`${PRODUCT_ID}_offline_queue`}</code>). Reload or return online to flush.
|
||||
</p>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
variant="secondary"
|
||||
style={{ justifySelf: "start" }}
|
||||
onClick={() => {
|
||||
try {
|
||||
@ -169,7 +170,7 @@ MCP API base: ${MCP_SERVER_URL}
|
||||
}}
|
||||
>
|
||||
Verify offline queue
|
||||
</button>
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
{/* Feedback */}
|
||||
@ -185,13 +186,14 @@ MCP API base: ${MCP_SERVER_URL}
|
||||
<input className="input-shell" placeholder="Title" aria-label="Feedback title" value={feedbackTitle} onChange={(e) => setFeedbackTitle(e.target.value)} />
|
||||
</div>
|
||||
<textarea className="input-shell" placeholder="Details (optional)" aria-label="Feedback details" rows={3} value={feedbackBody} onChange={(e) => setFeedbackBody(e.target.value)} style={{ resize: "vertical" }} />
|
||||
<button
|
||||
<Button
|
||||
onClick={handleSubmitFeedback}
|
||||
disabled={submittingFeedback || !feedbackTitle.trim()}
|
||||
style={{ padding: "8px 16px", background: "var(--nl-accent-primary)", color: "var(--nl-on-accent)", border: "none", borderRadius: "var(--nl-radius-sm)", fontWeight: 600, justifySelf: "start" }}
|
||||
loading={submittingFeedback}
|
||||
style={{ justifySelf: "start" }}
|
||||
>
|
||||
{submittingFeedback ? "Sending…" : "Send feedback"}
|
||||
</button>
|
||||
Send feedback
|
||||
</Button>
|
||||
</section>
|
||||
</AppShell>
|
||||
);
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Brain, Plus, Loader2, AlertTriangle } from "lucide-react";
|
||||
import { Brain, Plus, AlertTriangle } from "lucide-react";
|
||||
import { Button } from "@/components/ui/Primitives";
|
||||
import { getKnowledgeGaps } from "@/lib/prompt-client";
|
||||
import { toast } from "@/lib/toast";
|
||||
import type { KnowledgeGap } from "@/lib/types";
|
||||
@ -44,16 +45,16 @@ export default function KnowledgeGapsPage() {
|
||||
<Brain size={20} />
|
||||
<h1 style={{ fontSize: "var(--nl-fs-xl)", fontWeight: 700, margin: 0 }}>Knowledge Gaps</h1>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
<Button
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
onClick={() => void handleAnalyze()}
|
||||
aria-label={loading ? "Analyzing..." : "Analyze knowledge gaps"}
|
||||
style={{ display: "flex", alignItems: "center", gap: "var(--nl-space-1)" }}
|
||||
>
|
||||
{loading ? <Loader2 size={16} className="animate-spin" /> : <Brain size={16} />}
|
||||
{loading ? "Analyzing..." : "Re-analyze"}
|
||||
</button>
|
||||
{!loading ? <Brain size={16} /> : null}
|
||||
Re-analyze
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Topic coverage map */}
|
||||
@ -89,8 +90,8 @@ export default function KnowledgeGapsPage() {
|
||||
{gap.description}
|
||||
</p>
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-2)" }}>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
<Button
|
||||
variant="secondary"
|
||||
style={{ fontSize: "var(--nl-fs-sm)", display: "flex", alignItems: "center", gap: "var(--nl-space-1)" }}
|
||||
onClick={() => {
|
||||
toast.info(`Create note: "${gap.suggestedTitle}" (navigate to create)`);
|
||||
@ -98,7 +99,7 @@ export default function KnowledgeGapsPage() {
|
||||
aria-label={`Create note: ${gap.suggestedTitle}`}
|
||||
>
|
||||
<Plus size={14} /> Create: {gap.suggestedTitle}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -5,6 +5,7 @@ import { useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useMemo, useState } from "react";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { CreateWorkspaceModal } from "@/components/CreateWorkspaceModal";
|
||||
import { Button } from "@/components/ui/Primitives";
|
||||
import { exportNotes, listNoteSummaries, listWorkspaceSummaries, deleteWorkspace } from "@/lib/notes-client";
|
||||
import { buildWorkspaceContextPackMarkdown } from "@/lib/context-pack";
|
||||
import { toast } from "@/lib/toast";
|
||||
@ -144,14 +145,13 @@ function WorkspacesPageInner() {
|
||||
description="Workspace-level organization, filters, and saved-view entry points for note collections."
|
||||
actions={
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-3)" }}>
|
||||
<button
|
||||
style={{ padding: "8px 16px", background: "var(--nl-accent-primary)", color: "var(--nl-on-accent)", border: "none", borderRadius: "var(--nl-radius-sm)", fontWeight: 600 }}
|
||||
<Button
|
||||
onClick={() => setShowCreate(true)}
|
||||
>
|
||||
+ Workspace
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const data = await exportNotes("json");
|
||||
@ -168,7 +168,7 @@ function WorkspacesPageInner() {
|
||||
}}
|
||||
>
|
||||
Export Notes
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@ -231,20 +231,22 @@ function WorkspacesPageInner() {
|
||||
<Link href={`/workspaces?q=${encodeURIComponent(workspace.owner)}`} style={{ color: "var(--nl-text-secondary)" }}>
|
||||
Owner: {workspace.owner}
|
||||
</Link>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
style={{ padding: "4px 10px", fontSize: "var(--nl-fs-sm)" }}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => void downloadWorkspaceContextPack(workspace.id, workspace.name)}
|
||||
>
|
||||
Context pack (.md)
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleDelete(workspace.id, workspace.name)}
|
||||
style={{ padding: "4px 10px", fontSize: "var(--nl-fs-sm)", background: "var(--nl-danger-muted)", color: "var(--nl-danger)", border: "none", borderRadius: "var(--nl-radius-sm)" }}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="bg-[var(--nl-danger-muted)] text-[var(--nl-danger)] border-transparent hover:bg-[var(--nl-danger-muted)]"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap" }}>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, type FormEvent } from "react";
|
||||
import Link from "next/link";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
@ -15,7 +16,8 @@ export default function ForgotPasswordPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="surface-card" style={{ padding: "var(--nl-space-8)", display: "grid", gap: "var(--nl-space-5)" }}>
|
||||
<Card padding="lg">
|
||||
<form onSubmit={handleSubmit} style={{ display: "grid", gap: "var(--nl-space-5)" }}>
|
||||
<h1 style={{ margin: 0, fontSize: "var(--nl-fs-xl)" }}>Reset password</h1>
|
||||
<p style={{ margin: 0, color: "var(--nl-text-secondary)", fontSize: "var(--nl-fs-sm)" }}>
|
||||
Enter your email and we'll send a reset link if the account exists.
|
||||
@ -45,18 +47,18 @@ export default function ForgotPasswordPage() {
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="input-shell"
|
||||
style={{ background: "var(--nl-accent-primary)", color: "var(--nl-on-accent)", border: "none", fontWeight: 600, cursor: isLoading ? "wait" : "pointer" }}
|
||||
loading={isLoading}
|
||||
>
|
||||
{isLoading ? "Sending…" : "Send reset link"}
|
||||
</button>
|
||||
Send reset link
|
||||
</Button>
|
||||
|
||||
<p style={{ textAlign: "center", fontSize: "var(--nl-fs-sm)" }}>
|
||||
<Link href="/login" style={{ color: "var(--nl-accent-primary)" }}>Back to sign in</Link>
|
||||
</p>
|
||||
</form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { useState, type FormEvent } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
|
||||
export default function LoginPage() {
|
||||
@ -19,7 +20,8 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="surface-card" style={{ padding: "var(--nl-space-8)", display: "grid", gap: "var(--nl-space-5)" }}>
|
||||
<Card padding="lg">
|
||||
<form onSubmit={handleSubmit} style={{ display: "grid", gap: "var(--nl-space-5)" }}>
|
||||
<h1 style={{ margin: 0, fontSize: "var(--nl-fs-xl)" }}>Sign in</h1>
|
||||
|
||||
{error && (
|
||||
@ -52,19 +54,19 @@ export default function LoginPage() {
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="input-shell"
|
||||
style={{ background: "var(--nl-accent-primary)", color: "var(--nl-on-accent)", border: "none", fontWeight: 600, cursor: isLoading ? "wait" : "pointer" }}
|
||||
loading={isLoading}
|
||||
>
|
||||
{isLoading ? "Signing in…" : "Sign in"}
|
||||
</button>
|
||||
Sign in
|
||||
</Button>
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "space-between", fontSize: "var(--nl-fs-sm)" }}>
|
||||
<Link href="/forgot-password" style={{ color: "var(--nl-accent-primary)" }}>Forgot password?</Link>
|
||||
<Link href="/register" style={{ color: "var(--nl-accent-primary)" }}>Create account</Link>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { useState, type FormEvent } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
|
||||
export default function RegisterPage() {
|
||||
@ -20,7 +21,8 @@ export default function RegisterPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="surface-card" style={{ padding: "var(--nl-space-8)", display: "grid", gap: "var(--nl-space-5)" }}>
|
||||
<Card padding="lg">
|
||||
<form onSubmit={handleSubmit} style={{ display: "grid", gap: "var(--nl-space-5)" }}>
|
||||
<h1 style={{ margin: 0, fontSize: "var(--nl-fs-xl)" }}>Create account</h1>
|
||||
|
||||
{error && (
|
||||
@ -66,19 +68,19 @@ export default function RegisterPage() {
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="input-shell"
|
||||
style={{ background: "var(--nl-accent-primary)", color: "var(--nl-on-accent)", border: "none", fontWeight: 600, cursor: isLoading ? "wait" : "pointer" }}
|
||||
loading={isLoading}
|
||||
>
|
||||
{isLoading ? "Creating account…" : "Create account"}
|
||||
</button>
|
||||
Create account
|
||||
</Button>
|
||||
|
||||
<p style={{ textAlign: "center", fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}>
|
||||
Already have an account?{" "}
|
||||
<Link href="/login" style={{ color: "var(--nl-accent-primary)" }}>Sign in</Link>
|
||||
</p>
|
||||
</form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@ -18,6 +18,12 @@
|
||||
--nl-warning-muted: color-mix(in srgb, var(--nl-status-warning) 14%, transparent);
|
||||
--nl-info-muted: color-mix(in srgb, var(--nl-accent-primary) 12%, transparent);
|
||||
--nl-command-shadow: 0 24px 80px color-mix(in srgb, black 45%, transparent);
|
||||
--bl-accent: var(--nl-accent-primary);
|
||||
--bl-surface-card: var(--nl-surface-card);
|
||||
--bl-surface-muted: var(--nl-surface-muted);
|
||||
--bl-text-primary: var(--nl-text-primary);
|
||||
--bl-text-secondary: var(--nl-text-secondary);
|
||||
--bl-border: var(--nl-border-default);
|
||||
}
|
||||
|
||||
* {
|
||||
@ -161,8 +167,8 @@ button:active:not(:disabled), [role="button"]:active:not(:disabled) {
|
||||
}
|
||||
.skip-to-content:focus {
|
||||
position: fixed; top: 8px; left: 8px; width: auto; height: auto;
|
||||
padding: 12px 24px; background: var(--nl-bg-elevated); color: var(--nl-accent);
|
||||
border: 2px solid var(--nl-accent); border-radius: 8px; font-size: 14px; font-weight: 600; text-decoration: none;
|
||||
padding: 12px 24px; background: var(--nl-bg-elevated); color: var(--nl-accent-primary);
|
||||
border: 2px solid var(--nl-accent-primary); border-radius: 8px; font-size: 14px; font-weight: 600; text-decoration: none;
|
||||
}
|
||||
|
||||
/* Responsive sidebar */
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { type ReactNode, useState, useCallback, useEffect } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Sidebar } from "@/components/Sidebar";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
|
||||
export function AppShell({
|
||||
title,
|
||||
@ -30,14 +31,16 @@ export function AppShell({
|
||||
<a href="#main-content" className="skip-link">
|
||||
Skip to main content
|
||||
</a>
|
||||
<button
|
||||
<Button
|
||||
className="sidebar-toggle"
|
||||
onClick={toggle}
|
||||
aria-label={sidebarOpen ? "Close menu" : "Open menu"}
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
{sidebarOpen ? "\u2715" : "\u2630"}
|
||||
</button>
|
||||
</Button>
|
||||
<div
|
||||
className={`sidebar-overlay${sidebarOpen ? " open" : ""}`}
|
||||
onClick={close}
|
||||
@ -46,8 +49,8 @@ export function AppShell({
|
||||
<Sidebar open={sidebarOpen} />
|
||||
<main id="main-content" className="main-panel" tabIndex={-1} aria-labelledby="page-title">
|
||||
<div className="page-grid">
|
||||
<header
|
||||
className="surface-card"
|
||||
<header>
|
||||
<Card
|
||||
style={{ padding: "var(--nl-space-6)", display: "flex", justifyContent: "space-between", gap: "var(--nl-space-4)", alignItems: "start", flexWrap: "wrap" }}
|
||||
>
|
||||
<div style={{ display: "grid", gap: "var(--nl-space-2)" }}>
|
||||
@ -60,6 +63,7 @@ export function AppShell({
|
||||
<div style={{ color: "var(--nl-text-secondary)", maxWidth: 720 }}>{description}</div>
|
||||
</div>
|
||||
{actions ? <div aria-label="Page actions">{actions}</div> : null}
|
||||
</Card>
|
||||
</header>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { createNote } from "@/lib/notes-client";
|
||||
import { NOTE_TEMPLATES } from "@/lib/note-templates";
|
||||
import type { WorkspaceSummary } from "@/lib/types";
|
||||
@ -74,14 +75,14 @@ export function CreateNoteModal({ workspaces, defaultWorkspaceId, onCreated, onC
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
padding="lg"
|
||||
style={{ width: "100%", maxWidth: 520 }}
|
||||
>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="surface-card"
|
||||
style={{
|
||||
padding: "var(--nl-space-6)",
|
||||
width: "100%",
|
||||
maxWidth: 520,
|
||||
display: "grid",
|
||||
gap: "var(--nl-space-4)",
|
||||
}}
|
||||
@ -164,14 +165,15 @@ export function CreateNoteModal({ workspaces, defaultWorkspaceId, onCreated, onC
|
||||
</label>
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: "var(--nl-space-3)" }}>
|
||||
<button type="button" className="btn btn-secondary" onClick={onClose}>
|
||||
<Button type="button" variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={!canSubmit || saving}>
|
||||
{saving ? "Creating..." : "Create"}
|
||||
</button>
|
||||
</Button>
|
||||
<Button type="submit" disabled={!canSubmit || saving} loading={saving}>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { createWorkspace } from "@/lib/notes-client";
|
||||
|
||||
interface Props {
|
||||
@ -39,11 +40,14 @@ export function CreateWorkspaceModal({ onCreated, onClose }: Props) {
|
||||
<div
|
||||
style={{ position: "fixed", inset: 0, background: "var(--nl-overlay-scrim)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000 }}
|
||||
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
||||
>
|
||||
<Card
|
||||
padding="lg"
|
||||
style={{ width: "100%", maxWidth: 480 }}
|
||||
>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="surface-card"
|
||||
style={{ padding: "var(--nl-space-6)", width: "100%", maxWidth: 480, display: "grid", gap: "var(--nl-space-4)" }}
|
||||
style={{ display: "grid", gap: "var(--nl-space-4)" }}
|
||||
>
|
||||
<div style={{ fontSize: "var(--nl-fs-xl)", fontWeight: 700 }}>Create Workspace</div>
|
||||
|
||||
@ -60,12 +64,11 @@ export function CreateWorkspaceModal({ onCreated, onClose }: Props) {
|
||||
</label>
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: "var(--nl-space-3)" }}>
|
||||
<button type="button" className="surface-muted" style={{ padding: "8px 16px", border: "none" }} onClick={onClose}>Cancel</button>
|
||||
<button type="submit" disabled={!name.trim() || saving} style={{ padding: "8px 16px", background: "var(--nl-accent-primary)", color: "var(--nl-on-accent)", border: "none", borderRadius: "var(--nl-radius-sm)", fontWeight: 600 }}>
|
||||
{saving ? "Creating…" : "Create"}
|
||||
</button>
|
||||
<Button type="button" variant="secondary" onClick={onClose}>Cancel</Button>
|
||||
<Button type="submit" disabled={!name.trim() || saving} loading={saving}>Create</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { extractSuggestedTasks } from "@/lib/extraction-client";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { createNoteTask } from "@/lib/notes-client";
|
||||
import { toast } from "@/lib/toast";
|
||||
import type { NoteTask } from "@/lib/types";
|
||||
@ -72,17 +73,18 @@ export function ExtractedTasksPanel({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
<Card style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--nl-space-3)", alignItems: "center", flexWrap: "wrap" }}>
|
||||
<div style={{ fontWeight: 700 }}>Suggested tasks (AI)</div>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
variant="secondary"
|
||||
disabled={scanning}
|
||||
loading={scanning}
|
||||
onClick={() => void handleScan()}
|
||||
>
|
||||
{scanning ? "Scanning…" : "Scan note for tasks"}
|
||||
</button>
|
||||
Scan note for tasks
|
||||
</Button>
|
||||
</div>
|
||||
<p style={{ margin: 0, color: "var(--nl-text-secondary)", fontSize: "var(--nl-fs-sm)" }}>
|
||||
Runs extraction on demand. Accept adds a backend task; dismiss only hides the suggestion for this session.
|
||||
@ -93,28 +95,28 @@ export function ExtractedTasksPanel({
|
||||
<li key={task.id} className="surface-muted" style={{ padding: "var(--nl-space-3)", display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap", alignItems: "center", justifyContent: "space-between" }}>
|
||||
<span>{task.title}</span>
|
||||
<span style={{ display: "flex", gap: 8 }}>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
style={{ padding: "6px 10px", fontSize: "var(--nl-fs-sm)" }}
|
||||
size="sm"
|
||||
disabled={acceptingId === task.id}
|
||||
loading={acceptingId === task.id}
|
||||
onClick={() => void handleAccept(task)}
|
||||
>
|
||||
{acceptingId === task.id ? "Adding…" : "Accept"}
|
||||
</button>
|
||||
<button
|
||||
Accept
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
style={{ padding: "6px 10px", fontSize: "var(--nl-fs-sm)" }}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => handleDismiss(task.id)}
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</Button>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { submitIntake, type IntakeContentType } from "@/lib/intake-client";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
||||
@ -15,11 +16,11 @@ const CONTENT_TYPE_LABELS: Record<IntakeContentType, string> = {
|
||||
};
|
||||
|
||||
const CONTENT_TYPE_COLORS: Record<IntakeContentType, string> = {
|
||||
youtube: "var(--nl-red)",
|
||||
article: "var(--nl-primary)",
|
||||
pdf: "var(--nl-orange)",
|
||||
tweet: "var(--nl-blue)",
|
||||
reddit: "var(--nl-orange)",
|
||||
youtube: "var(--nl-danger)",
|
||||
article: "var(--nl-accent-primary)",
|
||||
pdf: "var(--nl-warning)",
|
||||
tweet: "var(--nl-accent-secondary)",
|
||||
reddit: "var(--nl-warning)",
|
||||
github: "var(--nl-text-primary)",
|
||||
generic: "var(--nl-text-secondary)",
|
||||
};
|
||||
@ -71,8 +72,7 @@ export function IntakeUrlBar({ workspaceId, onIntakeSubmitted }: IntakeUrlBarPro
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="surface-card"
|
||||
<Card
|
||||
style={{
|
||||
padding: "var(--nl-space-4)",
|
||||
display: "flex",
|
||||
@ -92,9 +92,9 @@ export function IntakeUrlBar({ workspaceId, onIntakeSubmitted }: IntakeUrlBarPro
|
||||
width: "100%",
|
||||
padding: "var(--nl-space-3) var(--nl-space-4)",
|
||||
paddingRight: detectedType ? "5rem" : "var(--nl-space-4)",
|
||||
border: "1px solid var(--nl-border)",
|
||||
border: "1px solid var(--nl-border-default)",
|
||||
borderRadius: "var(--nl-radius-md)",
|
||||
background: "var(--nl-surface)",
|
||||
background: "var(--nl-input-bg)",
|
||||
color: "var(--nl-text-primary)",
|
||||
fontSize: "var(--nl-fs-base)",
|
||||
}}
|
||||
@ -119,16 +119,16 @@ export function IntakeUrlBar({ workspaceId, onIntakeSubmitted }: IntakeUrlBarPro
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={loading || !url.trim() || !isValidUrl(url)}
|
||||
loading={loading}
|
||||
onClick={() => void handleSubmit()}
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
aria-label="Process URL"
|
||||
>
|
||||
{loading ? "Processing…" : "Process"}
|
||||
</button>
|
||||
</div>
|
||||
Process
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { queryEntity, getEntityTimeline, getKGContradictions, type PalaceKGTriple } from "@/lib/palace-client";
|
||||
|
||||
interface KnowledgeGraphViewProps {
|
||||
@ -56,7 +57,7 @@ export function KnowledgeGraphView({ wingId }: KnowledgeGraphViewProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
<Card style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
<div style={{ fontWeight: 700, fontSize: "var(--nl-text-lg)" }}>Knowledge Graph</div>
|
||||
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-2)" }}>
|
||||
@ -70,12 +71,12 @@ export function KnowledgeGraphView({ wingId }: KnowledgeGraphViewProps) {
|
||||
className="input"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<button onClick={handleQuery} className="btn btn-primary" aria-label="Query entity">
|
||||
<Button onClick={handleQuery} aria-label="Query entity">
|
||||
Query
|
||||
</button>
|
||||
<button onClick={handleTimeline} className="btn" aria-label="Show timeline">
|
||||
</Button>
|
||||
<Button onClick={handleTimeline} variant="secondary" aria-label="Show timeline">
|
||||
Timeline
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && <div style={{ color: "var(--nl-status-error)" }}>{error}</div>}
|
||||
@ -102,7 +103,7 @@ export function KnowledgeGraphView({ wingId }: KnowledgeGraphViewProps) {
|
||||
}}
|
||||
>
|
||||
<span style={{ fontWeight: 600 }}>{t.subject}</span>
|
||||
<span style={{ color: "var(--nl-accent)" }}>{t.predicate}</span>
|
||||
<span style={{ color: "var(--nl-accent-primary)" }}>{t.predicate}</span>
|
||||
<span style={{ fontWeight: 600 }}>{t.object}</span>
|
||||
<span style={{ marginLeft: "auto", color: "var(--nl-text-secondary)", fontSize: "0.75rem" }}>
|
||||
{Math.round(t.confidence * 100)}%
|
||||
@ -136,6 +137,6 @@ export function KnowledgeGraphView({ wingId }: KnowledgeGraphViewProps) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { searchNoteSummaries, createNoteRelationship } from "@/lib/notes-client";
|
||||
import type { NoteSummary } from "@/lib/types";
|
||||
|
||||
@ -76,10 +77,9 @@ export function LinkNoteModal({ noteId, workspaceId, existingLinkedIds, onLinked
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="surface-card"
|
||||
<Card
|
||||
padding="lg"
|
||||
style={{
|
||||
padding: "var(--nl-space-6)",
|
||||
width: "100%",
|
||||
maxWidth: 520,
|
||||
display: "grid",
|
||||
@ -101,9 +101,9 @@ export function LinkNoteModal({ noteId, workspaceId, existingLinkedIds, onLinked
|
||||
style={{ flex: 1 }}
|
||||
autoFocus
|
||||
/>
|
||||
<button type="submit" className="btn btn-secondary">
|
||||
<Button type="submit" variant="secondary">
|
||||
Search
|
||||
</button>
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{searched && results.length === 0 && (
|
||||
@ -113,22 +113,24 @@ export function LinkNoteModal({ noteId, workspaceId, existingLinkedIds, onLinked
|
||||
{results.length > 0 && (
|
||||
<div style={{ maxHeight: 200, overflowY: "auto", display: "grid", gap: "var(--nl-space-2)" }}>
|
||||
{results.map((note) => (
|
||||
<button
|
||||
<Button
|
||||
key={note.id}
|
||||
type="button"
|
||||
className={selectedId === note.id ? "surface-card" : "surface-muted"}
|
||||
variant="secondary"
|
||||
className="flex-col items-start"
|
||||
style={{
|
||||
padding: "var(--nl-space-3)",
|
||||
textAlign: "left",
|
||||
cursor: "pointer",
|
||||
border: selectedId === note.id ? "2px solid var(--nl-accent-primary)" : "2px solid transparent",
|
||||
justifyContent: "start",
|
||||
height: "auto",
|
||||
}}
|
||||
onClick={() => setSelectedId(note.id)}
|
||||
aria-label={`Select note: ${note.title}`}
|
||||
>
|
||||
<div style={{ fontWeight: 600 }}>{note.title}</div>
|
||||
<div style={{ color: "var(--nl-text-secondary)", fontSize: "0.875rem" }}>{note.excerpt}</div>
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@ -147,14 +149,14 @@ export function LinkNoteModal({ noteId, workspaceId, existingLinkedIds, onLinked
|
||||
)}
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: "var(--nl-space-3)" }}>
|
||||
<button type="button" className="btn btn-secondary" onClick={onClose}>
|
||||
<Button type="button" variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary" disabled={!selectedId || saving} onClick={handleLink}>
|
||||
{saving ? "Linking..." : "Link"}
|
||||
</button>
|
||||
</div>
|
||||
</Button>
|
||||
<Button type="button" disabled={!selectedId || saving} loading={saving} onClick={handleLink}>
|
||||
Link
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { searchPalace, listMemories, type PalaceMemory } from "@/lib/palace-client";
|
||||
|
||||
interface PalacePanelProps {
|
||||
@ -51,13 +52,13 @@ export function PalacePanel({ wingId }: PalacePanelProps) {
|
||||
decisions: "var(--nl-status-info)",
|
||||
events: "var(--nl-status-success)",
|
||||
discoveries: "var(--nl-status-warning)",
|
||||
preferences: "var(--nl-accent)",
|
||||
preferences: "var(--nl-accent-primary)",
|
||||
advice: "var(--nl-text-secondary)",
|
||||
insights: "var(--nl-status-info)",
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
<Card style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
<div style={{ fontWeight: 700, fontSize: "var(--nl-text-lg)" }}>Palace Memory</div>
|
||||
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-2)" }}>
|
||||
@ -71,9 +72,9 @@ export function PalacePanel({ wingId }: PalacePanelProps) {
|
||||
className="input"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<button onClick={handleSearch} className="btn btn-primary" aria-label="Search">
|
||||
<Button onClick={handleSearch} aria-label="Search">
|
||||
Search
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && <div style={{ color: "var(--nl-status-error)" }}>{error}</div>}
|
||||
@ -112,6 +113,6 @@ export function PalacePanel({ wingId }: PalacePanelProps) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ export function PalaceStats() {
|
||||
border: "1px solid var(--nl-border-subtle)",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: "1.5rem", fontWeight: 700, color: "var(--nl-accent)" }}>{value}</div>
|
||||
<div style={{ fontSize: "1.5rem", fontWeight: 700, color: "var(--nl-accent-primary)" }}>{value}</div>
|
||||
<div style={{ fontSize: "0.75rem", color: "var(--nl-text-secondary)" }}>{label}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { Copy, FilePlus, Save, X, CheckCircle } from "lucide-react";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { toast } from "@/lib/toast";
|
||||
import type { RunPromptOutput } from "@/lib/types";
|
||||
|
||||
@ -28,18 +29,19 @@ export function PromptResultView({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-4)" }}>
|
||||
<Card style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-4)" }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<strong style={{ fontSize: "var(--nl-fs-md)" }}>Result</strong>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={onDismiss}
|
||||
aria-label="Dismiss result"
|
||||
style={{ padding: 4 }}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
@ -61,37 +63,36 @@ export function PromptResultView({
|
||||
{/* Action buttons */}
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap" }}>
|
||||
{onSaveAsNote && (
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
<Button
|
||||
onClick={() => onSaveAsNote(result.content)}
|
||||
aria-label="Save as new note"
|
||||
style={{ display: "flex", alignItems: "center", gap: "var(--nl-space-1)" }}
|
||||
>
|
||||
<FilePlus size={14} /> Save as Note
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
{onApplyToNote && (
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => onApplyToNote(result.content)}
|
||||
aria-label="Apply to current note"
|
||||
style={{ display: "flex", alignItems: "center", gap: "var(--nl-space-1)" }}
|
||||
>
|
||||
<Save size={14} /> Apply to Note
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => void handleCopy()}
|
||||
aria-label="Copy result to clipboard"
|
||||
style={{ display: "flex", alignItems: "center", gap: "var(--nl-space-1)" }}
|
||||
>
|
||||
{copied ? <CheckCircle size={14} /> : <Copy size={14} />}
|
||||
{copied ? "Copied" : "Copy"}
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={onDismiss} aria-label="Discard result">
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={onDismiss} aria-label="Discard result">
|
||||
Discard
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Metadata footer */}
|
||||
@ -102,6 +103,6 @@ export function PromptResultView({
|
||||
{result.approvalState && <span>Status: {result.approvalState}</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { Save, X } from "lucide-react";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { createPromptTemplate } from "@/lib/prompt-client";
|
||||
import { toast } from "@/lib/toast";
|
||||
import type { PromptCategory } from "@/lib/types";
|
||||
@ -68,16 +69,16 @@ export function PromptTemplateEditor({ onClose, onCreated }: PromptTemplateEdito
|
||||
aria-modal="true"
|
||||
aria-label="Create custom prompt template"
|
||||
>
|
||||
<div
|
||||
className="surface-card"
|
||||
<Card
|
||||
padding="lg"
|
||||
style={{ width: "min(90vw, 600px)", maxHeight: "90vh", overflowY: "auto", padding: "var(--nl-space-6)", display: "grid", gap: "var(--nl-space-4)" }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<strong>Create Custom Prompt</strong>
|
||||
<button className="btn btn-secondary" onClick={onClose} aria-label="Close editor" style={{ padding: 4 }}>
|
||||
<Button variant="secondary" size="sm" onClick={onClose} aria-label="Close editor" style={{ padding: 4 }}>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Name + slug */}
|
||||
@ -136,16 +137,16 @@ export function PromptTemplateEditor({ onClose, onCreated }: PromptTemplateEdito
|
||||
</label>
|
||||
|
||||
{/* Save */}
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
<Button
|
||||
disabled={saving}
|
||||
loading={saving}
|
||||
onClick={() => void handleSave()}
|
||||
aria-label={saving ? "Saving..." : "Create template"}
|
||||
style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: "var(--nl-space-2)" }}
|
||||
>
|
||||
<Save size={16} /> {saving ? "Saving..." : "Create Template"}
|
||||
</button>
|
||||
</div>
|
||||
{!saving ? <Save size={16} /> : null} Create Template
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { X, Play, Loader2 } from "lucide-react";
|
||||
import { X, Play } from "lucide-react";
|
||||
import { Badge, Button, Card } from "@/components/ui/Primitives";
|
||||
import { runPrompt } from "@/lib/prompt-client";
|
||||
import { toast } from "@/lib/toast";
|
||||
import type { PromptTemplate, RunPromptOutput } from "@/lib/types";
|
||||
@ -64,11 +65,10 @@ export function RunPromptModal({
|
||||
aria-modal="true"
|
||||
aria-label={`Run: ${template.name}`}
|
||||
>
|
||||
<div
|
||||
className="surface-card"
|
||||
<Card
|
||||
padding="lg"
|
||||
style={{
|
||||
width: "min(90vw, 520px)",
|
||||
padding: "var(--nl-space-6)",
|
||||
display: "grid",
|
||||
gap: "var(--nl-space-4)",
|
||||
}}
|
||||
@ -76,9 +76,9 @@ export function RunPromptModal({
|
||||
{/* Header */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<strong>{template.name}</strong>
|
||||
<button className="btn btn-secondary" onClick={onClose} aria-label="Close modal" style={{ padding: 4 }}>
|
||||
<Button variant="secondary" size="sm" onClick={onClose} aria-label="Close modal" style={{ padding: 4 }}>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p style={{ fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}>
|
||||
@ -121,23 +121,23 @@ export function RunPromptModal({
|
||||
|
||||
{/* Info badges */}
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap" }}>
|
||||
<span className="badge">{template.inputType}</span>
|
||||
<span className="badge">{template.outputType}</span>
|
||||
<span className="badge">{template.category}</span>
|
||||
<Badge>{template.inputType}</Badge>
|
||||
<Badge>{template.outputType}</Badge>
|
||||
<Badge>{template.category}</Badge>
|
||||
</div>
|
||||
|
||||
{/* Run button */}
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
<Button
|
||||
disabled={running}
|
||||
loading={running}
|
||||
onClick={() => void handleRun()}
|
||||
aria-label={running ? "Running prompt..." : "Run prompt"}
|
||||
style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: "var(--nl-space-2)" }}
|
||||
>
|
||||
{running ? <Loader2 size={16} className="animate-spin" /> : <Play size={16} />}
|
||||
{running ? "Running..." : "Run"}
|
||||
</button>
|
||||
</div>
|
||||
{!running ? <Play size={16} /> : null}
|
||||
Run
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { createNoteShare } from "@/lib/notes-client";
|
||||
import { exportNoteText, shareNoteWithUser } from "@/lib/intake-client";
|
||||
import { getWebAppOrigin } from "@/lib/product-config";
|
||||
@ -107,45 +108,42 @@ export function ShareDialog({ noteId, workspaceId, noteTitle, onClose }: ShareDi
|
||||
role="dialog"
|
||||
aria-label="Share note"
|
||||
>
|
||||
<div
|
||||
className="surface-card"
|
||||
<Card
|
||||
padding="lg"
|
||||
style={{
|
||||
width: "min(480px, 90vw)",
|
||||
padding: "var(--nl-space-6)",
|
||||
display: "grid",
|
||||
gap: "var(--nl-space-4)",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<h2 style={{ margin: 0, fontSize: "var(--nl-fs-xl)" }}>Share Note</h2>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="Close share dialog"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
color: "var(--nl-text-secondary)",
|
||||
fontSize: "var(--nl-fs-xl)",
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap" }}>
|
||||
{tabs.map((t) => (
|
||||
<button
|
||||
<Button
|
||||
key={t.key}
|
||||
type="button"
|
||||
onClick={() => setTab(t.key)}
|
||||
className={tab === t.key ? "btn btn-primary" : "btn btn-secondary"}
|
||||
style={{ fontSize: "var(--nl-fs-sm)" }}
|
||||
variant={tab === t.key ? "primary" : "secondary"}
|
||||
size="sm"
|
||||
aria-label={t.label}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -154,9 +152,9 @@ export function ShareDialog({ noteId, workspaceId, noteTitle, onClose }: ShareDi
|
||||
<p style={{ margin: 0, color: "var(--nl-text-secondary)" }}>
|
||||
Generate a public read-only link anyone can view.
|
||||
</p>
|
||||
<button type="button" className="btn btn-primary" disabled={loading} onClick={() => void handleCopyLink()} aria-label="Copy share link">
|
||||
{loading ? "Generating…" : "Copy Share Link"}
|
||||
</button>
|
||||
<Button type="button" disabled={loading} loading={loading} onClick={() => void handleCopyLink()} aria-label="Copy share link">
|
||||
Copy Share Link
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -173,9 +171,9 @@ export function ShareDialog({ noteId, workspaceId, noteTitle, onClose }: ShareDi
|
||||
aria-label="User ID to share with"
|
||||
style={{
|
||||
padding: "var(--nl-space-3)",
|
||||
border: "1px solid var(--nl-border)",
|
||||
border: "1px solid var(--nl-border-default)",
|
||||
borderRadius: "var(--nl-radius-md)",
|
||||
background: "var(--nl-surface)",
|
||||
background: "var(--nl-input-bg)",
|
||||
color: "var(--nl-text-primary)",
|
||||
}}
|
||||
/>
|
||||
@ -185,9 +183,9 @@ export function ShareDialog({ noteId, workspaceId, noteTitle, onClose }: ShareDi
|
||||
aria-label="Permission level"
|
||||
style={{
|
||||
padding: "var(--nl-space-3)",
|
||||
border: "1px solid var(--nl-border)",
|
||||
border: "1px solid var(--nl-border-default)",
|
||||
borderRadius: "var(--nl-radius-md)",
|
||||
background: "var(--nl-surface)",
|
||||
background: "var(--nl-input-bg)",
|
||||
color: "var(--nl-text-primary)",
|
||||
}}
|
||||
>
|
||||
@ -195,9 +193,9 @@ export function ShareDialog({ noteId, workspaceId, noteTitle, onClose }: ShareDi
|
||||
<option value="comment">Can comment</option>
|
||||
<option value="edit">Can edit</option>
|
||||
</select>
|
||||
<button type="button" className="btn btn-primary" disabled={loading} onClick={() => void handleShareWithUser()} aria-label="Share with user">
|
||||
{loading ? "Sharing…" : "Share"}
|
||||
</button>
|
||||
<Button type="button" disabled={loading} loading={loading} onClick={() => void handleShareWithUser()} aria-label="Share with user">
|
||||
Share
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -206,9 +204,9 @@ export function ShareDialog({ noteId, workspaceId, noteTitle, onClose }: ShareDi
|
||||
<p style={{ margin: 0, color: "var(--nl-text-secondary)" }}>
|
||||
Copy the note content as plain text — paste into email, WhatsApp, Messages, etc.
|
||||
</p>
|
||||
<button type="button" className="btn btn-primary" disabled={loading} onClick={() => void handleCopyText()} aria-label="Copy note text">
|
||||
{loading ? "Copying…" : "Copy Note Text"}
|
||||
</button>
|
||||
<Button type="button" disabled={loading} loading={loading} onClick={() => void handleCopyText()} aria-label="Copy note text">
|
||||
Copy Note Text
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -217,17 +215,17 @@ export function ShareDialog({ noteId, workspaceId, noteTitle, onClose }: ShareDi
|
||||
<p style={{ margin: 0, color: "var(--nl-text-secondary)" }}>
|
||||
Open your device's native share sheet (AirDrop, Messages, email, etc.)
|
||||
</p>
|
||||
<button type="button" className="btn btn-primary" disabled={loading} onClick={() => void handleNativeShare()} aria-label="Open share sheet">
|
||||
{loading ? "Opening…" : "Open Share Sheet"}
|
||||
</button>
|
||||
<Button type="button" disabled={loading} loading={loading} onClick={() => void handleNativeShare()} aria-label="Open share sheet">
|
||||
Open Share Sheet
|
||||
</Button>
|
||||
{typeof navigator !== "undefined" && !navigator.share && (
|
||||
<p style={{ margin: 0, color: "var(--nl-orange)", fontSize: "var(--nl-fs-sm)" }}>
|
||||
<p style={{ margin: 0, color: "var(--nl-warning)", fontSize: "var(--nl-fs-sm)" }}>
|
||||
Web Share API not supported in this browser.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Sparkles, Clock, Tag, Copy, FileText, GitCompare, Loader2 } from "lucide-react";
|
||||
import { Sparkles, Clock, Tag, Copy, FileText, Loader2 } from "lucide-react";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { listPromptTemplates, runPrompt, suggestTags, getReadingTime } from "@/lib/prompt-client";
|
||||
import { toast } from "@/lib/toast";
|
||||
import type { PromptTemplate, PromptCategory, RunPromptOutput } from "@/lib/types";
|
||||
@ -77,7 +78,7 @@ export function SmartActionsPanel({
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-4)" }}>
|
||||
<Card style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-4)" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "var(--nl-space-2)" }}>
|
||||
<Sparkles size={16} />
|
||||
<strong>Smart Actions</strong>
|
||||
@ -93,49 +94,52 @@ export function SmartActionsPanel({
|
||||
|
||||
{/* Tag suggestions */}
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap", alignItems: "center" }}>
|
||||
<button className="btn btn-secondary" style={{ fontSize: "var(--nl-fs-sm)", padding: "4px 10px" }} onClick={() => void handleSuggestTags()} aria-label="Suggest tags">
|
||||
<Button variant="secondary" size="sm" onClick={() => void handleSuggestTags()} aria-label="Suggest tags">
|
||||
<Tag size={14} /> Suggest tags
|
||||
</button>
|
||||
</Button>
|
||||
{suggestedTags.map((tag) => (
|
||||
<button
|
||||
<Button
|
||||
key={tag}
|
||||
className="badge"
|
||||
style={{ cursor: "pointer" }}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => handleAcceptTag(tag)}
|
||||
aria-label={`Accept tag: ${tag}`}
|
||||
>
|
||||
+ {tag}
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Category filter */}
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-1)", flexWrap: "wrap" }}>
|
||||
<button
|
||||
className={`badge ${activeCategory === "all" ? "" : "surface-muted"}`}
|
||||
<Button
|
||||
variant={activeCategory === "all" ? "primary" : "secondary"}
|
||||
size="sm"
|
||||
onClick={() => setActiveCategory("all")}
|
||||
aria-label="All categories"
|
||||
>
|
||||
All
|
||||
</button>
|
||||
</Button>
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
<Button
|
||||
key={cat}
|
||||
className={`badge ${activeCategory === cat ? "" : "surface-muted"}`}
|
||||
variant={activeCategory === cat ? "primary" : "secondary"}
|
||||
size="sm"
|
||||
onClick={() => setActiveCategory(cat)}
|
||||
aria-label={`Filter: ${CATEGORY_LABELS[cat]}`}
|
||||
>
|
||||
{CATEGORY_LABELS[cat]}
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Template grid */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "var(--nl-space-2)" }}>
|
||||
{filtered.map((t) => (
|
||||
<button
|
||||
<Button
|
||||
key={t.id}
|
||||
className="surface-muted"
|
||||
variant="secondary"
|
||||
className="flex-col items-start"
|
||||
disabled={runningId !== null}
|
||||
onClick={() => void handleRun(t)}
|
||||
aria-label={`Run: ${t.name}`}
|
||||
@ -145,6 +149,7 @@ export function SmartActionsPanel({
|
||||
cursor: runningId ? "wait" : "pointer",
|
||||
opacity: runningId && runningId !== t.id ? 0.5 : 1,
|
||||
display: "grid",
|
||||
height: "auto",
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
@ -153,7 +158,7 @@ export function SmartActionsPanel({
|
||||
{t.name}
|
||||
</span>
|
||||
<span style={{ fontSize: "var(--nl-fs-xs)", color: "var(--nl-text-secondary)" }}>{t.description}</span>
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -163,22 +168,22 @@ export function SmartActionsPanel({
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<strong style={{ fontSize: "var(--nl-fs-sm)" }}>Result</strong>
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-2)" }}>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
style={{ fontSize: "var(--nl-fs-xs)", padding: "2px 8px" }}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => { void navigator.clipboard.writeText(result.content); toast.success("Copied"); }}
|
||||
aria-label="Copy result"
|
||||
>
|
||||
<Copy size={12} /> Copy
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
style={{ fontSize: "var(--nl-fs-xs)", padding: "2px 8px" }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setResult(null)}
|
||||
aria-label="Dismiss result"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ whiteSpace: "pre-wrap", fontSize: "var(--nl-fs-sm)", maxHeight: 300, overflowY: "auto" }}>
|
||||
@ -191,6 +196,6 @@ export function SmartActionsPanel({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { Button } from "@/components/ui/Primitives";
|
||||
import { getSurveyClient } from "@/lib/survey-client";
|
||||
import { toast } from "@/lib/toast";
|
||||
import type { ActiveSurvey, Question, QuestionAnswer } from "@bytelyst/survey-client";
|
||||
@ -83,8 +84,8 @@ export function SurveyBanner() {
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "var(--nl-space-3) var(--nl-space-4)", background: "var(--nl-success-muted)", borderRadius: "var(--nl-radius-sm)", fontSize: "var(--nl-fs-sm)" }}>
|
||||
<span><strong>{survey.title}</strong> — Quick survey ({survey.questions.length} questions)</span>
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-3)" }}>
|
||||
<button onClick={handleStart} style={{ background: "var(--nl-accent-primary)", color: "var(--nl-on-accent)", border: "none", borderRadius: "var(--nl-radius-sm)", padding: "4px 12px", fontWeight: 600, cursor: "pointer" }}>Start</button>
|
||||
<button onClick={dismiss} aria-label="Dismiss survey" style={{ background: "none", border: "none", color: "var(--nl-text-secondary)", cursor: "pointer" }}>×</button>
|
||||
<Button onClick={handleStart} size="sm">Start</Button>
|
||||
<Button onClick={dismiss} aria-label="Dismiss survey" variant="ghost" size="sm">×</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -132,21 +133,19 @@ export function SurveyBanner() {
|
||||
{isRatingType && (
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-2)" }}>
|
||||
{Array.from({ length: (question.maxValue ?? 5) - (question.minValue ?? 1) + 1 }, (_, i) => (question.minValue ?? 1) + i).map((n) => (
|
||||
<button
|
||||
<Button
|
||||
key={n}
|
||||
onClick={() => handleAnswer(question, String(n))}
|
||||
aria-label={`Rate ${n}`}
|
||||
variant={getRatingValue() === n ? "primary" : "secondary"}
|
||||
style={{
|
||||
width: 36, height: 36,
|
||||
borderRadius: "var(--nl-radius-sm)",
|
||||
border: getRatingValue() === n ? "2px solid var(--nl-accent-primary)" : "1px solid var(--nl-border-default)",
|
||||
background: getRatingValue() === n ? "var(--nl-accent-primary)" : "transparent",
|
||||
color: getRatingValue() === n ? "var(--nl-on-accent)" : "var(--nl-text-primary)",
|
||||
cursor: "pointer", fontWeight: 600,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{n}
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@ -154,33 +153,31 @@ export function SurveyBanner() {
|
||||
{isChoiceType && question.options && (
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap" }}>
|
||||
{question.options.map((opt) => (
|
||||
<button
|
||||
<Button
|
||||
key={opt.id}
|
||||
onClick={() => handleAnswer(question, opt.id)}
|
||||
aria-label={`Choose ${opt.text}`}
|
||||
variant={getChoiceValue() === opt.id ? "primary" : "secondary"}
|
||||
style={{
|
||||
padding: "4px 12px",
|
||||
borderRadius: "var(--nl-radius-sm)",
|
||||
border: getChoiceValue() === opt.id ? "2px solid var(--nl-accent-primary)" : "1px solid var(--nl-border-default)",
|
||||
background: getChoiceValue() === opt.id ? "var(--nl-accent-muted)" : "transparent",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{opt.emoji ? `${opt.emoji} ` : ""}{opt.text}
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", marginTop: "var(--nl-space-3)", gap: "var(--nl-space-2)" }}>
|
||||
<button onClick={dismiss} style={{ background: "none", border: "none", color: "var(--nl-text-secondary)", cursor: "pointer" }}>Dismiss</button>
|
||||
<button
|
||||
<Button onClick={dismiss} variant="ghost" size="sm">Dismiss</Button>
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
disabled={!hasAnswer}
|
||||
style={{ padding: "4px 14px", background: "var(--nl-accent-primary)", color: "var(--nl-on-accent)", border: "none", borderRadius: "var(--nl-radius-sm)", fontWeight: 600, cursor: "pointer" }}
|
||||
size="sm"
|
||||
>
|
||||
{currentIdx < survey.questions.length - 1 ? "Next" : "Submit"}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
48
web/src/components/ui/Primitives.tsx
Normal file
48
web/src/components/ui/Primitives.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import {
|
||||
Badge as BytelystBadge,
|
||||
Button as BytelystButton,
|
||||
Card as BytelystCard,
|
||||
type BadgeProps,
|
||||
type ButtonProps,
|
||||
type CardProps,
|
||||
} from "@bytelyst/ui";
|
||||
|
||||
function mergeClassNames(...classes: Array<string | undefined>) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
export function Button({ className, ...props }: ButtonProps) {
|
||||
return (
|
||||
<BytelystButton
|
||||
className={mergeClassNames(
|
||||
"rounded-[var(--nl-radius-sm)] focus-visible:ring-[var(--nl-accent-primary)] focus-visible:ring-offset-[var(--nl-bg-canvas)]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Badge({ className, ...props }: BadgeProps) {
|
||||
return (
|
||||
<BytelystBadge
|
||||
className={mergeClassNames(
|
||||
"border-[var(--nl-border-default)] bg-[var(--nl-accent-muted)] text-[var(--nl-text-primary)]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Card({ className, ...props }: CardProps) {
|
||||
return (
|
||||
<BytelystCard
|
||||
className={mergeClassNames(
|
||||
"rounded-[var(--nl-radius-md)] border-[var(--nl-border-default)] bg-[var(--nl-surface-card-translucent)] shadow-[var(--nl-elevation-md)]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user