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.",
"raw_interactive_controls": 38,
"legacy_global_surface_classes": 92,
"//": "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": 25,
"legacy_global_surface_classes": 67,
"hardcoded_color_literals": 0,
"direct_bytelyst_ui_imports_outside_adapter": 0
}

View File

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

View File

@ -1,6 +1,7 @@
"use client";
import Link from "next/link";
import { Badge, Input } from "@/components/ui/Primitives";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useKeyboardShortcuts } from "@/lib/use-keyboard-shortcuts";
@ -176,25 +177,21 @@ export function CommandPalette() {
onClick={() => setOpen(false)}
>
<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={{
width: "min(560px, 100%)",
maxHeight: "70vh",
overflow: "hidden",
display: "grid",
gridTemplateRows: "auto 1fr",
boxShadow: "var(--nl-command-shadow)",
}}
onClick={(e) => e.stopPropagation()}
>
<div style={{ padding: "var(--nl-space-3) var(--nl-space-4)", borderBottom: "1px solid var(--nl-border-default)" }}>
<input
<Input
ref={inputRef}
className="input-shell"
placeholder="Jump to note, workspace, or action…"
value={query}
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)" }}>
<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={{ fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)" }}>
<span className="badge" style={{ marginRight: 8 }}>
<Badge variant="neutral" className="mr-2">
{item.kind}
</span>
</Badge>
{item.hint}
</div>
</Link>

View File

@ -107,7 +107,7 @@ export function ExtractedTasksPanel({
{proposals.length === 0 ? null : (
<ul style={{ listStyle: "none", margin: 0, padding: 0, display: "grid", gap: "var(--nl-space-2)" }}>
{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 style={{ display: "flex", gap: 8 }}>
<Button

View File

@ -1,16 +1,21 @@
import Link from "next/link";
import { Card } from "@/components/ui/Primitives";
import type { LinkedNote } from "@/lib/types";
export function LinkedNotesPanel({ linkedNotes }: { linkedNotes: LinkedNote[] }) {
return (
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}>
<div style={{ fontWeight: 700 }}>Linked notes</div>
<Card padding="md" className="grid gap-3">
<div className="font-bold">Linked notes</div>
{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>
<span style={{ color: "var(--nl-text-secondary)" }}>{linkedNote.relationship}</span>
<span className="text-[color:var(--nl-text-secondary)]">{linkedNote.relationship}</span>
</Link>
))}
</section>
</Card>
);
}

View File

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

View File

@ -5,6 +5,7 @@ import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder";
import type { NoteDetail } from "@/lib/types";
import { Card, Input } from "@/components/ui/Primitives";
import { useDebounce } from "@/lib/use-debounce";
import { copilotTransform, type CopilotAction, type CopilotTone } from "@/lib/copilot-client";
import { toast } from "@/lib/toast";
@ -87,7 +88,7 @@ export function NoteEditor({
content: note.body,
editorProps: {
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;",
},
},
@ -178,23 +179,20 @@ export function NoteEditor({
);
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
style={{ display: "grid", gap: "var(--nl-space-4)" }}
className="grid gap-4"
onSubmit={(e) => {
e.preventDefault();
handleSave();
}}
>
<div style={{ display: "grid", gap: "var(--nl-space-2)" }}>
<label htmlFor="note-title" style={{ color: "var(--nl-text-secondary)" }}>Title</label>
<input
id="note-title"
className="input-shell"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
<Input
id="note-title"
label="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<div style={{ display: "grid", gap: "var(--nl-space-2)" }}>
<label style={{ color: "var(--nl-text-secondary)" }}>Body</label>
@ -278,6 +276,6 @@ export function NoteEditor({
</button>
</div>
</form>
</section>
</Card>
);
}

View File

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

View File

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

View File

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

View File

@ -164,7 +164,7 @@ export function SmartActionsPanel({
{/* Result display */}
{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" }}>
<strong style={{ fontSize: "var(--nl-fs-sm)" }}>Result</strong>
<div style={{ display: "flex", gap: "var(--nl-space-2)" }}>

View File

@ -1,7 +1,7 @@
"use client";
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 { toast } from "@/lib/toast";
import type { ActiveSurvey, Question, QuestionAnswer } from "@bytelyst/survey-client";
@ -121,8 +121,7 @@ export function SurveyBanner() {
</div>
{isTextType && (
<input
className="input-shell"
<Input
placeholder="Your answer…"
value={getTextValue()}
onChange={(e) => handleAnswer(question, e.target.value)}

View File

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

View File

@ -1,3 +1,4 @@
import { forwardRef } from "react";
import {
AlertBanner as BytelystAlertBanner,
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} />;
}
export function Input({ className, ...props }: InputProps) {
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{ className, ...props },
ref,
) {
return (
<BytelystInput
className={mergeClassNames(
"rounded-[var(--nl-radius-sm)]",
className,
)}
ref={ref}
className={mergeClassNames("rounded-[var(--nl-radius-sm)]", className)}
{...props}
/>
);
}
});
export function Textarea({ className, ...props }: TextareaProps) {
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(
{ className, ...props },
ref,
) {
return (
<BytelystTextarea
className={mergeClassNames(
"rounded-[var(--nl-radius-sm)]",
className,
)}
ref={ref}
className={mergeClassNames("rounded-[var(--nl-radius-sm)]", className)}
{...props}
/>
);
}
});
export function Select({ className, ...props }: SelectProps) {
return (