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.", "//": "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": 25, "raw_interactive_controls": 14,
"legacy_global_surface_classes": 67, "legacy_global_surface_classes": 21,
"hardcoded_color_literals": 0, "hardcoded_color_literals": 0,
"direct_bytelyst_ui_imports_outside_adapter": 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 { 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 { Badge, Button, Card } from "@/components/ui/Primitives";
import { import {
archiveNote, archiveNote,
createNoteArtifact, createNoteArtifact,
@ -214,11 +214,11 @@ export default function NoteDetailPage() {
<AppShell <AppShell
title="Note detail" title="Note detail"
description="Editable note surface with metadata, linked context, tasks, artifacts, and review history." 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…"} {error ?? "Loading note…"}
</section> </Card>
</AppShell> </AppShell>
); );
} }
@ -232,8 +232,8 @@ export default function NoteDetailPage() {
{isSaving ? ( {isSaving ? (
<Badge>Saving</Badge> <Badge>Saving</Badge>
) : ( ) : (
<Link href="/reviews" className="badge"> <Link href="/reviews">
{`Review: ${note.metadata.reviewState}`} <Badge variant="neutral">{`Review: ${note.metadata.reviewState}`}</Badge>
</Link> </Link>
)} )}
<Button variant="secondary" onClick={handleSummarize}> <Button variant="secondary" onClick={handleSummarize}>
@ -269,7 +269,7 @@ export default function NoteDetailPage() {
/> />
<aside style={{ display: "grid", gap: "var(--nl-space-4)" }}> <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 <SmartActionsPanel
noteId={note.id} noteId={note.id}
workspaceId={note.workspaceId} workspaceId={note.workspaceId}

View File

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

View File

@ -96,9 +96,9 @@ export default function PromptsPage() {
</div> </div>
{loading && ( {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 Loading templates
</div> </Card>
)} )}
{/* Built-in templates */} {/* Built-in templates */}
@ -167,9 +167,9 @@ export default function PromptsPage() {
)} )}
{!loading && filtered.length === 0 && ( {!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. No templates found for this category.
</div> </Card>
)} )}
</AppShell> </AppShell>
); );

View File

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

View File

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

View File

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

View File

@ -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 { getBroadcastClient } from "@/lib/broadcast-client"; import { getBroadcastClient } from "@/lib/broadcast-client";
import type { InAppMessage } from "@bytelyst/broadcast-client"; import type { InAppMessage } from "@bytelyst/broadcast-client";
@ -65,15 +66,21 @@ export function BroadcastBanner() {
<strong style={{ whiteSpace: "nowrap" }}>{msg.title}</strong> <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.body && <span style={{ color: "var(--nl-text-secondary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{msg.body}</span>}
{msg.ctaUrl && ( {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"} {msg.ctaText ?? "Learn more"}
</button> </Button>
)} )}
</div> </div>
{msg.dismissible !== false && ( {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)" }}> <Button
&times; variant="ghost"
</button> size="sm"
onClick={() => dismiss(msg.id)}
aria-label="Dismiss"
className="text-[color:var(--nl-text-secondary)]"
>
×
</Button>
)} )}
</div> </div>
))} ))}

View File

@ -1,7 +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 { Button, Card, Input } 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 {
@ -61,15 +61,14 @@ export function KnowledgeGraphView({ wingId }: KnowledgeGraphViewProps) {
<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)" }}>
<input <Input
type="text" type="text"
value={entity} value={entity}
onChange={(e) => setEntity(e.target.value)} onChange={(e) => setEntity(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleQuery()} onKeyDown={(e) => e.key === "Enter" && handleQuery()}
placeholder="Query entity (e.g. React, Fastify)..." placeholder="Query entity (e.g. React, Fastify)..."
aria-label="Query knowledge graph entity" aria-label="Query knowledge graph entity"
className="input" className="flex-1"
style={{ flex: 1 }}
/> />
<Button onClick={handleQuery} aria-label="Query entity"> <Button onClick={handleQuery} aria-label="Query entity">
Query Query

View File

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

View File

@ -1,7 +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 { Badge, Button, Card, Input } 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 {
@ -62,15 +62,14 @@ export function PalacePanel({ wingId }: PalacePanelProps) {
<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)" }}>
<input <Input
type="text" type="text"
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSearch()} onKeyDown={(e) => e.key === "Enter" && handleSearch()}
placeholder="Search memories..." placeholder="Search memories..."
aria-label="Search palace memories" aria-label="Search palace memories"
className="input" className="flex-1"
style={{ flex: 1 }}
/> />
<Button onClick={handleSearch} aria-label="Search"> <Button onClick={handleSearch} aria-label="Search">
Search Search
@ -99,12 +98,12 @@ export function PalacePanel({ wingId }: PalacePanelProps) {
}} }}
> >
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}> <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span <Badge
className="badge" variant="neutral"
style={{ backgroundColor: hallColor[mem.hall] ?? "var(--nl-text-secondary)", color: "var(--nl-on-accent)", fontSize: "0.75rem" }} style={{ backgroundColor: hallColor[mem.hall] ?? "var(--nl-text-secondary)", color: "var(--nl-on-accent)" }}
> >
{mem.hall} {mem.hall}
</span> </Badge>
<span style={{ color: "var(--nl-text-secondary)", fontSize: "0.75rem" }}> <span style={{ color: "var(--nl-text-secondary)", fontSize: "0.75rem" }}>
{new Date(mem.createdAt).toLocaleDateString()} {new Date(mem.createdAt).toLocaleDateString()}
</span> </span>