feat(web/ui7): migrate note detail, palace, gaps/prompts pages, broadcast banner

Phase UI7 — completes the note detail surface, the Palace knowledge
exploration page + its panels, the knowledge-gaps page, the prompts
page empty states, and the broadcast banner. Brings the ratchet down
to 14 raw controls / 21 legacy class matches — both genuine remaining
intentional items (NoteEditor toolbar, hidden file input, audit false
positives matching Tailwind arbitrary values).

notes/[noteId]/page.tsx:
- 'Loading' badge → Badge variant=neutral.
- Loading/error sections → Card.
- Review-state link → Link wrapping Badge.

palace/page.tsx:
- Wing <select> → Select with options=[{value,label}].

palace components:
- PalacePanel.tsx — search input → Input, hall chip → Badge.
- MemoryTimeline.tsx — hall chip → Badge.
- KnowledgeGraphView.tsx — entity query input → Input.

workspaces/[id]/gaps/page.tsx:
- Topic Coverage section → Card, chip → Badge.
- Empty-state + per-gap items → Card.

prompts/page.tsx:
- Loading + empty-state divs → Card.

landing page (/):
- section.surface-card → Card.
- 'Backend-backed web surface' badge → Badge.
- 'Open dashboard'/'Browse workspaces' links → utility classes.

share/[token]/page.tsx:
- Read-only public share badge → Badge.
- Main content surface-card + input-shell body wrapper → Card with
  bordered body container.

BroadcastBanner.tsx:
- CTA + Dismiss raw <button> → Button (ghost variant, size sm).

Cumulative ratchet impact since session start:
  raw interactive controls       38 → 14   (-24)
  legacy global surface classes  92 → 21   (-71)
  hardcoded color literals       0           (clean)
  direct @bytelyst/ui imports    0           (clean)

Verified: pnpm typecheck, test (96/96), ratchet at new baseline.
This commit is contained in:
saravanakumardb1 2026-05-23 01:49:15 -07:00
parent 8d484c30d1
commit 3288e28f5c
11 changed files with 100 additions and 85 deletions

View File

@ -1,7 +1,7 @@
{
"//": "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,
"//": "Baseline UI drift counts. Updated 2026-05-23T08:48:54Z by scripts/ui-drift-ratchet.sh --update. Commit alongside the migration that lowered the counts.",
"raw_interactive_controls": 14,
"legacy_global_surface_classes": 21,
"hardcoded_color_literals": 0,
"direct_bytelyst_ui_imports_outside_adapter": 0
}

View File

