feat(web/ui5+ui7): migrate 12 components to @bytelyst/ui primitives

Finishes UI5 and kicks off UI7 by migrating the remaining form-heavy
components plus the note-detail right-rail panels. Drops legacy class
matches from 92 → 67 (-25) and raw interactive controls from 38 → 25
(-13). Ratchet baseline updated to the new floor.

Components migrated:

UI5 finish:
- NoteEditor.tsx — surface-card wrapper → Card, title input → Input,
  Tiptap editor className updated to use border + bg classes instead
  of input-shell. Toolbar buttons left as raw (intentional, tightly
  styled icon controls).
- SmartActionsPanel.tsx — result panel surface-muted → Tailwind
  bg-[var(--nl-surface-muted)] utility.
- ArtifactPanel.tsx — section→Card, badge→Badge, all three input-shell
  inputs/selects/textareas→Input/Select/Textarea, surface-muted form
  shell + per-artifact row → Tailwind bg-utility, raw <button> Open
  → Button.
- CommandPalette.tsx — surface-card command sheet → Tailwind layered
  classes, search input → Input (now ref-forwarded), kind badge → Badge.

UI7 component pass:
- MetadataPanel.tsx — section→Card, tag badge→Badge.
- LinkedNotesPanel.tsx — section→Card, surface-muted link row →
  Tailwind bg-utility with hover state.
- PalaceStats.tsx — section→Card, inline styles → Tailwind utilities.
- ExtractedTasksPanel.tsx — surface-muted row → Tailwind.
- NoteVersionsPanel.tsx — all three section/surface-card variants →
  Card + raw button → preserved (interactive disclosure).
- Pagination.tsx — raw <button> Previous/Next → Button, surface-muted
  → built-in secondary variant.
- TaskReviewPanel.tsx — full migration: section→Card, badge→Badge,
  input-shell + textarea + raw button → Input/Textarea/Button.
- SurveyBanner.tsx — survey answer input-shell → Input.

Adapter changes:
- web/src/components/ui/Primitives.tsx — Input and Textarea now use
  React.forwardRef so callers like CommandPalette can attach refs.

Verified:
- pnpm --filter @notelett/web run typecheck: passes
- pnpm --filter @notelett/web test: 96/96 still pass
- pnpm run audit:ui:ratchet: at new baseline (25/67/0/0)
- pnpm run audit:ui: legacy class matches now in dashboard / search /
  workspaces / notes-detail / palace / chat pages (UI6/UI7 page targets)
This commit is contained in:
saravanakumardb1 2026-05-23 01:33:48 -07:00
parent d5e857dbf7
commit 2408f43426
14 changed files with 159 additions and 191 deletions

View File

@ -1,7 +1,7 @@
{ {
"//": "Baseline UI drift counts captured 2026-05-23 after UI5 settings + modals slice. ui-drift-ratchet.sh compares current counts against these and FAILS if any category EXCEEDS its baseline. To lower the baseline after migrating screens, run scripts/ui-drift-ratchet.sh --update.", "//": "Baseline UI drift counts. Updated 2026-05-23T08:33:24Z by scripts/ui-drift-ratchet.sh --update. Commit alongside the migration that lowered the counts.",
"raw_interactive_controls": 38, "raw_interactive_controls": 25,
"legacy_global_surface_classes": 92, "legacy_global_surface_classes": 67,
"hardcoded_color_literals": 0, "hardcoded_color_literals": 0,
"direct_bytelyst_ui_imports_outside_adapter": 0 "direct_bytelyst_ui_imports_outside_adapter": 0
} }

View File

