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 { type CheckStatus, type Probe, probeHealth } from "./health";
|
||||||
|
import { armedDeleteIcon, icon, SVG_NS } from "./icons";
|
||||||
import {
|
import {
|
||||||
fetchLibrary,
|
fetchLibrary,
|
||||||
type LibrarySeries,
|
type LibrarySeries,
|
||||||
@@ -1265,37 +1266,8 @@ function wireCollapsedToggle(bucket: CollapsedBucketDom) {
|
|||||||
|
|
||||||
/* ---- icon-only controls (#230) ----------------------------------------- */
|
/* ---- icon-only controls (#230) ----------------------------------------- */
|
||||||
|
|
||||||
const SVG_NS = "http://www.w3.org/2000/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.
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** §9.6 provider marks, drawn and shipped inline — never a remote image. */
|
/** §9.6 provider marks, drawn and shipped inline — never a remote image. */
|
||||||
function providerMark(name: "tmdb" | "tvdb" | "imdb" | "rt"): SVGSVGElement {
|
function providerMark(name: "tmdb" | "tvdb" | "imdb" | "rt"): SVGSVGElement {
|
||||||
@@ -1334,47 +1306,6 @@ function providerMark(name: "tmdb" | "tvdb" | "imdb" | "rt"): SVGSVGElement {
|
|||||||
return svg;
|
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 ------------------------- */
|
/* ---- §9.6 painters shared by both detail pages ------------------------- */
|
||||||
|
|
||||||
/** The drawn star before a TMDB rating — no glyph standing in for an icon. */
|
/** The drawn star before a TMDB rating — no glyph standing in for an icon. */
|
||||||
|
|||||||
Reference in New Issue
Block a user