@ -13,7 +13,7 @@ import { ArtifactPanel } from "@/components/ArtifactPanel";
import { AgentTimeline } from "@/components/AgentTimeline";
import { SmartActionsPanel } from "@/components/SmartActionsPanel";
import { LinkNoteModal } from "@/components/LinkNoteModal";
import { Badge, Button } from "@/components/ui/Primitives";
import { Badge, Button, Card } from "@/components/ui/Primitives";
import {
archiveNote,
createNoteArtifact,
@ -214,11 +214,11 @@ export default function NoteDetailPage() {
<AppShell
title="Note detail"
description="Editable note surface with metadata, linked context, tasks, artifacts, and review history."
actions={<div className="badge">Loading</div>}
actions={<Badge variant="neutral">Loading</Badge>}
>
<section className="surface-card" style={{ padding: "var(--nl-space-6)", color: "var(--nl-text-secondary)" }}>
<Card padding="lg" className="text-[color:var(--nl-text-secondary)]">
{error ?? "Loading note…"}
</section>
</Card>
</AppShell>
);
}
@ -232,8 +232,8 @@ export default function NoteDetailPage() {
{isSaving ? (
<Badge>Saving</Badge>
) : (
<Link href="/reviews" className="badge">
{`Review: ${note.metadata.reviewState}`}
<Link href="/reviews">
<Badge variant="neutral">{`Review: ${note.metadata.reviewState}`}</Badge>
</Link>
)}
<Button variant="secondary" onClick={handleSummarize}>
@ -269,7 +269,7 @@ export default function NoteDetailPage() {
/>
<aside style={{ display: "grid", gap: "var(--nl-space-4)" }}>
{error ? <div className="surface-card" style={{ padding: "var(--nl-space-4)", color: "var(--nl-text-secondary)" }}>{error}</div> : null}
{error ? <Card padding="sm" className="text-[color:var(--nl-text-secondary)]">{error}</Card> : null}
<SmartActionsPanel
noteId={note.id}
workspaceId={note.workspaceId}

View File

@ -2,7 +2,7 @@
import { useEffect, useState } from "react";
import { AppShell } from "@/components/AppShell";
import { Button } from "@/components/ui/Primitives";
import { Button, Select } from "@/components/ui/Primitives";
import { PalacePanel } from "@/components/PalacePanel";
import { PalaceStats } from "@/components/PalaceStats";
import { KnowledgeGraphView } from "@/components/KnowledgeGraphView";
@ -45,20 +45,16 @@ export default function PalacePage() {
<h1 style={{ fontSize: "var(--nl-fs-2xl)", fontWeight: 700 }}>Memory Palace</h1>
{!loadingWings && wings.length > 0 && (
<select
<Select
value={selectedWing ?? ""}
onChange={(e) => setSelectedWing(e.target.value || undefined)}
className="input"
style={{ maxWidth: 240 }}
aria-label="Select wing"
>
<option value="">All wings</option>
{wings.map((w) => (
<option key={w.id} value={w.id}>
{w.name} ({w.memoryCount})
</option>
))}
</select>
className="max-w-[240px]"
options={[
{ value: "", label: "All wings" },
...wings.map((w) => ({ value: w.id, label: `${w.name} (${w.memoryCount})` })),
]}
/>
)}
</div>

View File

@ -96,9 +96,9 @@ export default function PromptsPage() {
</div>
{loading && (
<div className="surface-card" style={{ padding: "var(--nl-space-6)", textAlign: "center", color: "var(--nl-text-secondary)" }}>
<Card padding="lg" className="text-center text-[color:var(--nl-text-secondary)]">
Loading templates
</div>
</Card>
)}
{/* Built-in templates */}
@ -167,9 +167,9 @@ export default function PromptsPage() {
)}
{!loading && filtered.length === 0 && (
<div className="surface-card" style={{ padding: "var(--nl-space-6)", textAlign: "center", color: "var(--nl-text-secondary)" }}>
<Card padding="lg" className="text-center text-[color:var(--nl-text-secondary)]">
No templates found for this category.
</div>
</Card>
)}
</AppShell>
);

View File

@ -3,7 +3,7 @@
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { Brain, Plus, AlertTriangle } from "lucide-react";
import { Button } from "@/components/ui/Primitives";
import { Badge, Button, Card } from "@/components/ui/Primitives";
import { getKnowledgeGaps } from "@/lib/prompt-client";
import { toast } from "@/lib/toast";
import type { KnowledgeGap } from "@/lib/types";
@ -59,29 +59,29 @@ export default function KnowledgeGapsPage() {
{/* Topic coverage map */}
{analyzed && Object.keys(topicMap).length > 0 && (
<div className="surface-card" style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-3)" }}>
<strong style={{ fontSize: "var(--nl-fs-sm)" }}>Topic Coverage</strong>
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap" }}>
<Card padding="sm" className="grid gap-3">
<strong className="text-[length:var(--nl-fs-sm)]">Topic Coverage</strong>
<div className="flex flex-wrap gap-2">
{Object.entries(topicMap)
.sort(([, a], [, b]) => b - a)
.map(([topic, count]) => (
<span key={topic} className="badge" style={{ fontSize: "var(--nl-fs-xs)" }}>
<Badge key={topic} variant="neutral" size="sm">
{topic} ({count})
</span>
</Badge>
))}
</div>
</div>
</Card>
)}
{/* Gaps list */}
{analyzed && gaps.length === 0 && !loading && (
<div className="surface-muted" style={{ padding: "var(--nl-space-6)", textAlign: "center", color: "var(--nl-text-secondary)" }}>
<Card padding="lg" className="bg-[color:var(--nl-surface-muted)] text-center text-[color:var(--nl-text-secondary)]">
No knowledge gaps detected. Your workspace has good topic coverage.
</div>
</Card>
)}
{gaps.map((gap, i) => (
<div key={i} className="surface-card" style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}>
<Card key={i} padding="sm" className="grid gap-2">
<div style={{ display: "flex", alignItems: "center", gap: "var(--nl-space-2)" }}>
<AlertTriangle size={14} style={{ color: "var(--nl-warning)" }} />
<strong style={{ fontSize: "var(--nl-fs-md)" }}>{gap.topic}</strong>
@ -101,7 +101,7 @@ export default function KnowledgeGapsPage() {
<Plus size={14} /> Create: {gap.suggestedTitle}
</Button>
</div>
</div>
</Card>
))}
</div>
);

View File

@ -1,27 +1,36 @@
import Link from "next/link";
import { Badge, Card } from "@/components/ui/Primitives";
export default function HomePage() {
return (
<main style={{ minHeight: "100vh", display: "grid", placeItems: "center", padding: "var(--nl-space-8)" }}>
<section className="surface-card" style={{ maxWidth: 760, padding: "var(--nl-space-8)", display: "grid", gap: "var(--nl-space-5)" }}>
<div className="badge">Backend-backed web surface</div>
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
<h1 style={{ margin: 0, fontFamily: "var(--nl-font-display)", fontSize: "var(--nl-fs-3xl)" }}>
<main className="grid min-h-screen place-items-center p-8">
<Card padding="lg" className="grid max-w-[760px] gap-5 p-8">
<Badge variant="info" className="justify-self-start">
Backend-backed web surface
</Badge>
<div className="grid gap-3">
<h1 className="m-0 font-[family-name:var(--nl-font-display)] text-[length:var(--nl-fs-3xl)]">
NoteLett
</h1>
<p style={{ margin: 0, color: "var(--nl-text-secondary)", lineHeight: 1.6 }}>
<p className="m-0 leading-relaxed text-[color:var(--nl-text-secondary)]">
Structured notes workspace for humans and agents with search, review, and operational context.
</p>
</div>
<div style={{ display: "flex", gap: "var(--nl-space-3)", flexWrap: "wrap" }}>
<Link href="/dashboard" className="surface-muted" style={{ padding: "12px 16px", background: "var(--nl-accent-muted)" }}>
<div className="flex flex-wrap gap-3">
<Link
href="/dashboard"
className="rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-accent-muted)] px-4 py-3"
>
Open dashboard
</Link>
<Link href="/workspaces" className="surface-muted" style={{ padding: "12px 16px" }}>
<Link
href="/workspaces"
className="rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] px-4 py-3"
>
Browse workspaces
</Link>
</div>
</section>
</Card>
</main>
);
}

View File

@ -2,6 +2,7 @@
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { Badge, Card } from "@/components/ui/Primitives";
import { NOTES_API_URL, PRODUCT_NAME } from "@/lib/product-config";
import { sanitizeSharedNoteHtml } from "@/lib/sanitize-share-html";
@ -46,28 +47,31 @@ export default function SharedNotePage() {
}, [token]);
return (
<div style={{ minHeight: "100vh", background: "var(--nl-bg-canvas)", color: "var(--nl-text-primary)", padding: "var(--nl-space-8)" }}>
<header style={{ maxWidth: 720, margin: "0 auto var(--nl-space-6)" }}>
<div className="badge" style={{ marginBottom: 12 }}>
<div className="min-h-screen bg-[color:var(--nl-bg-canvas)] p-8 text-[color:var(--nl-text-primary)]">
<header className="mx-auto mb-6 max-w-[720px]">
<Badge variant="info" className="mb-3">
Read-only public share · {PRODUCT_NAME}
</div>
<h1 style={{ margin: 0, fontSize: "var(--nl-fs-2xl)" }}>{note?.title ?? (error ? "Unavailable" : "Loading…")}</h1>
</Badge>
<h1 className="m-0 text-[length:var(--nl-fs-2xl)]">
{note?.title ?? (error ? "Unavailable" : "Loading…")}
</h1>
</header>
<main id="main-content" className="surface-card" style={{ maxWidth: 720, margin: "0 auto", padding: "var(--nl-space-6)" }}>
{error ? <p style={{ color: "var(--nl-text-secondary)" }}>{error}</p> : null}
{note ? (
<>
<p style={{ color: "var(--nl-text-secondary)", fontSize: "var(--nl-fs-sm)" }}>
Updated {new Date(note.updatedAt).toLocaleString()} · {note.expiresAt ? `Expires ${new Date(note.expiresAt).toLocaleDateString()}` : "Expires when revoked"} · Note ID {note.noteId}
</p>
<div
className="input-shell"
style={{ marginTop: 16, minHeight: 200, lineHeight: 1.7 }}
dangerouslySetInnerHTML={{ __html: sanitizeSharedNoteHtml(note.body) }}
/>
</>
) : null}
</main>
<Card padding="lg" className="mx-auto max-w-[720px]">
<main id="main-content">
{error ? <p className="text-[color:var(--nl-text-secondary)]">{error}</p> : null}
{note ? (
<>
<p className="text-[length:var(--nl-fs-sm)] text-[color:var(--nl-text-secondary)]">
Updated {new Date(note.updatedAt).toLocaleString()} · {note.expiresAt ? `Expires ${new Date(note.expiresAt).toLocaleDateString()}` : "Expires when revoked"} · Note ID {note.noteId}
</p>
<div
className="mt-4 min-h-[200px] rounded-[var(--nl-radius-md)] border border-[color:var(--nl-border-default)] bg-[color:var(--nl-input-bg)] p-3 leading-relaxed"
dangerouslySetInnerHTML={{ __html: sanitizeSharedNoteHtml(note.body) }}
/>
</>
) : null}
</main>
</Card>
</div>
);
}

