feat(arr): compact armed delete and new glyphs

The subtitle chip needs the settings arm-then-confirm at chip scale: a
chip fits neither the control plate nor the word "confirm". The shared
control grows a compact variant that swaps the glyph instead, rather than
each view keeping a private copy of the idiom.
This commit is contained in:
Miguel Palhas
2026-08-25 18:11:06 +01:00
parent 34e5663920
commit 1e778afd18
2 changed files with 79 additions and 48 deletions
+40 -7
View File
@@ -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", () => {
+39 -41
View File
@@ -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;
}