learning_ai_notes/web/src/components/BroadcastBanner.tsx
saravanakumardb1 3288e28f5c 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.
2026-05-23 01:49:15 -07:00

90 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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";
export function BroadcastBanner() {
const [messages, setMessages] = useState<InAppMessage[]>([]);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const fetchMessages = useCallback(async () => {
try {
const { messages: list } = await getBroadcastClient().listMessages();
setMessages(list.filter((m) => m.status !== "dismissed"));
} catch {
// non-critical
}
}, []);
useEffect(() => {
fetchMessages();
intervalRef.current = setInterval(fetchMessages, 5 * 60_000);
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, [fetchMessages]);
async function dismiss(id: string) {
try {
await getBroadcastClient().markDismissed(id);
} catch {
// best-effort
}
setMessages((prev) => prev.filter((m) => m.id !== id));
}
async function handleClick(msg: InAppMessage) {
try {
await getBroadcastClient().trackClick(msg.id);
} catch {
// best-effort
}
if (msg.ctaUrl) window.open(msg.ctaUrl, "_blank", "noopener");
}
if (messages.length === 0) return null;
return (
<div style={{ display: "grid", gap: "var(--nl-space-2)", padding: "var(--nl-space-3) var(--nl-space-4)" }}>
{messages.map((msg) => (
<div
key={msg.id}
role="status"
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "var(--nl-space-3) var(--nl-space-4)",
background: msg.priority === "high" || msg.priority === "urgent" ? "var(--nl-warning-muted)" : "var(--nl-info-muted)",
borderRadius: "var(--nl-radius-sm)",
fontSize: "var(--nl-fs-sm)",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "var(--nl-space-3)", flex: 1, minWidth: 0 }}>
<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 variant="ghost" size="sm" onClick={() => handleClick(msg)} className="whitespace-nowrap font-semibold text-[color:var(--nl-accent-primary)]">
{msg.ctaText ?? "Learn more"}
</Button>
)}
</div>
{msg.dismissible !== false && (
<Button
variant="ghost"
size="sm"
onClick={() => dismiss(msg.id)}
aria-label="Dismiss"
className="text-[color:var(--nl-text-secondary)]"
>
×
</Button>
)}
</div>
))}
</div>
);
}