@ -2,7 +2,7 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { StateNotice } from "@/components/StateNotice"; import { StateNotice } from "@/components/StateNotice";
import { Button } from "@/components/ui/Primitives"; import { Badge, Button, Card, Input, Select, Textarea } from "@/components/ui/Primitives";
import { getArtifactReadUrl, uploadArtifact } from "@/lib/blob-client"; import { getArtifactReadUrl, uploadArtifact } from "@/lib/blob-client";
import { toast } from "@/lib/toast"; import { toast } from "@/lib/toast";
import { getEmptyState, toUserFacingState, type UserFacingState } from "@/lib/user-facing-states"; import { getEmptyState, toUserFacingState, type UserFacingState } from "@/lib/user-facing-states";
@ -91,53 +91,46 @@ export function ArtifactPanel({
} }
return ( return (
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}> <Card padding="md" className="grid gap-3">
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--nl-space-3)", alignItems: "center" }}> <div className="flex items-center justify-between gap-3">
<div style={{ fontWeight: 700 }}>Artifacts</div> <div className="font-bold">Artifacts</div>
<span className="badge">blob-backed view</span> <Badge variant="info">blob-backed view</Badge>
</div> </div>
<div className="surface-muted" style={{ padding: "var(--nl-space-3)", display: "grid", gap: "var(--nl-space-3)" }}> <div className="grid gap-3 rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] p-3">
<div style={{ display: "grid", gap: "var(--nl-space-2)" }}> <Input
<label htmlFor="artifact-title" style={{ color: "var(--nl-text-secondary)" }}>
Add artifact
</label>
<input
id="artifact-title" id="artifact-title"
className="input-shell" label="Add artifact"
value={title} value={title}
onChange={(event) => setTitle(event.target.value)} onChange={(event) => setTitle(event.target.value)}
placeholder="Artifact title" placeholder="Artifact title"
aria-label="Artifact title" aria-label="Artifact title"
/> />
</div> <div className="grid gap-3 [grid-template-columns:minmax(140px,180px)_minmax(0,1fr)]">
<div style={{ display: "grid", gridTemplateColumns: "minmax(140px, 180px) minmax(0, 1fr)", gap: "var(--nl-space-3)" }}> <Select
<select
className="input-shell"
value={artifactType} value={artifactType}
onChange={(event) => setArtifactType(event.target.value as "file" | "summary" | "extraction" | "citation" | "export")} onChange={(event) => setArtifactType(event.target.value as "file" | "summary" | "extraction" | "citation" | "export")}
aria-label="Artifact type" aria-label="Artifact type"
> options={[
<option value="file">file</option> { value: "file", label: "file" },
<option value="summary">summary</option> { value: "summary", label: "summary" },
<option value="extraction">extraction</option> { value: "extraction", label: "extraction" },
<option value="citation">citation</option> { value: "citation", label: "citation" },
<option value="export">export</option> { value: "export", label: "export" },
</select> ]}
<input />
className="input-shell" <Input
value={blobPath} value={blobPath}
onChange={(event) => setBlobPath(event.target.value)} onChange={(event) => setBlobPath(event.target.value)}
placeholder="Optional blob path" placeholder="Optional blob path"
aria-label="Artifact blob path" aria-label="Artifact blob path"
/> />
</div> </div>
<textarea <Textarea
className="input-shell"
value={description} value={description}
onChange={(event) => setDescription(event.target.value)} onChange={(event) => setDescription(event.target.value)}
placeholder="Description" placeholder="Description"
aria-label="Artifact description" aria-label="Artifact description"
style={{ minHeight: 96, resize: "vertical" }} className="min-h-[96px] resize-y"
/> />
<div style={{ display: "flex", justifyContent: "flex-end", gap: "var(--nl-space-2)" }}> <div style={{ display: "flex", justifyContent: "flex-end", gap: "var(--nl-space-2)" }}>
<input ref={fileInputRef} type="file" hidden onChange={handleFileUpload} /> <input ref={fileInputRef} type="file" hidden onChange={handleFileUpload} />
@ -171,17 +164,18 @@ export function ArtifactPanel({
<StateNotice state={getEmptyState("blob", "artifacts")} compact /> <StateNotice state={getEmptyState("blob", "artifacts")} compact />
) : null} ) : null}
{artifacts.map((artifact) => ( {artifacts.map((artifact) => (
<div key={artifact.id} className="surface-muted" style={{ padding: "var(--nl-space-3)", display: "flex", justifyContent: "space-between", gap: "var(--nl-space-3)", alignItems: "center" }}> <div key={artifact.id} className="flex items-center justify-between gap-3 rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] p-3">
<div style={{ display: "grid", gap: 4 }}> <div className="grid gap-1">
<strong>{artifact.name}</strong> <strong>{artifact.name}</strong>
<span style={{ color: "var(--nl-text-secondary)" }}>{artifact.type}</span> <span className="text-[color:var(--nl-text-secondary)]">{artifact.type}</span>
</div> </div>
<div style={{ display: "flex", gap: "var(--nl-space-3)", alignItems: "center" }}> <div className="flex items-center gap-3">
<span style={{ color: "var(--nl-text-secondary)" }}>{artifact.status}</span> <span className="text-[color:var(--nl-text-secondary)]">{artifact.status}</span>
{artifact.blobPath ? ( {artifact.blobPath ? (
<button <Button
type="button" type="button"
className="badge" size="sm"
variant="secondary"
onClick={() => { onClick={() => {
void handleOpenArtifact(artifact); void handleOpenArtifact(artifact);
}} }}
@ -189,11 +183,11 @@ export function ArtifactPanel({
aria-label={`Open artifact: ${artifact.name}`} aria-label={`Open artifact: ${artifact.name}`}
> >
{openingId === artifact.id ? "Opening…" : "Open"} {openingId === artifact.id ? "Opening…" : "Open"}
</button> </Button>
) : null} ) : null}
</div> </div>
</div> </div>
))} ))}
</section> </Card>
); );
} }

