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