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:
+40
-7
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user