refactor(web): extract shared icon controls to module
The #230 icon block (glyph set, icon(), armedDeleteIcon()) moves from main.ts to icons.ts so the settings rows (#231) can consume it without an import cycle. The shared set is extended with one pencil glyph for the rows' edit action — extension of the shared block, not a fork. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
// 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.
|
||||
|
||||
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",
|
||||
],
|
||||
} as const;
|
||||
|
||||
/** Stroke glyph on the shared 16-grid; the control's aria-label carries the meaning. */
|
||||
export function icon(name: keyof typeof ICON_PATHS): SVGSVGElement {
|
||||
const svg = document.createElementNS(SVG_NS, "svg");
|
||||
svg.setAttribute("viewBox", "0 0 16 16");
|
||||
svg.setAttribute("class", "icon");
|
||||
svg.setAttribute("aria-hidden", "true");
|
||||
for (const d of ICON_PATHS[name]) {
|
||||
const path = document.createElementNS(SVG_NS, "path");
|
||||
path.setAttribute("d", d);
|
||||
svg.append(path);
|
||||
}
|
||||
return svg;
|
||||
}
|
||||
|
||||
/**
|
||||
* The settings arm-then-confirm, icon-only: a trash glyph resting, the
|
||||
* visible word "confirm" while armed, so an accidental hit never destroys.
|
||||
*/
|
||||
export function armedDeleteIcon(label: string, execute: () => void): HTMLButtonElement {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "control control-quiet control-icon";
|
||||
button.setAttribute("aria-label", label);
|
||||
button.title = 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"));
|
||||
};
|
||||
button.addEventListener("click", () => {
|
||||
if (armed) {
|
||||
disarm();
|
||||
button.disabled = true;
|
||||
execute();
|
||||
return;
|
||||
}
|
||||
armed = true;
|
||||
button.dataset.armed = "true";
|
||||
button.textContent = "confirm";
|
||||
button.setAttribute("aria-label", `confirm — ${label}`);
|
||||
resetTimer = window.setTimeout(disarm, 4000);
|
||||
});
|
||||
button.addEventListener("blur", () => {
|
||||
if (armed) {
|
||||
disarm();
|
||||
}
|
||||
});
|
||||
return button;
|
||||
}
|
||||
+3
-72
@@ -1,4 +1,5 @@
|
||||
import { type CheckStatus, type Probe, probeHealth } from "./health";
|
||||
import { armedDeleteIcon, icon, SVG_NS } from "./icons";
|
||||
import {
|
||||
fetchLibrary,
|
||||
type LibrarySeries,
|
||||
@@ -1265,37 +1266,8 @@ function wireCollapsedToggle(bucket: CollapsedBucketDom) {
|
||||
|
||||
/* ---- icon-only controls (#230) ----------------------------------------- */
|
||||
|
||||
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",
|
||||
],
|
||||
} as const;
|
||||
|
||||
/** Stroke glyph on the shared 16-grid; the control's aria-label carries the meaning. */
|
||||
function icon(name: keyof typeof ICON_PATHS): SVGSVGElement {
|
||||
const svg = document.createElementNS(SVG_NS, "svg");
|
||||
svg.setAttribute("viewBox", "0 0 16 16");
|
||||
svg.setAttribute("class", "icon");
|
||||
svg.setAttribute("aria-hidden", "true");
|
||||
for (const d of ICON_PATHS[name]) {
|
||||
const path = document.createElementNS(SVG_NS, "path");
|
||||
path.setAttribute("d", d);
|
||||
svg.append(path);
|
||||
}
|
||||
return svg;
|
||||
}
|
||||
// icon() and armedDeleteIcon() live in icons.ts, shared with the settings
|
||||
// rows (#231); the §9.6 painters below stay here as their only consumer.
|
||||
|
||||
/** §9.6 provider marks, drawn and shipped inline — never a remote image. */
|
||||
function providerMark(name: "tmdb" | "tvdb" | "imdb" | "rt"): SVGSVGElement {
|
||||
@@ -1334,47 +1306,6 @@ function providerMark(name: "tmdb" | "tvdb" | "imdb" | "rt"): SVGSVGElement {
|
||||
return svg;
|
||||
}
|
||||
|
||||
/**
|
||||
* The settings arm-then-confirm, icon-only: a trash glyph resting, the
|
||||
* visible word "confirm" while armed, so an accidental hit never destroys.
|
||||
*/
|
||||
function armedDeleteIcon(label: string, execute: () => void): HTMLButtonElement {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "control control-quiet control-icon";
|
||||
button.setAttribute("aria-label", label);
|
||||
button.title = 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"));
|
||||
};
|
||||
button.addEventListener("click", () => {
|
||||
if (armed) {
|
||||
disarm();
|
||||
button.disabled = true;
|
||||
execute();
|
||||
return;
|
||||
}
|
||||
armed = true;
|
||||
button.dataset.armed = "true";
|
||||
button.textContent = "confirm";
|
||||
button.setAttribute("aria-label", `confirm — ${label}`);
|
||||
resetTimer = window.setTimeout(disarm, 4000);
|
||||
});
|
||||
button.addEventListener("blur", () => {
|
||||
if (armed) {
|
||||
disarm();
|
||||
}
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
/* ---- §9.6 painters shared by both detail pages ------------------------- */
|
||||
|
||||
/** The drawn star before a TMDB rating — no glyph standing in for an icon. */
|
||||
|
||||
Reference in New Issue
Block a user