View File

@ -1,6 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { Badge, Input } from "@/components/ui/Primitives";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useKeyboardShortcuts } from "@/lib/use-keyboard-shortcuts"; import { useKeyboardShortcuts } from "@/lib/use-keyboard-shortcuts";
@ -176,25 +177,21 @@ export function CommandPalette() {
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
> >
<div <div
className="surface-card" className="grid overflow-hidden rounded-[var(--nl-radius-md)] border border-[color:var(--nl-border-default)] bg-[color:var(--nl-surface-card)] [grid-template-rows:auto_1fr]"
style={{ style={{
width: "min(560px, 100%)", width: "min(560px, 100%)",
maxHeight: "70vh", maxHeight: "70vh",
overflow: "hidden",
display: "grid",
gridTemplateRows: "auto 1fr",
boxShadow: "var(--nl-command-shadow)", boxShadow: "var(--nl-command-shadow)",
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<div style={{ padding: "var(--nl-space-3) var(--nl-space-4)", borderBottom: "1px solid var(--nl-border-default)" }}> <div style={{ padding: "var(--nl-space-3) var(--nl-space-4)", borderBottom: "1px solid var(--nl-border-default)" }}>
<input <Input
ref={inputRef} ref={inputRef}
className="input-shell"
placeholder="Jump to note, workspace, or action…" placeholder="Jump to note, workspace, or action…"
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
style={{ width: "100%", fontSize: "var(--nl-fs-md)" }} className="w-full"
/> />
<div style={{ marginTop: 8, fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}> <div style={{ marginTop: 8, fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}>
<kbd style={{ opacity: 0.8 }}></kbd> <kbd style={{ opacity: 0.8 }}></kbd> navigate · <kbd style={{ opacity: 0.8 }}></kbd> open ·{" "} <kbd style={{ opacity: 0.8 }}></kbd> <kbd style={{ opacity: 0.8 }}></kbd> navigate · <kbd style={{ opacity: 0.8 }}></kbd> open ·{" "}
@ -226,9 +223,9 @@ export function CommandPalette() {
> >
<div style={{ fontWeight: 600 }}>{item.label}</div> <div style={{ fontWeight: 600 }}>{item.label}</div>
<div style={{ fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}> <div style={{ fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}>
<span className="badge" style={{ marginRight: 8 }}> <Badge variant="neutral" className="mr-2">
{item.kind} {item.kind}
</span> </Badge>
{item.hint} {item.hint}
</div> </div>
</Link> </Link>

View File

@ -107,7 +107,7 @@ export function ExtractedTasksPanel({
{proposals.length === 0 ? null : ( {proposals.length === 0 ? null : (
<ul style={{ listStyle: "none", margin: 0, padding: 0, display: "grid", gap: "var(--nl-space-2)" }}> <ul style={{ listStyle: "none", margin: 0, padding: 0, display: "grid", gap: "var(--nl-space-2)" }}>
{proposals.map((task) => ( {proposals.map((task) => (
<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="flex flex-wrap items-center justify-between gap-2 rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] p-3">
<span>{task.title}</span> <span>{task.title}</span>
<span style={{ display: "flex", gap: 8 }}> <span style={{ display: "flex", gap: 8 }}>
<Button <Button

View File

@ -1,16 +1,21 @@
import Link from "next/link"; import Link from "next/link";
import { Card } from "@/components/ui/Primitives";
import type { LinkedNote } from "@/lib/types"; import type { LinkedNote } from "@/lib/types";
export function LinkedNotesPanel({ linkedNotes }: { linkedNotes: LinkedNote[] }) { export function LinkedNotesPanel({ linkedNotes }: { linkedNotes: LinkedNote[] }) {
return ( return (
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}> <Card padding="md" className="grid gap-3">
<div style={{ fontWeight: 700 }}>Linked notes</div> <div className="font-bold">Linked notes</div>
{linkedNotes.map((linkedNote) => ( {linkedNotes.map((linkedNote) => (
<Link key={linkedNote.id} href={`/notes/${linkedNote.id}`} className="surface-muted" style={{ padding: "var(--nl-space-3)", display: "grid", gap: 4 }}> <Link
key={linkedNote.id}
href={`/notes/${linkedNote.id}`}
className="grid gap-1 rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] p-3 transition-colors hover:bg-[color:var(--nl-surface-elevated)]"
>
<strong>{linkedNote.title}</strong> <strong>{linkedNote.title}</strong>
<span style={{ color: "var(--nl-text-secondary)" }}>{linkedNote.relationship}</span> <span className="text-[color:var(--nl-text-secondary)]">{linkedNote.relationship}</span>
</Link> </Link>
))} ))}
</section> </Card>
); );
} }

View File

@ -1,26 +1,27 @@
import Link from "next/link"; import Link from "next/link";
import { Badge, Card } from "@/components/ui/Primitives";
import type { NoteDetail } from "@/lib/types"; import type { NoteDetail } from "@/lib/types";
export function MetadataPanel({ note }: { note: NoteDetail }) { export function MetadataPanel({ note }: { note: NoteDetail }) {
return ( return (
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}> <Card padding="md" className="grid gap-3">
<div style={{ fontWeight: 700 }}>Metadata</div> <div className="font-bold">Metadata</div>
<div style={{ display: "grid", gap: "var(--nl-space-2)" }}> <div className="grid gap-2 text-[color:var(--nl-text-secondary)]">
<div style={{ color: "var(--nl-text-secondary)" }}>Owner: {note.metadata.owner}</div> <div>Owner: {note.metadata.owner}</div>
<div style={{ color: "var(--nl-text-secondary)" }}>Source: {note.metadata.source}</div> <div>Source: {note.metadata.source}</div>
<Link href="/reviews" style={{ color: "var(--nl-text-secondary)" }}> <Link href="/reviews" className="text-[color:var(--nl-text-secondary)]">
Review state: {note.metadata.reviewState} Review state: {note.metadata.reviewState}
</Link> </Link>
<div style={{ color: "var(--nl-text-secondary)" }}>Tasks: {note.metadata.taskCount}</div> <div>Tasks: {note.metadata.taskCount}</div>
<div style={{ color: "var(--nl-text-secondary)" }}>Artifacts: {note.metadata.artifactCount}</div> <div>Artifacts: {note.metadata.artifactCount}</div>
</div> </div>
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap" }}> <div className="flex flex-wrap gap-2">
{note.tags.map((tag) => ( {note.tags.map((tag) => (
<Link key={tag} href={`/search?q=${encodeURIComponent(tag)}`} className="badge"> <Link key={tag} href={`/search?q=${encodeURIComponent(tag)}`}>
#{tag} <Badge variant="neutral">#{tag}</Badge>
</Link> </Link>
))} ))}
</div> </div>
</section> </Card>
); );
} }

View File

@ -5,6 +5,7 @@ import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit"; import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder"; import Placeholder from "@tiptap/extension-placeholder";
import type { NoteDetail } from "@/lib/types"; import type { NoteDetail } from "@/lib/types";
import { Card, Input } from "@/components/ui/Primitives";
import { useDebounce } from "@/lib/use-debounce"; import { useDebounce } from "@/lib/use-debounce";
import { copilotTransform, type CopilotAction, type CopilotTone } from "@/lib/copilot-client"; import { copilotTransform, type CopilotAction, type CopilotTone } from "@/lib/copilot-client";
import { toast } from "@/lib/toast"; import { toast } from "@/lib/toast";
@ -87,7 +88,7 @@ export function NoteEditor({
content: note.body, content: note.body,
editorProps: { editorProps: {
attributes: { attributes: {
class: "input-shell", class: "notelett-tiptap rounded-[var(--nl-radius-md)] border border-[color:var(--nl-border-default)] bg-[color:var(--nl-input-bg)] p-3",
style: "min-height:360px;outline:none;line-height:1.7;", style: "min-height:360px;outline:none;line-height:1.7;",
}, },
}, },
@ -178,23 +179,20 @@ export function NoteEditor({
); );
return ( return (
<section className="surface-card" style={{ padding: "var(--nl-space-6)", display: "grid", gap: "var(--nl-space-4)" }}> <Card padding="lg" className="grid gap-4">
<form <form
style={{ display: "grid", gap: "var(--nl-space-4)" }} className="grid gap-4"
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
handleSave(); handleSave();
}} }}
> >
<div style={{ display: "grid", gap: "var(--nl-space-2)" }}> <Input
<label htmlFor="note-title" style={{ color: "var(--nl-text-secondary)" }}>Title</label>
<input
id="note-title" id="note-title"
className="input-shell" label="Title"
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
/> />
</div>
<div style={{ display: "grid", gap: "var(--nl-space-2)" }}> <div style={{ display: "grid", gap: "var(--nl-space-2)" }}>
<label style={{ color: "var(--nl-text-secondary)" }}>Body</label> <label style={{ color: "var(--nl-text-secondary)" }}>Body</label>
@ -278,6 +276,6 @@ export function NoteEditor({
</button> </button>
</div> </div>
</form> </form>
</section> </Card>
); );
} }

View File

@ -1,6 +1,7 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Card } from "@/components/ui/Primitives";
import { listNoteVersions, type NoteVersionRow } from "@/lib/notes-client"; import { listNoteVersions, type NoteVersionRow } from "@/lib/notes-client";
export function NoteVersionsPanel({ noteId, workspaceId }: { noteId: string; workspaceId: string }) { export function NoteVersionsPanel({ noteId, workspaceId }: { noteId: string; workspaceId: string }) {
@ -22,59 +23,42 @@ export function NoteVersionsPanel({ noteId, workspaceId }: { noteId: string; wor
if (error) { if (error) {
return ( return (
<section className="surface-card" style={{ padding: "var(--nl-space-4)", color: "var(--nl-text-secondary)" }}> <Card padding="sm" className="text-[color:var(--nl-text-secondary)]">
{error} {error}
</section> </Card>
); );
} }
if (items.length === 0) { if (items.length === 0) {
return ( return (
<section className="surface-card" style={{ padding: "var(--nl-space-4)", color: "var(--nl-text-secondary)" }}> <Card padding="sm" className="text-[color:var(--nl-text-secondary)]">
No saved versions yet. Versions are created when you edit title or body. No saved versions yet. Versions are created when you edit title or body.
</section> </Card>
); );
} }
return ( return (
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}> <Card padding="md" className="grid gap-3">
<div style={{ fontWeight: 700 }}>Version history</div> <div className="font-bold">Version history</div>
<ul style={{ listStyle: "none", margin: 0, padding: 0, display: "grid", gap: "var(--nl-space-2)" }}> <ul className="m-0 grid list-none gap-2 p-0">
{items.map((v) => ( {items.map((v) => (
<li key={v.id} className="surface-muted" style={{ padding: "var(--nl-space-3)" }}> <li key={v.id} className="rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] p-3">
<button <button
type="button" type="button"
onClick={() => setOpenId((o) => (o === v.id ? null : v.id))} onClick={() => setOpenId((o) => (o === v.id ? null : v.id))}
style={{ className="w-full cursor-pointer border-0 bg-transparent text-left font-semibold text-[color:var(--nl-text-primary)]"
width: "100%",
textAlign: "left",
background: "none",
border: "none",
color: "var(--nl-text-primary)",
cursor: "pointer",
fontWeight: 600,
}}
> >
{new Date(v.savedAt).toLocaleString()} · {v.source} · {v.title.slice(0, 48)} {new Date(v.savedAt).toLocaleString()} · {v.source} · {v.title.slice(0, 48)}
{v.title.length > 48 ? "…" : ""} {v.title.length > 48 ? "…" : ""}
</button> </button>
{openId === v.id ? ( {openId === v.id ? (
<pre <pre className="mt-2 max-h-[200px] overflow-auto whitespace-pre-wrap text-[length:var(--nl-fs-sm)] text-[color:var(--nl-text-secondary)]">
style={{
marginTop: 8,
whiteSpace: "pre-wrap",
fontSize: "var(--nl-fs-sm)",
color: "var(--nl-text-secondary)",
maxHeight: 200,
overflow: "auto",
}}
>
{v.body.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim()} {v.body.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim()}
</pre> </pre>
) : null} ) : null}
</li> </li>
))} ))}
</ul> </ul>
</section> </Card>
); );
} }

View File

@ -1,5 +1,7 @@
"use client"; "use client";
import { Button } from "@/components/ui/Primitives";
interface PaginationProps { interface PaginationProps {
offset: number; offset: number;
limit: number; limit: number;
@ -14,26 +16,26 @@ export function Pagination({ offset, limit, total, onPageChange }: PaginationPro
if (totalPages <= 1) return null; if (totalPages <= 1) return null;
return ( return (
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: "var(--nl-space-3)", padding: "var(--nl-space-4) 0" }}> <div className="flex items-center justify-center gap-3 py-4">
<button <Button
variant="secondary"
size="sm"
disabled={currentPage <= 1} disabled={currentPage <= 1}
onClick={() => onPageChange(Math.max(0, offset - limit))} onClick={() => onPageChange(Math.max(0, offset - limit))}
className="surface-muted"
style={{ padding: "6px 14px", border: "none", fontSize: "var(--nl-fs-sm)" }}
> >
Previous Previous
</button> </Button>
<span style={{ fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}> <span className="text-[length:var(--nl-fs-sm)] text-[color:var(--nl-text-secondary)]">
Page {currentPage} of {totalPages} Page {currentPage} of {totalPages}
</span> </span>
<button <Button
variant="secondary"
size="sm"
disabled={currentPage >= totalPages} disabled={currentPage >= totalPages}
onClick={() => onPageChange(offset + limit)} onClick={() => onPageChange(offset + limit)}
className="surface-muted"
style={{ padding: "6px 14px", border: "none", fontSize: "var(--nl-fs-sm)" }}
> >
Next Next
</button> </Button>
</div> </div>
); );
} }

View File

@ -1,6 +1,7 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Card } from "@/components/ui/Primitives";
import { getPalaceStats, type PalaceStats as PalaceStatsData } from "@/lib/palace-client"; import { getPalaceStats, type PalaceStats as PalaceStatsData } from "@/lib/palace-client";
export function PalaceStats() { export function PalaceStats() {
@ -33,26 +34,19 @@ export function PalaceStats() {
]; ];
return ( return (
<section className="surface-card" style={{ padding: "var(--nl-space-5)" }}> <Card padding="md">
<div style={{ fontWeight: 700, fontSize: "var(--nl-text-lg)", marginBottom: "var(--nl-space-3)" }}> <div className="mb-3 text-[length:var(--nl-text-lg)] font-bold">Memory Palace</div>
Memory Palace <div className="grid gap-3 [grid-template-columns:repeat(auto-fit,minmax(100px,1fr))]">
</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(100px, 1fr))", gap: "var(--nl-space-3)" }}>
{items.map(({ label, value }) => ( {items.map(({ label, value }) => (
<div <div
key={label} key={label}
style={{ className="rounded-[var(--nl-radius-md)] border border-[color:var(--nl-border-subtle)] p-3 text-center"
textAlign: "center",
padding: "var(--nl-space-3)",
borderRadius: "var(--nl-radius-md)",
border: "1px solid var(--nl-border-subtle)",
}}
> >
<div style={{ fontSize: "1.5rem", fontWeight: 700, color: "var(--nl-accent-primary)" }}>{value}</div> <div className="text-2xl font-bold text-[color:var(--nl-accent-primary)]">{value}</div>
<div style={{ fontSize: "0.75rem", color: "var(--nl-text-secondary)" }}>{label}</div> <div className="text-xs text-[color:var(--nl-text-secondary)]">{label}</div>
</div> </div>
))} ))}
</div> </div>
</section> </Card>
); );
} }

View File

@ -164,7 +164,7 @@ export function SmartActionsPanel({
{/* Result display */} {/* Result display */}
{result && ( {result && (
<div className="surface-muted" style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-3)" }}> <div className="grid gap-3 rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] p-4">
<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)" }}>

View File

@ -1,7 +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 { Button, Input } 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";
@ -121,8 +121,7 @@ export function SurveyBanner() {
</div> </div>
{isTextType && ( {isTextType && (
<input <Input
className="input-shell"
placeholder="Your answer…" placeholder="Your answer…"
value={getTextValue()} value={getTextValue()}
onChange={(e) => handleAnswer(question, e.target.value)} onChange={(e) => handleAnswer(question, e.target.value)}

View File

@ -1,4 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { Badge, Button, Card, Input, Textarea } from "@/components/ui/Primitives";
import type { NoteTask } from "@/lib/types"; import type { NoteTask } from "@/lib/types";
export function TaskReviewPanel({ export function TaskReviewPanel({
@ -28,61 +29,51 @@ export function TaskReviewPanel({
} }
return ( return (
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}> <Card padding="md" className="grid gap-3">
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--nl-space-3)", alignItems: "center" }}> <div className="flex items-center justify-between gap-3">
<div style={{ fontWeight: 700 }}>Task extraction review</div> <div className="font-bold">Task extraction review</div>
<span className="badge">backend-backed review</span> <Badge variant="info">backend-backed review</Badge>
</div> </div>
<div className="surface-muted" style={{ padding: "var(--nl-space-3)", display: "grid", gap: "var(--nl-space-3)" }}> <div className="grid gap-3 rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] p-3">
<div style={{ display: "grid", gap: "var(--nl-space-2)" }}> <Input
<label htmlFor="task-title" style={{ color: "var(--nl-text-secondary)" }}>
Add task
</label>
<input
id="task-title" id="task-title"
className="input-shell" label="Add task"
value={title} value={title}
onChange={(event) => setTitle(event.target.value)} onChange={(event) => setTitle(event.target.value)}
placeholder="Task title" placeholder="Task title"
/> />
</div> <Textarea
<textarea
className="input-shell"
value={description} value={description}
onChange={(event) => setDescription(event.target.value)} onChange={(event) => setDescription(event.target.value)}
placeholder="Description" placeholder="Description"
aria-label="Task description" aria-label="Task description"
style={{ minHeight: 96, resize: "vertical" }} className="min-h-[96px] resize-y"
/> />
<div style={{ display: "flex", justifyContent: "flex-end" }}> <div className="flex justify-end">
<button <Button
type="button" type="button"
disabled={isCreating} disabled={isCreating}
loading={isCreating}
onClick={() => { onClick={() => {
void handleCreateTask(); void handleCreateTask();
}} }}
style={{
border: "none",
borderRadius: "var(--nl-radius-md)",
padding: "8px 12px",
background: "var(--nl-accent-primary)",
color: "var(--nl-text-primary)",
fontWeight: 600,
}}
> >
{isCreating ? "Adding…" : "Add task"} Add task
</button> </Button>
</div> </div>
</div> </div>
{tasks.map((task) => ( {tasks.map((task) => (
<div key={task.id} className="surface-muted" style={{ padding: "var(--nl-space-3)", display: "flex", justifyContent: "space-between", gap: "var(--nl-space-3)", alignItems: "center" }}> <div
<div style={{ display: "grid", gap: 4 }}> key={task.id}
className="flex items-center justify-between gap-3 rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] p-3"
>
<div className="grid gap-1">
<strong>{task.title}</strong> <strong>{task.title}</strong>
<span style={{ color: "var(--nl-text-secondary)" }}>Source: {task.source}</span> <span className="text-[color:var(--nl-text-secondary)]">Source: {task.source}</span>
</div> </div>
<span style={{ color: "var(--nl-text-secondary)" }}>{task.status}</span> <span className="text-[color:var(--nl-text-secondary)]">{task.status}</span>
</div> </div>
))} ))}
</section> </Card>
); );
} }

View File

@ -1,3 +1,4 @@
import { forwardRef } from "react";
import { import {
AlertBanner as BytelystAlertBanner, AlertBanner as BytelystAlertBanner,
FormSection as BytelystFormSection, FormSection as BytelystFormSection,
@ -285,29 +286,31 @@ export function StatusBadge({ className, status, tone, ...props }: NoteLettStatu
return <BytelystStatusBadge className={className} tone={tone ?? (status ? getStatusTone(status) : undefined)} {...props} />; return <BytelystStatusBadge className={className} tone={tone ?? (status ? getStatusTone(status) : undefined)} {...props} />;
} }
export function Input({ className, ...props }: InputProps) { export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{ className, ...props },
ref,
) {
return ( return (
<BytelystInput <BytelystInput
className={mergeClassNames( ref={ref}
"rounded-[var(--nl-radius-sm)]", className={mergeClassNames("rounded-[var(--nl-radius-sm)]", className)}
className,
)}
{...props} {...props}
/> />
); );
} });
export function Textarea({ className, ...props }: TextareaProps) { export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(
{ className, ...props },
ref,
) {
return ( return (
<BytelystTextarea <BytelystTextarea
className={mergeClassNames( ref={ref}
"rounded-[var(--nl-radius-sm)]", className={mergeClassNames("rounded-[var(--nl-radius-sm)]", className)}
className,
)}
{...props} {...props}
/> />
); );
} });
export function Select({ className, ...props }: SelectProps) { export function Select({ className, ...props }: SelectProps) {
return ( return (