diff --git a/web/src/icons.ts b/web/src/icons.ts index 45786e2..8f6c09a 100644 --- a/web/src/icons.ts +++ b/web/src/icons.ts @@ -1,6 +1,8 @@ // The shared icon-only control block (#230), consumed by the title pages, -// decks and readouts in main.ts and the settings rows (#231). One glyph set, -// one arm-then-confirm idiom — never a second implementation per view. +// 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"; @@ -22,6 +24,9 @@ const ICON_PATHS = { "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", ], + check: ["M3.2 8.4l3.1 3.1 6.5-6.9"], + chevronUp: ["M3 10.2l5-4.6 5 4.6"], + chevronDown: ["M3 5.8l5 4.6 5-4.6"], } as const; /** Stroke glyph on the shared 16-grid; the control's aria-label carries the meaning. */ @@ -39,13 +44,35 @@ export function icon(name: keyof typeof ICON_PATHS): SVGSVGElement { } /** - * The settings arm-then-confirm, icon-only: a trash glyph resting, the - * visible word "confirm" while armed, so an accidental hit never destroys. + * How much room the control has. A settings row can spell the confirmation + * out; a chip cannot, and says the same thing in one glyph. */ -export function armedDeleteIcon(label: string, execute: () => void): HTMLButtonElement { +export interface ArmedDeleteOptions { + /** + * Chip scale: the control plate comes off and the armed state swaps the + * trash for a check instead of rendering the word "confirm", which a chip + * has no width for (§9.6 keeps a chip to one row). Everything else — the + * two clicks, the amber armed state, the 4-second disarm, the label — is + * the same control, so a chip and a settings row arm alike. + */ + compact?: boolean; +} + +/** + * The settings arm-then-confirm, icon-only: a trash glyph resting, a visible + * change of what the next click does while armed, so an accidental hit never + * destroys. + */ +export function armedDeleteIcon( + label: string, + execute: () => void, + options: ArmedDeleteOptions = {}, +): HTMLButtonElement { const button = document.createElement("button"); button.type = "button"; - button.className = "control control-quiet control-icon"; + button.className = options.compact + ? "control control-quiet control-icon control-micro" + : "control control-quiet control-icon"; button.setAttribute("aria-label", label); button.title = label; button.replaceChildren(icon("trash")); @@ -56,6 +83,7 @@ export function armedDeleteIcon(label: string, execute: () => void): HTMLButtonE window.clearTimeout(resetTimer); delete button.dataset.armed; button.setAttribute("aria-label", label); + button.title = label; button.replaceChildren(icon("trash")); }; button.addEventListener("click", () => { @@ -67,8 +95,13 @@ export function armedDeleteIcon(label: string, execute: () => void): HTMLButtonE } armed = true; button.dataset.armed = "true"; - button.textContent = "confirm"; + if (options.compact) { + button.replaceChildren(icon("check")); + } else { + button.textContent = "confirm"; + } button.setAttribute("aria-label", `confirm — ${label}`); + button.title = `confirm — ${label}`; resetTimer = window.setTimeout(disarm, 4000); }); button.addEventListener("blur", () => { diff --git a/web/src/style.css b/web/src/style.css index 17abb03..5d9d316 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -1173,6 +1173,42 @@ body { border-color: var(--signal-warn); } +/* The chip-scale variant of the same control (#251). A chip has room for + neither the plate nor the word "confirm", so the plate comes off and the + armed glyph does the word's job — the two clicks, the amber and the + 4-second disarm are the shared block's, unchanged. */ +.control-micro { + min-width: 0; + min-height: 0; + width: 1rem; + height: 1rem; + padding: 0; + color: var(--ink-faint); + background: none; + border: 0; + box-shadow: none; + transition: + color 150ms var(--ease-out), + background 150ms var(--ease-out); +} + +.control-micro:hover:not(:disabled), +.control-micro:focus-visible { + color: var(--signal-fault); + background: oklch(from var(--signal-fault) l c h / 15%); +} + +/* armed wins over hover: the pointer is already on it when it arms */ +.control-micro[data-armed="true"], +.control-micro[data-armed="true"]:hover:not(:disabled) { + color: var(--signal-warn); + background: oklch(from var(--signal-warn) l c h / 15%); +} + +.control-micro:disabled { + opacity: 0.5; +} + .bucket-head { align-items: center; } @@ -1665,44 +1701,10 @@ body { border-color: oklch(from var(--signal-fault) l c h / 55%); } -/* the delete affordance nested in a present-subtitle chip (issue #223) */ -.chip-delete { - display: inline-flex; - align-items: center; - justify-content: center; - width: 1rem; - height: 1rem; +/* the delete affordance nested in a present-subtitle chip (issue #223): the + shared .control-micro, spaced off the chip's own label */ +.chip .control-micro { margin-inline-start: var(--space-1); - padding: 0; - background: none; - border: 0; - border-radius: var(--radius); - color: var(--ink-faint); - cursor: pointer; - transition: - color 150ms var(--ease-out), - background 150ms var(--ease-out); -} - -.chip-delete:hover, -.chip-delete:focus-visible { - color: var(--signal-fault); - background: oklch(from var(--signal-fault) l c h / 15%); -} - -.chip-delete[data-armed="true"] { - color: var(--signal-fault); - background: oklch(from var(--signal-fault) l c h / 15%); -} - -.chip-delete:disabled { - cursor: default; - opacity: 0.5; -} - -.close-icon { - width: 0.625rem; - height: 0.625rem; } /* ---- manual subtitle deck (§9.3, §15, issue #203) ---------------------- */ @@ -2406,10 +2408,6 @@ select.form-input { width: 5.5rem; } -.provider-move { - min-width: 3rem; -} - .provider-move:disabled { visibility: hidden; }