"use client"; // THE /channels FOCUS LINE โ€” the corpus-wide focus selector. // // It is ALWAYS on the page, with or without a selection: the focus is ONE // corpus-wide fact, so choosing a site to focus and ending a focus are not // properties of a row selection and must not require one. The selection verbs // live in `ChannelSelectionDeck`, which docks below the rack and unmounts when // nothing is ticked. // // Both post through `saveChannelPriorityAction`'s named gestures, which is the // only function that writes `settings.channelPriority` and the only one that // recompiles the four lane trees. import { useState, useTransition } from "react"; import { endFocusAction, focusSiteAction, type ActionResult, } from "../actions"; // A FAILED WRITE MUST SAY SO โ€” the writer returns `{error}` before it // revalidates, so a swallowed result reads as "the click did nothing". One // runner per bar, one alert line, the shape SyncAllChannelsButton already uses. export function useBarAction(): { pending: boolean; error: string | null; run: (action: () => Promise, after?: () => void) => void; } { const [pending, startTransition] = useTransition(); const [error, setError] = useState(null); return { pending, error, run: (action, after) => startTransition(async () => { try { const result = await action(); setError(result?.error ?? null); if (!result?.error) after?.(); } catch (e) { setError((e as Error).message); } }), }; } export type FocusSite = { siteId: string; title: string }; export function ChannelFocusBar({ sites, focusLabel, heldCount = 0, }: { sites: FocusSite[]; // What is focused right now, already resolved by the page ("site Foo", // "2 channels"), or null when nothing is. The full banner with the per-lane // pending numbers is S4's FocusBanner; this bar is only the control. focusLabel: string | null; // How many rows the focus is holding. Stated HERE, once, because it is one // corpus-wide fact โ€” which is what lets each held row shrink to a chip // instead of repeating the same sentence 61 times. heldCount?: number; }) { const { pending, error, run } = useBarAction(); const [siteId, setSiteId] = useState(sites[0]?.siteId ?? ""); if (sites.length === 0 && !focusLabel) return null; return ( // A STATEMENT, not a panel: one line under the header with a rule beneath // it, not a card competing with the rack for weight. The eyebrow names it // the way the nav groups and the deck's verb groups are named.
Focus {focusLabel ? `Focus: ${focusLabel}` : "No focus"} {focusLabel && heldCount > 0 && ( ยท {heldCount} held )} {sites.length > 0 && ( <> )} {focusLabel && ( )} {error && ( {error} )}
); }