// The shared icon-only control block (#230), consumed by the title pages, // decks and readouts in main.ts, the settings rows (#231) and the subtitle // chips and provider list (#251). One glyph set, one arm-then-confirm idiom — // never a second implementation per view. A view that cannot fit the shared // control grows a variant of it here; it does not keep a private copy. export const SVG_NS = "http://www.w3.org/2000/svg"; const ICON_PATHS = { trash: [ "M2.8 4.3h10.4", "M5.7 4.3V3.1c0-.5.4-.9.9-.9h2.8c.5 0 .9.4.9.9v1.2", "M4.2 4.3l.5 8.5c0 .8.7 1.4 1.5 1.4h3.6c.8 0 1.5-.6 1.5-1.4l.5-8.5", "M6.6 7v4.2", "M9.4 7v4.2", ], search: ["M7.2 2.6a4.4 4.4 0 1 1 0 8.8 4.4 4.4 0 0 1 0-8.8Z", "M10.4 10.4l3.2 3.2"], disk: [ "M2.5 9.8l1.6-5c.2-.7.8-1.2 1.5-1.2h4.8c.7 0 1.3.5 1.5 1.2l1.6 5", "M2.5 9.8h11v2.5c0 .7-.5 1.2-1.2 1.2H3.7c-.7 0-1.2-.5-1.2-1.2z", "M10.9 11.6h.01", ], pencil: [ "M11.2 2.4a1.5 1.5 0 0 1 2.1 0l.3.3a1.5 1.5 0 0 1 0 2.1L5.9 12.5l-3.2.8.8-3.2z", "M10.1 3.5l2.4 2.4", ], cog: [ "M6.81 3.56 L7.00 1.68 L9.00 1.68 L9.19 3.56 L11.25 4.75 L12.97 3.97 L13.97 5.71 L12.44 6.81 L12.44 9.19 L13.97 10.29 L12.97 12.03 L11.25 11.25 L9.19 12.44 L9.00 14.32 L7.00 14.32 L6.81 12.44 L4.75 11.25 L3.03 12.03 L2.03 10.29 L3.56 9.19 L3.56 6.81 L2.03 5.71 L3.03 3.97 L4.75 4.75Z", "M8 5.6a2.4 2.4 0 1 0 0 4.8 2.4 2.4 0 0 0 0-4.8Z", ], warning: ["M8 1.8l6.2 11H1.8z", "M8 5.3v3.4M8 11.2h.01"], check: ["M3.2 8.4l3 3 6.6-6.8"], // the caption frame the rest of the world draws for subtitles: a screen // with two lines of text sitting low in it captions: [ "M2 3.4h12c.4 0 .7.3.7.7v7.8c0 .4-.3.7-.7.7H2c-.4 0-.7-.3-.7-.7V4.1c0-.4.3-.7.7-.7Z", "M4.1 8.2h3.1", "M8.8 8.2h3.1", "M4.1 10.5h7.8", ], chevronUp: ["M3 10.2l5-4.6 5 4.6"], chevronDown: ["M3 5.8l5 4.6 5-4.6"], chevronRight: ["M6 3l4.6 5L6 13"], back: ["M13 8H3.4", "M7.4 3.8L3.2 8l4.2 4.2"], play: ["M4.6 2.9v10.2c0 .4.4.6.7.4l7.8-5.1c.3-.2.3-.6 0-.8L5.3 2.5c-.3-.2-.7 0-.7.4Z"], more: [ "M3.2 7.3a.7.7 0 1 1 0 1.4.7.7 0 0 1 0-1.4Z", "M8 7.3a.7.7 0 1 1 0 1.4.7.7 0 0 1 0-1.4Z", "M12.8 7.3a.7.7 0 1 1 0 1.4.7.7 0 0 1 0-1.4Z", ], plus: ["M8 3v10", "M3 8h10"], grid: [ "M2.5 2.5h4.4v4.4H2.5z", "M9.1 2.5h4.4v4.4H9.1z", "M2.5 9.1h4.4v4.4H2.5z", "M9.1 9.1h4.4v4.4H9.1z", ], list: ["M5.6 4h8", "M5.6 8h8", "M5.6 12h8", "M2.4 4h.01", "M2.4 8h.01", "M2.4 12h.01"], library: ["M2.5 2.5h3v11h-3z", "M6.5 2.5h3v11h-3z", "M10.3 3.1l2.9-.8 2.3 10.6-2.9.7z"], close: ["M3.6 3.6l8.8 8.8", "M12.4 3.6l-8.8 8.8"], info: ["M8 1.8a6.2 6.2 0 1 1 0 12.4A6.2 6.2 0 0 1 8 1.8Z", "M8 7.2v4", "M8 4.8h.01"], } as const; export type IconName = keyof typeof ICON_PATHS; /** Stroke glyph on the shared 16-grid; the control's aria-label carries the meaning. */ export function icon(name: IconName): SVGSVGElement { const svg = document.createElementNS(SVG_NS, "svg"); svg.setAttribute("viewBox", "0 0 16 16"); svg.setAttribute("class", "icon"); svg.setAttribute("aria-hidden", "true"); svg.dataset.icon = name; for (const d of ICON_PATHS[name]) { const path = document.createElementNS(SVG_NS, "path"); path.setAttribute("d", d); svg.append(path); } return svg; } /** * How much room the control has, and where the armed state explains itself. */ export interface ArmedDeleteOptions { /** * Chip scale: the control plate comes off (§9.6 keeps a chip to one row). * The two clicks, the amber armed state and the 4-second disarm are the * same control either way. */ compact?: boolean; /** * The row's status slot. Arming writes "tap again to …" here and disarming * clears it, so the button itself never changes size — the explanation * lives beside it, not inside it. */ explain?: (text: string | null) => void; } /** * The arm-then-confirm, icon-only: a trash glyph resting, an amber check * while armed, in the same square either way, so an accidental hit never * destroys and arming never moves its neighbours. */ export function armedDeleteIcon( label: string, execute: () => void, options: ArmedDeleteOptions = {}, ): HTMLButtonElement { const button = document.createElement("button"); button.type = "button"; button.className = options.compact ? "control-micro" : "control control-icon"; button.dataset.kind = "delete"; button.setAttribute("aria-label", label); button.replaceChildren(icon("trash")); let armed = false; let resetTimer: number | undefined; const disarm = () => { armed = false; window.clearTimeout(resetTimer); delete button.dataset.armed; button.setAttribute("aria-label", label); button.replaceChildren(icon("trash")); options.explain?.(null); }; button.addEventListener("click", () => { if (armed) { disarm(); button.disabled = true; execute(); return; } armed = true; button.dataset.armed = "true"; button.replaceChildren(icon("check")); button.setAttribute("aria-label", `confirm — ${label}`); options.explain?.(`tap again to ${label}`); resetTimer = window.setTimeout(disarm, 4000); }); button.addEventListener("blur", () => { if (armed) { disarm(); } }); return button; }