/** * The rail's SERVICE block — the "Steward" box. * * Three stacked zones: the brand lockup, the control row, and the router facts * folded in from what used to be a separate CONFIG block. The status readout — * `started` / `stopped` / `not connected` — is a chip riding in the lockup row * beside the wordmark, not a zone of its own: it is the one thing in the block * that cannot be acted on, so it does not get the block's heaviest element. * * The chip is deliberately not a button: it reports state and nothing more, so * it is a `role="status"` region that a screen reader announces when the state * changes. Its state rides a SHAPE (`data-state` → filled disc, ring, dotted * ring) as well as a hue, so it survives a monochrome screen. * * The control row holds only actions this machine actually has a consented * command for; with none, one setup affordance takes its place. Stop and * restart never fire from a single click — they open an inline confirm strip * that names what will be unloaded, moves focus to Cancel, and closes on Esc. * A command that failed leaves an alert here rather than nothing at all. */ import type { DriftNoticeVm, ServiceControlVm, ServiceVm } from "../../core/select.js"; import type { ServiceAction } from "../../core/types.js"; import type { View } from "../dom.js"; import { el, setAttr, setText, setVar, svg, syncRows } from "../dom.js"; export interface ServiceHandlers { onToggleTheme: () => void; /** Runs an action now: a non-disruptive one, or one already confirmed. */ onService: (action: ServiceAction) => void; /** Asks for the confirm strip before a disruptive action. */ onConfirmService: (action: ServiceAction) => void; /** Dismisses the confirm strip without acting. */ onCancelService: () => void; /** * Quiets the drift notice for this session. It is keyed by what drifted, so * the notice returns on a reload and the moment the mismatch changes. */ onDismissDrift: (key: string) => void; } /** One router fact: `listen 127.0.0.1:8080`. Patched per repaint. */ interface FactRow { root: HTMLElement; key: HTMLElement; value: HTMLElement; } function createFactRow(): FactRow { const key = el("span", { class: "config-row__key" }); const value = el("span", { class: "config-row__value" }); return { root: el("div", { class: "config-row", children: [key, value] }), key, value }; } /** * One control button. The action and whether it needs confirming live on the * record rather than in the closure, so a row can be reused across repaints * when the available set changes without rebinding its listener. */ interface ControlRow { root: HTMLButtonElement; action: ServiceAction; confirms: boolean; } /** The Steward mark: a serving dome over two trays, the lower one faded. */ function mark(): SVGElement { return svg( "svg", { width: "20", height: "20", viewBox: "0 0 40 40", fill: "none", "aria-hidden": "true", class: "lockup__mark", focusable: "false", }, [ svg("path", { d: "M8 17C8 10.9 13.4 6 20 6s12 4.9 12 11", stroke: "currentColor", "stroke-width": "4", "stroke-linecap": "round", }), svg("rect", { x: "4", y: "21", width: "32", height: "5", rx: "2.5", fill: "currentColor" }), svg("rect", { x: "4", y: "29.6", width: "32", height: "5", rx: "2.5", fill: "currentColor", opacity: "0.55", }), ], ); } export function createServiceBlock(handlers: ServiceHandlers): View { const statusDot = el("span", { class: "service__status-dot", attrs: { "aria-hidden": "true" } }); const statusLabel = el("span", { class: "service__status-label" }); // Not a