View File

@ -1,6 +1,7 @@
"use client";
import { useEffect, useState, useCallback, useRef } from "react";
import { Button } from "@/components/ui/Primitives";
import { getBroadcastClient } from "@/lib/broadcast-client";
import type { InAppMessage } from "@bytelyst/broadcast-client";
@ -65,15 +66,21 @@ export function BroadcastBanner() {
<strong style={{ whiteSpace: "nowrap" }}>{msg.title}</strong>
{msg.body && <span style={{ color: "var(--nl-text-secondary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{msg.body}</span>}
{msg.ctaUrl && (
<button onClick={() => handleClick(msg)} style={{ background: "none", border: "none", color: "var(--nl-accent-primary)", cursor: "pointer", fontWeight: 600, whiteSpace: "nowrap" }}>
<Button variant="ghost" size="sm" onClick={() => handleClick(msg)} className="whitespace-nowrap font-semibold text-[color:var(--nl-accent-primary)]">
{msg.ctaText ?? "Learn more"}
</button>
</Button>
)}
</div>
{msg.dismissible !== false && (
<button onClick={() => dismiss(msg.id)} aria-label="Dismiss" style={{ background: "none", border: "none", color: "var(--nl-text-secondary)", cursor: "pointer", fontSize: 16, lineHeight: 1, padding: "0 0 0 var(--nl-space-3)" }}>
&times;
</button>
<Button
variant="ghost"
size="sm"
onClick={() => dismiss(msg.id)}
aria-label="Dismiss"
className="text-[color:var(--nl-text-secondary)]"
>
×
</Button>
)}
</div>
))}

View File

@ -1,7 +1,7 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { Button, Card } from "@/components/ui/Primitives";
import { Button, Card, Input } from "@/components/ui/Primitives";
import { queryEntity, getEntityTimeline, getKGContradictions, type PalaceKGTriple } from "@/lib/palace-client";
interface KnowledgeGraphViewProps {
@ -61,15 +61,14 @@ export function KnowledgeGraphView({ wingId }: KnowledgeGraphViewProps) {
<div style={{ fontWeight: 700, fontSize: "var(--nl-text-lg)" }}>Knowledge Graph</div>
<div style={{ display: "flex", gap: "var(--nl-space-2)" }}>
<input
<Input
type="text"
value={entity}
onChange={(e) => setEntity(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleQuery()}
placeholder="Query entity (e.g. React, Fastify)..."
aria-label="Query knowledge graph entity"
className="input"
style={{ flex: 1 }}
className="flex-1"
/>
<Button onClick={handleQuery} aria-label="Query entity">
Query

View File

@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { Badge } from "@/components/ui/Primitives";
import { listMemories, type PalaceMemory } from "@/lib/palace-client";
interface MemoryTimelineProps {
@ -57,7 +58,7 @@ export function MemoryTimeline({ wingId }: MemoryTimelineProps) {
{mems.map((mem) => (
<div key={mem.id} style={{ display: "grid", gap: "var(--nl-space-1)" }}>
<div style={{ display: "flex", gap: "var(--nl-space-2)", alignItems: "center" }}>
<span className="badge" style={{ fontSize: "0.7rem" }}>{mem.hall}</span>
<Badge variant="neutral" size="sm">{mem.hall}</Badge>
<span style={{ color: "var(--nl-text-secondary)", fontSize: "0.75rem" }}>
{new Date(mem.createdAt).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}
</span>

View File

@ -1,7 +1,7 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { Button, Card } from "@/components/ui/Primitives";
import { Badge, Button, Card, Input } from "@/components/ui/Primitives";
import { searchPalace, listMemories, type PalaceMemory } from "@/lib/palace-client";
interface PalacePanelProps {
@ -62,15 +62,14 @@ export function PalacePanel({ wingId }: PalacePanelProps) {
<div style={{ fontWeight: 700, fontSize: "var(--nl-text-lg)" }}>Palace Memory</div>
<div style={{ display: "flex", gap: "var(--nl-space-2)" }}>
<input
<Input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
placeholder="Search memories..."
aria-label="Search palace memories"
className="input"
style={{ flex: 1 }}
className="flex-1"
/>
<Button onClick={handleSearch} aria-label="Search">
Search
@ -99,12 +98,12 @@ export function PalacePanel({ wingId }: PalacePanelProps) {
}}
>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span
className="badge"
style={{ backgroundColor: hallColor[mem.hall] ?? "var(--nl-text-secondary)", color: "var(--nl-on-accent)", fontSize: "0.75rem" }}
<Badge
variant="neutral"
style={{ backgroundColor: hallColor[mem.hall] ?? "var(--nl-text-secondary)", color: "var(--nl-on-accent)" }}
>
{mem.hall}
</span>
</Badge>
<span style={{ color: "var(--nl-text-secondary)", fontSize: "0.75rem" }}>
{new Date(mem.createdAt).toLocaleDateString()}
</span>