refactor(web): use shared ui primitives

This commit is contained in:
Saravana Achu Mac 2026-05-05 10:49:25 -07:00
parent f692a94d25
commit d26a4ae9de
28 changed files with 411 additions and 320 deletions

View File

@ -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>
);
}

View File

@ -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 &quot;Getting started&quot; 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}

View File

@ -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>
}

View File

@ -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>

View File

@ -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>&middot;</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>&middot;</span>
<span>Output: {t.outputType}</span>
</div>
</div>
</Card>
);
})}
</div>

View File

@ -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 ?? "—"} &middot; {user?.email ?? "—"} &middot; {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>
);

View File

@ -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>
))}

View File

@ -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" }}>

View File

@ -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&apos;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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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 */

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
))}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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&apos;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>
);
}

View File

@ -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>
);
}

View File

@ -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" }}>&times;</button>
<Button onClick={handleStart} size="sm">Start</Button>
<Button onClick={dismiss} aria-label="Dismiss survey" variant="ghost" size="sm">&times;</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>
);

View 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}
/>
);
}