@wildwinter/app-shell 0.43.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/{chunk-MU2WO6KQ.js → chunk-MS3BSTQB.js} +10 -2
- package/dist/chunk-MS3BSTQB.js.map +1 -0
- package/dist/{chunk-URLZYOYR.js → chunk-OHVWMLRM.js} +3 -3
- package/dist/{chunk-URLZYOYR.js.map → chunk-OHVWMLRM.js.map} +1 -1
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/kit-gallery.cjs +8 -0
- package/dist/kit-gallery.cjs.map +1 -1
- package/dist/kit-gallery.css +3 -5
- package/dist/kit-gallery.css.map +1 -1
- package/dist/kit-gallery.d.cts +6 -0
- package/dist/kit-gallery.d.ts +6 -0
- package/dist/kit-gallery.js +1 -1
- package/dist/welcome.cjs.map +1 -1
- package/dist/welcome.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-MU2WO6KQ.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export { mountWelcome } from './chunk-
|
|
1
|
+
export { mountWelcome } from './chunk-OHVWMLRM.js';
|
|
2
2
|
export { feedUpdaterDownloadProgress, showUpdaterDialog } from './chunk-OPZPFOPS.js';
|
|
3
3
|
export { defaultProblemCopy, describeProblem, problemLine, problemName } from './chunk-4OAORWEM.js';
|
|
4
4
|
export { PROPERTY_NAME_HINT, PROPERTY_TYPES, RESERVED_PROPERTY_NAMES, bindPropertyName, bindPropertyRef, defaultControl, dupGuard, expandableRow, firstIllegalPropertyName, focusNewRow, isCaseOnlyPropertyName, isValidPropertyName, mountPropertyList, mountSettingsDialog, propertyNameProblem, propertyNameify, propertyRefProblem, revalidatePropertyRefs, revealRow, revealRowWhenReady } from './chunk-R3NXPHRZ.js';
|
|
5
5
|
import { isKeyCombo, tipWithKey } from './chunk-YWMSQFXX.js';
|
|
6
6
|
export { hintBar, isKeyCombo, keyHint, keyLabel, keyLegends, keyPlatform, setKeyPlatform, tipWithKey } from './chunk-YWMSQFXX.js';
|
|
7
|
-
export { kitTile, openKitGallery } from './chunk-
|
|
7
|
+
export { kitTile, openKitGallery } from './chunk-MS3BSTQB.js';
|
|
8
8
|
export { linkAddress, linkStatusClass, linkStatusTip, mountLinkStatus } from './chunk-G2JT3HZH.js';
|
|
9
9
|
import { isEditableTarget } from './chunk-P6WLAEKZ.js';
|
|
10
10
|
export { copyWithFeedback, debounce, formatCount, isEditableTarget, plural, relativeTime } from './chunk-P6WLAEKZ.js';
|
package/dist/kit-gallery.cjs
CHANGED
|
@@ -331,6 +331,7 @@ function openKitGallery(opts) {
|
|
|
331
331
|
fields.hidden = !details || nameInput === void 0 && opts.details === void 0;
|
|
332
332
|
setText(panelNote, details ? void 0 : section.note);
|
|
333
333
|
primary.textContent = section.action ?? "Create";
|
|
334
|
+
opts.onChoose?.(item.id);
|
|
334
335
|
};
|
|
335
336
|
const commit = () => {
|
|
336
337
|
const { item, section } = chosen;
|
|
@@ -344,6 +345,13 @@ function openKitGallery(opts) {
|
|
|
344
345
|
return;
|
|
345
346
|
}
|
|
346
347
|
}
|
|
348
|
+
const missing = opts.validate?.(item.id) ?? null;
|
|
349
|
+
if (missing) {
|
|
350
|
+
missing.classList.add("kit-gallery-missing");
|
|
351
|
+
missing.addEventListener("input", () => missing.classList.remove("kit-gallery-missing"), { once: true });
|
|
352
|
+
missing.focus();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
347
355
|
frame.close();
|
|
348
356
|
opts.onPick(item.id, name !== void 0 ? { name } : {});
|
|
349
357
|
};
|
package/dist/kit-gallery.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tooltip.ts","../src/dom.ts","../src/exit.ts","../src/dialog.ts","../src/kit-gallery.ts"],"names":["el","timer"],"mappings":"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","file":"kit-gallery.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// A single themed tooltip the family controls, replacing native `title`\n// rollovers: those are unstyled OS chrome and appear on the platform's slow\n// (~1s) delay, which is exactly the seam design-language section 4 (\"coherent\n// to the edges\") says not to leave showing.\n//\n// ONE floating bubble, document-DELEGATED: any element carrying a `data-tip`\n// attribute gets it, so a caller just sets `data-tip=\"...\"` anywhere and wires\n// nothing. The bubble anchors above the element (flipping below near the top\n// edge), uses our own snappy delay and the shared panel-enter motion.\n// `initTooltips()` is idempotent, so it is safe to call from more than one\n// place in an app that has several entry points.\n//\n// Lifted from Patterpad's patterpad-surface/web/tooltip.ts, which proved it,\n// with the one app-specific rule (suppressed in Writing View) generalised into\n// the `suppressed` option so the shell knows nothing about either app's modes.\n// ---------------------------------------------------------------------------\n\n// Bold markers: a producer wraps a span in these PRIVATE control chars (U+0002 STX / U+0003 ETX,\n// never present in user text) and the renderer turns it into a <strong>. Lets a data-tip bold part\n// of itself (a name, a \"VO:\" prefix) without any HTML, and each segment is still set via\n// textContent, so there is no injection risk from user content.\nconst B_OPEN = \"\\u0002\";\nconst B_CLOSE = \"\\u0003\";\n\n/** Wrap `s` so it renders bold inside a tooltip (see the marker note above). */\nexport function tipBold(s: string): string { return `${B_OPEN}${s}${B_CLOSE}`; }\n\n/** Render `text` into `host`, turning `\\u0002…\\u0003`-marked spans into <strong>; all set as text (safe). */\nfunction renderTip(host: HTMLElement, text: string): void {\n host.textContent = \"\";\n const re = /\\u0002([^\\u0003]*)\\u0003/g;\n let last = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n if (m.index > last) host.append(text.slice(last, m.index));\n const strong = document.createElement(\"strong\");\n strong.textContent = m[1] ?? \"\";\n host.append(strong);\n last = m.index + m[0].length;\n }\n if (last < text.length) host.append(text.slice(last));\n}\n\nexport interface TooltipOptions {\n /** Called before every show: return true to keep the bubble down. For a mode\n * that wants no chrome at all (Patterpad's Writing View). */\n suppressed?: () => boolean;\n}\n\nlet inited = false;\nlet checked = false; // the one-shot no-host warning below has been scheduled\nlet suppressed: (() => boolean) | undefined;\nconst SHOW_DELAY = 350; // ms - snappier than the OS title delay, slow enough not to flicker on pass-through\nconst EDGE = 6; // viewport inset so the bubble never touches the window edge\nconst GAP = 7; // px between the anchor and the bubble\n\nlet tip: HTMLDivElement | null = null;\nlet timer = 0;\nlet active: HTMLElement | null = null;\n/** The subject of a rect-anchored tip (see `tipAt`), when there is one. */\nlet activeKey: string | null = null;\n\nfunction ensureEl(): HTMLDivElement {\n if (tip) return tip;\n const el = document.createElement(\"div\");\n el.className = \"tooltip\";\n el.setAttribute(\"role\", \"tooltip\");\n el.hidden = true;\n document.body.appendChild(el);\n tip = el;\n return el;\n}\n\n/** A rectangle in VIEWPORT coordinates, for anchoring a tip to something that is\n * not a DOM element: a shape on a canvas, a run of text inside one. */\nexport interface TipRect { left: number; top: number; width: number; height: number }\n\nconst rectOf = (anchor: HTMLElement | TipRect): TipRect =>\n (anchor instanceof HTMLElement ? anchor.getBoundingClientRect() : anchor);\n\n/** Anchor the (already-visible, so measurable) bubble above the anchor, flipping below near the top. */\nfunction place(anchor: HTMLElement | TipRect): void {\n const t = ensureEl();\n const r = rectOf(anchor);\n const tr = t.getBoundingClientRect();\n let top = r.top - tr.height - GAP;\n if (top < EDGE) top = r.top + r.height + GAP; // not enough room above -> below\n let left = r.left + r.width / 2 - tr.width / 2;\n left = Math.max(EDGE, Math.min(left, window.innerWidth - tr.width - EDGE));\n t.style.left = `${Math.round(left)}px`;\n t.style.top = `${Math.round(top)}px`;\n}\n\nfunction show(anchor: HTMLElement): void {\n const text = anchor.dataset[\"tip\"];\n if (!text) return;\n paint(anchor, text);\n}\n\n/** Fill, place and reveal the bubble. The one path: both the delegated\n * `data-tip` route and `tipAt` end up here, so a canvas tip and a button tip\n * are the same bubble with the same look and the same motion. */\nfunction paint(anchor: HTMLElement | TipRect, text: string): void {\n if (suppressed?.()) return;\n const t = ensureEl();\n // A modal <dialog> (settings, a confirm) renders in the browser TOP LAYER, above every normal-flow\n // element whatever its z-index. If the anchor is inside one, move the bubble into that dialog so it\n // shares the top layer; otherwise keep it on <body>. Re-parenting per show also moves it back out\n // once the modal closes.\n const host: HTMLElement = (anchor instanceof HTMLElement ? anchor.closest<HTMLElement>(\"dialog[open]\") : null)\n ?? document.body;\n if (t.parentElement !== host) host.appendChild(t);\n active = anchor instanceof HTMLElement ? anchor : null;\n renderTip(t, text);\n t.hidden = false;\n t.classList.remove(\"show\"); void t.offsetWidth; // restart the enter animation\n place(anchor); // measure + position now it has content + is visible\n t.classList.add(\"show\");\n}\n\nfunction hide(): void {\n if (timer) { window.clearTimeout(timer); timer = 0; }\n active = null;\n activeKey = null;\n if (tip) { tip.hidden = true; tip.classList.remove(\"show\"); }\n}\n\nfunction schedule(anchor: HTMLElement): void {\n if (anchor === active) return;\n if (timer) window.clearTimeout(timer);\n timer = window.setTimeout(() => show(anchor), SHOW_DELAY);\n}\n\n/**\n * Show a tip for something that has no DOM node of its own: a card on a canvas,\n * a zone, a pin. The caller says WHERE (a viewport rectangle) and WHAT, and\n * identifies the subject with `key`.\n *\n * The key is what makes this usable from a canvas. Re-calling with the same key\n * re-places the bubble WITHOUT restarting the delay, so a tip can follow its\n * shape while the camera moves; calling with a different key starts the wait\n * again, so sweeping the pointer across a dense board does not strobe.\n *\n * The bubble is ordinary DOM, so it holds its size whatever the canvas is\n * zoomed to. That is the point of it: the label it stands in for is the one\n * thing on a canvas that must not shrink with everything else.\n */\nexport function tipAt(key: string, rect: TipRect, text: string): void {\n if (key === activeKey) {\n // Already up for this subject: move it, do not re-animate it.\n if (tip && !tip.hidden) { renderTip(tip, text); place(rect); }\n return;\n }\n // A different subject: down first, then wait again. Leaving the old bubble up\n // during the wait would put one card's name over another card, which is worse\n // than no name at all - and it matches the delegated route, where leaving an\n // element hides the tip before the next one is scheduled.\n hide();\n activeKey = key;\n timer = window.setTimeout(() => { timer = 0; paint(rect, text); }, SHOW_DELAY);\n}\n\n/** Take any tip down: the pointer has left, or a gesture has started. */\nexport function hideTip(): void { hide(); }\n\n/**\n * Mount the renderer because something is about to speak the grammar.\n *\n * The themed tooltip is not a feature a host turns on; it is the renderer for a grammar the shell's own\n * components already speak, and `data-tip` with nothing listening draws NOTHING - not the themed\n * bubble, not the platform one. That silence cost Patterpad two releases of a pin with no tooltip, and\n * cost it again when a `title` was converted to `data-tip` in a window that had never mounted this.\n *\n * So `el` calls this whenever it sets a tip. Mounting a delegated listener is the same class of act as\n * `confirmDialog` appending to `document.body`, which this package has always done.\n */\nexport function ensureTooltipHost(): void { initTooltips(); }\n\n/**\n * One deferred check that an app writing `data-tip` ITSELF has a host, warning if not.\n *\n * `ensureTooltipHost` covers anything built through `el`. It cannot cover a `data-tip` written straight\n * into markup or set by hand, which is the case that leaves a whole window of dead tips with no shell\n * component involved - so that one gets a warning rather than silence. Scheduled once, after the\n * current task, so it sees a rendered document rather than an empty one.\n */\nexport function checkTooltipHost(): void {\n if (checked) return;\n checked = true;\n setTimeout(() => {\n if (inited || typeof document === \"undefined\") return;\n if (document.querySelector(\"[data-tip]\")) {\n console.warn(\"app-shell: something set `data-tip` but no tooltip host is mounted, so those rollovers will not draw. Call initTooltips() once per window.\");\n }\n }, 0);\n}\n\nconst tipAncestor = (n: EventTarget | null): HTMLElement | null =>\n (n instanceof Element ? n.closest<HTMLElement>(\"[data-tip]\") : null);\n\n/**\n * Wire the one delegated controller. Mounting happens once; OPTIONS do not.\n *\n * An explicit call is always authoritative about its options, even after something else has already\n * mounted the listener. That ordering is not hypothetical, and Storyletter caught it before it shipped:\n * once components self-mount (see `ensureTooltipHost`), a host's own\n * `initTooltips({ suppressed })` could land second and be dropped by an early return, silently taking\n * a rule like \"no tooltips in Writing View\" with it. Nothing would fail and nothing would show.\n *\n * Components therefore call this with NO arguments: they mount the renderer without claiming the\n * options, and the host keeps the last word on behaviour.\n */\nexport function initTooltips(opts: TooltipOptions = {}): void {\n if (opts.suppressed !== undefined) suppressed = opts.suppressed; // an explicit call always wins\n if (inited) return;\n inited = true;\n\n document.addEventListener(\"pointerover\", (e) => {\n const el = tipAncestor(e.target);\n if (el) schedule(el);\n });\n document.addEventListener(\"pointerout\", (e) => {\n const el = tipAncestor(e.target);\n if (!el) return;\n const to = (e as PointerEvent).relatedTarget;\n if (to instanceof Node && el.contains(to)) return; // still inside the same anchor\n hide();\n });\n // Keyboard parity: reveal on focus, dismiss on blur.\n document.addEventListener(\"focusin\", (e) => { const el = tipAncestor(e.target); if (el) schedule(el); });\n document.addEventListener(\"focusout\", () => hide());\n // It's ANCHORED (not cursor-following), so anything that moves the layout or context dismisses it.\n document.addEventListener(\"pointerdown\", () => hide(), true);\n document.addEventListener(\"scroll\", () => { if (active) hide(); }, true);\n document.addEventListener(\"keydown\", (e) => { if (e.key === \"Escape\") hide(); });\n window.addEventListener(\"blur\", () => hide());\n}\n","// ---------------------------------------------------------------------------\n// `el`: the tiny tag-typed element factory both apps build their imperative,\n// idempotent views with.\n//\n// A SUPERSET of the two apps' prior signatures, so neither rewrites call sites:\n// - Patterpad: el(tag, cls?, text?) (positional)\n// - Storylet Studio: el(tag, { className, text, ... }, ...kids) (options bag)\n// A string second argument is the className; an object is the props bag; string\n// children become text nodes. So `el(\"div\", \"c\", \"t\")` and\n// `el(\"div\", { className: \"c\" }, \"t\")` produce identical DOM.\n// ---------------------------------------------------------------------------\n\nimport { ensureTooltipHost, checkTooltipHost } from \"./tooltip.js\";\nimport { iconNode, iconNameOfGlyph, isIconName, type IconName } from \"./icons.js\";\n\nexport type Child = Node | string | null | undefined;\n\nexport interface ElProps {\n className?: string;\n text?: string;\n /** A NATIVE rollover. Prefer `tip`: `title` is OS chrome on the platform's own\n * slow delay, which is the seam the themed tooltip exists to close. */\n title?: string;\n /** A themed rollover (`data-tip`, picked up by the delegated controller that\n * `initTooltips()` wires). Also the accessible name, since the elements that\n * want one are usually icon buttons with no text of their own. */\n tip?: string;\n onClick?: (event: MouseEvent) => void;\n}\n\nexport function el<K extends keyof HTMLElementTagNameMap>(\n tag: K,\n classNameOrProps?: string | ElProps,\n ...children: Child[]\n): HTMLElementTagNameMap[K] {\n const node = document.createElement(tag);\n const props: ElProps = typeof classNameOrProps === \"string\"\n ? { className: classNameOrProps }\n : classNameOrProps ?? {};\n if (props.className) node.className = props.className;\n if (props.text !== undefined && props.text !== null) node.textContent = props.text;\n if (props.title !== undefined) node.title = props.title;\n if (props.tip !== undefined) {\n node.dataset[\"tip\"] = props.tip;\n node.setAttribute(\"aria-label\", props.tip);\n // Setting a tip mounts the renderer. Without this, `data-tip` draws nothing at all in a window whose\n // host never called `initTooltips()`, which is a silent failure rather than a degraded one. No\n // options passed: the host stays authoritative about behaviour (see tooltip.ts).\n ensureTooltipHost();\n }\n if (props.onClick) node.addEventListener(\"click\", props.onClick as EventListener);\n for (const child of children) {\n if (child === null || child === undefined) continue;\n node.append(child);\n }\n checkTooltipHost(); // one-shot: warns if the APP wrote data-tip and nothing mounted a host\n return node;\n}\n\n/** A small square icon button (the move/delete controls list editors share).\n * Its rollover is the THEMED one (`data-tip`), so an app that has called\n * `initTooltips()` gets our bubble rather than the platform's.\n *\n * `name` is a word from the vocabulary (\"up\", \"close\"), drawn at 14px. A\n * typed glyph from the deprecated `icon` table (\"↑\", \"✕\") is accepted for\n * now and drawn as the word it stood for, so an app that has not moved its\n * call sites yet still gets the drawn set; any other string is set as text,\n * which is the old behaviour and on its way out. */\nexport function iconBtn(name: IconName | (string & {}), title: string, onClick: () => void, disabled = false, danger = false): HTMLButtonElement {\n const b = el(\"button\", `shell-icon${danger ? \" danger\" : \"\"}`);\n b.type = \"button\"; b.dataset[\"tip\"] = title; b.setAttribute(\"aria-label\", title);\n const word = isIconName(name) ? name : iconNameOfGlyph(name);\n if (word) b.append(iconNode(word)); else b.textContent = name;\n b.disabled = disabled;\n b.addEventListener(\"click\", onClick);\n return b;\n}\n\n/**\n * A captioned field: `<label class=\"shell-labelled\"><span class=\"shell-fieldcap\">…</span>control</label>`.\n *\n * The label is pointed at the field it captions, EXPLICITLY, and never at a button. A `<label>` with no\n * `for` forwards a click to its first labelable descendant, and buttons are labelable - so a caption\n * wrapped around a control containing buttons (`tagChips`, whose every chip carries a remove button)\n * turns every click on the row's dead space into a press of the FIRST button in it. That shipped:\n * patterkit/patter#44, where clicking a Game Data list value anywhere but its ✕ deleted the first value\n * in the list. Reported as \"clicking a value removes the wrong one\", which is not a wrong-index bug at\n * all - the click never reached the chip.\n *\n * When the control holds no labelable field (a chips editor with the add input removed, say), this is a\n * plain `<div>`: no caption behaviour is better than a caption that presses something.\n */\nexport function labelled(label: string, control: HTMLElement): HTMLElement {\n const target = control.matches(\"input, select, textarea\")\n ? control\n : control.querySelector<HTMLElement>(\"input:not([type=button]):not([type=submit]), select, textarea\");\n const w = el(target ? \"label\" : \"div\", \"shell-labelled\");\n w.append(el(\"span\", \"shell-fieldcap\", label), control);\n if (target) {\n if (!target.id) target.id = `shell-f-${Math.random().toString(36).slice(2, 9)}`;\n (w as HTMLLabelElement).htmlFor = target.id;\n }\n return w;\n}\n\n/** Swap item `i` with its neighbour `i + delta` IN PLACE (the up/down reorder list editors share);\n * a no-op when the target is out of range. Returns whether anything moved. */\nexport function moveItem<T>(arr: T[], i: number, delta: number): boolean {\n const j = i + delta;\n if (j < 0 || j >= arr.length) return false;\n [arr[i], arr[j]] = [arr[j]!, arr[i]!];\n return true;\n}\n\n/** The icon size inside a chip's remove / move buttons: the chip is set at\n * 0.76rem, so its controls draw smaller than a toolbar button's 14px. */\nconst CHIP_ICON = 10;\n\n/** A tag-style editor for a string-list field (an enum's allowed values / flags): removable chips + an\n * add input (Enter or \",\" commits; blank / duplicate ignored). Mutates `holder.values` in place (read\n * back on save). `onChange` fires after any add / remove so callers can refresh a dependent control. */\nexport function tagChips(holder: { values?: string[] }, onChange?: () => void): HTMLElement {\n const wrap = el(\"div\", \"shell-tags\");\n const input = el(\"input\", \"shell-tag-input\");\n // Sentence-cased like every other hint (the microcopy ruling, 2026-08-28):\n // lowercase fragments read as adrift, and a placeholder is a hint speaking.\n input.type = \"text\"; input.placeholder = \"Add value\"; input.spellcheck = false;\n const makeChip = (v: string): HTMLElement => {\n const chip = el(\"span\", \"shell-tag\", v);\n const x = el(\"button\", \"shell-tag-x\", iconNode(\"close\", CHIP_ICON));\n x.type = \"button\"; x.dataset[\"tip\"] = `Remove ${v}`; x.setAttribute(\"aria-label\", `Remove ${v}`);\n x.addEventListener(\"click\", () => { holder.values = (holder.values ?? []).filter((o) => o !== v); chip.remove(); onChange?.(); });\n chip.append(x);\n return chip;\n };\n const commit = (): void => {\n const v = input.value.trim();\n if (v && !(holder.values ?? []).includes(v)) { (holder.values ??= []).push(v); wrap.insertBefore(makeChip(v), input); onChange?.(); }\n input.value = \"\";\n };\n input.addEventListener(\"keydown\", (e) => { if (e.key === \"Enter\" || e.key === \",\") { e.preventDefault(); commit(); } });\n input.addEventListener(\"blur\", commit);\n for (const v of holder.values ?? []) wrap.append(makeChip(v));\n wrap.append(input);\n return wrap;\n}\n\n/**\n * An ORDERED chips editor for a string-list field whose order means something\n * (a quality's stages: `advance()` walks them, so 2 comes after 1): numbered\n * chips, each with move-earlier / move-later / remove, plus an add input\n * (Enter or \",\" commits; blank / duplicate ignored). Mutates `holder.stages`\n * in place; `onChange` fires after any add / move / remove.\n *\n * Patterpad's, lifted (ui-review-2026-09, finding 3); Storyletter had been\n * faking it with `tagChips` over a `values` holder, which loses the order\n * controls. Same class family as `tagChips` (`.shell-tags`), so it is styled by\n * settings.css already.\n */\nexport function stageChips(holder: { stages?: string[] }, onChange?: () => void): HTMLElement {\n const wrap = el(\"div\", \"shell-tags shell-stages\");\n const input = el(\"input\", \"shell-tag-input\");\n input.type = \"text\"; input.placeholder = \"Add stage\"; input.spellcheck = false;\n const control = (name: IconName, tip: string, disabled: boolean, onClick: () => void): HTMLButtonElement => {\n const b = el(\"button\", \"shell-tag-x shell-tag-move\", iconNode(name, CHIP_ICON));\n b.type = \"button\"; b.dataset[\"tip\"] = tip; b.setAttribute(\"aria-label\", tip);\n b.disabled = disabled;\n b.addEventListener(\"click\", onClick);\n return b;\n };\n const rebuild = (): void => {\n for (const c of Array.from(wrap.children)) if (c !== input) c.remove();\n const stages = holder.stages ?? [];\n stages.forEach((v, i) => {\n const chip = el(\"span\", \"shell-tag\", `${i + 1}. ${v}`);\n chip.dataset[\"stage\"] = v;\n chip.append(\n control(\"back\", `Move ${v} earlier`, i === 0,\n () => { if (moveItem(stages, i, -1)) { rebuild(); onChange?.(); } }),\n control(\"forward\", `Move ${v} later`, i === stages.length - 1,\n () => { if (moveItem(stages, i, 1)) { rebuild(); onChange?.(); } }),\n );\n const x = el(\"button\", \"shell-tag-x\", iconNode(\"close\", CHIP_ICON));\n x.type = \"button\"; x.dataset[\"tip\"] = `Remove ${v}`; x.setAttribute(\"aria-label\", `Remove ${v}`);\n x.addEventListener(\"click\", () => { holder.stages = (holder.stages ?? []).filter((o) => o !== v); rebuild(); onChange?.(); });\n chip.append(x);\n wrap.insertBefore(chip, input);\n });\n };\n const commit = (): void => {\n const v = input.value.trim();\n if (v && !(holder.stages ?? []).includes(v)) { (holder.stages ??= []).push(v); rebuild(); onChange?.(); }\n input.value = \"\";\n };\n input.addEventListener(\"keydown\", (e) => { if (e.key === \"Enter\" || e.key === \",\") { e.preventDefault(); commit(); } });\n input.addEventListener(\"blur\", commit);\n wrap.append(input);\n rebuild();\n return wrap;\n}\n\n// --- drag-to-reorder ---------------------------------------------------------\n// One drag at a time per window, shared across every wired list, so the\n// midpoint test knows what is being carried without a dataTransfer round trip.\nlet reorderDragId: string | null = null;\nconst clearDropMarks = (host: ParentNode): void =>\n host.querySelectorAll(\".drop-before, .drop-after\").forEach((e) => e.classList.remove(\"drop-before\", \"drop-after\"));\n\n/**\n * Make `el` a drag source and a drop target for HTML5 drag-to-reorder among\n * its siblings. While carried it wears `.dragging`; a hovered target wears\n * `.drop-before` or `.drop-after` by the midpoint test on `axis` (\"y\" for\n * rows, \"x\" for a wrapping grid); a drop calls `onMove(draggedId, before,\n * targetId)` and the caller reorders its model and re-renders.\n *\n * Storyletter's `wireCardDrag`, lifted (ui-review-2026-09, finding 30);\n * Patterpad's `wireSceneDrag` is the same idea fixed on \"y\". The marks are\n * classes only: the app draws them (Patterpad's `box-shadow: 0 -2px 0 0\n * var(--accent)` pair is the family's look).\n */\nexport function wireReorder(el: HTMLElement, id: string, axis: \"x\" | \"y\", onMove: (id: string, before: boolean, targetId: string) => void): void {\n el.draggable = true;\n el.dataset[\"reorderId\"] = id;\n const host = (): ParentNode => el.parentElement ?? el;\n el.addEventListener(\"dragstart\", (e) => {\n reorderDragId = id; el.classList.add(\"dragging\");\n if (e.dataTransfer) { e.dataTransfer.effectAllowed = \"move\"; e.dataTransfer.setData(\"text/plain\", id); }\n });\n el.addEventListener(\"dragend\", () => { reorderDragId = null; el.classList.remove(\"dragging\"); clearDropMarks(host()); });\n el.addEventListener(\"dragover\", (e) => {\n if (!reorderDragId || reorderDragId === id) return;\n e.preventDefault();\n if (e.dataTransfer) e.dataTransfer.dropEffect = \"move\";\n const r = el.getBoundingClientRect();\n const before = axis === \"y\" ? e.clientY < r.top + r.height / 2 : e.clientX < r.left + r.width / 2;\n clearDropMarks(host());\n el.classList.add(before ? \"drop-before\" : \"drop-after\");\n });\n el.addEventListener(\"dragleave\", () => el.classList.remove(\"drop-before\", \"drop-after\"));\n el.addEventListener(\"drop\", (e) => {\n e.preventDefault();\n const before = el.classList.contains(\"drop-before\");\n clearDropMarks(host());\n if (reorderDragId && reorderDragId !== id) onMove(reorderDragId, before, id);\n });\n}\n\n// --- metadata and breadcrumbs ---------------------------------------------------\n// \"Icons are drawn, and so are separators\" (design-language.md section 4,\n// 2026-09-16): metadata is never joined with \"·\" and a breadcrumb is never a\n// string with \"›\" in it. Both apps had been building exactly those strings\n// (ui-review-2026-09, marker A); these two helpers make the same lines as DOM,\n// with the separator drawn by CSS (controls.css) or by the icon vocabulary.\n\n/**\n * A metadata line: `parts` as spans in a flex row, separated by a drawn\n * 3px disc (a `::before` on every part after the first, in controls.css),\n * so `metaLine([\"6 runs\", \"200 max steps\", \"seed 4\"])` is what\n * \"6 runs · 200 max steps · seed 4\" used to be, without the typed dot.\n * Empty, null and undefined parts are skipped, so a caller can pass an\n * optional segment without branching.\n */\nexport function metaLine(parts: (string | Node | null | undefined)[]): HTMLElement {\n const row = el(\"span\", \"shell-meta\");\n for (const part of parts) {\n if (part === null || part === undefined || part === \"\") continue;\n const seg = el(\"span\", \"shell-meta-part\");\n seg.append(part);\n row.append(seg);\n }\n return row;\n}\n\nexport interface Crumb {\n label: string;\n /** Makes the crumb a button. Ignored on the last crumb, which is where the\n * reader already is. */\n onClick?: (event: MouseEvent) => void;\n}\n\n/**\n * A breadcrumb trail: each crumb a span, or a button when it carries\n * `onClick` and is not the last, with `iconNode(\"forward\", 12)` drawn between\n * them. `breadcrumb([\"Box\", \"Deck\", \"Card\"])` is what \"Box › Deck › Card\"\n * used to be; the chevrons are the family's own drawing, not a symbol-font\n * character.\n */\nexport function breadcrumb(parts: (string | Crumb)[]): HTMLElement {\n const trail = el(\"nav\", \"shell-crumbs\");\n trail.setAttribute(\"aria-label\", \"Where this is\");\n parts.forEach((part, i) => {\n const crumb: Crumb = typeof part === \"string\" ? { label: part } : part;\n const last = i === parts.length - 1;\n if (i > 0) {\n const sep = iconNode(\"forward\", 12);\n sep.classList.add(\"shell-crumb-sep\");\n trail.append(sep);\n }\n if (crumb.onClick && !last) {\n const b = el(\"button\", { className: \"shell-crumb shell-crumb-link\", text: crumb.label, onClick: crumb.onClick });\n b.type = \"button\";\n trail.append(b);\n } else {\n const s = el(\"span\", \"shell-crumb\", crumb.label);\n if (last) s.setAttribute(\"aria-current\", \"location\");\n trail.append(s);\n }\n });\n return trail;\n}\n","// ---------------------------------------------------------------------------\n// The exit half of the panel motion vocabulary.\n//\n// Enters are pure CSS: they run on insertion. Exits cannot be, because the node\n// has to OUTLIVE the close for the animation to play and then be torn down. So\n// every closing surface routes its teardown through here instead of removing\n// itself directly.\n//\n// Patterpad's `closeWithExit`, lifted. It was already app-agnostic, and the\n// shell had grown a second copy of the same twelve lines inside `confirm.ts`,\n// which is the drift this package exists to stop: one motion vocabulary, drawn\n// one way. `tokens.css` already carried the keyframes and the durations; this\n// is the missing half that plays them.\n// ---------------------------------------------------------------------------\n\n/**\n * Play the close animation on `element`, then run `done` exactly once.\n *\n * `done` does the real teardown: remove the node, drop listeners, unmount\n * whatever was inside it. Under `prefers-reduced-motion` the duration reads 0\n * and `done` runs synchronously, so reduced motion means instant rather than\n * an invisible wait.\n */\nexport function closeWithExit(element: HTMLElement, done: () => void): void {\n element.classList.add(\"closing\");\n // getComputedStyle flushes pending style, so this reflects the `.closing`\n // rule we just added rather than the state before it.\n const seconds = parseFloat(getComputedStyle(element).animationDuration) || 0;\n if (seconds === 0) { done(); return; }\n\n let fired = false;\n const finish = (): void => {\n if (fired) return;\n fired = true;\n element.removeEventListener(\"animationend\", onEnd);\n clearTimeout(timer);\n done();\n };\n // Only OUR animation ending counts. Content inside the panel (rows, pills,\n // spinners) raises its own animationend, and an inner one firing first would\n // tear the panel down mid-fade.\n const onEnd = (e: AnimationEvent): void => { if (e.target === element) finish(); };\n element.addEventListener(\"animationend\", onEnd);\n // Fallback: animationend is missed if the element is hidden or the animation\n // is interrupted, and a panel that never tears down is worse than an abrupt one.\n const timer = setTimeout(finish, seconds * 1000 + 120);\n}\n","// ---------------------------------------------------------------------------\n// dialogFrame: the one modal skeleton every dialog in the family sits on.\n//\n// Title, an optional sub line, a body the caller fills, an actions row the\n// caller fills, on a native <dialog> so the focus trap and Escape come from the\n// platform. Four scrims and three panel drawings had grown across five shell\n// files and a dozen app dialogs (ui-review-2026-09, findings 9 and 30); this is\n// the frame they all sit on now, and the shell's own dialogs (confirm, identity,\n// about, notes) were moved onto it first, which is how it is proven.\n//\n// What the frame owns: the backdrop, the panel, the enter and exit motion\n// (closeWithExit), Escape, and giving focus back to whatever had it when the\n// dialog opened. What it does not own: the content classes, the buttons'\n// meaning, and when to close. The caller keeps those.\n//\n// Styles ship as dialog.css. The action buttons are expected to wear `.btn`\n// from controls.css; the frame does not draw buttons of its own.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\nimport { closeWithExit } from \"./exit.js\";\n\nexport interface DialogFrameOptions {\n /** The heading. Also the dialog's accessible name. */\n title: string;\n /** One plain sentence under the heading, if the title needs one. */\n sub?: string;\n /** Extra class(es) on the <dialog>, for the caller's own content rules and\n * width (`--dialog-width` on the class; see dialog.css). */\n className?: string;\n /** Runs once, after the dialog has left the document, however it closed. */\n onClose?: () => void;\n}\n\nexport interface DialogFrame {\n readonly dialog: HTMLDialogElement;\n /** Fill this with the content. */\n readonly body: HTMLElement;\n /** Fill this with the buttons, right-aligned; empty, it draws nothing. */\n readonly actions: HTMLElement;\n /** Show modally. Records what had focus so `close()` can give it back. */\n open(): void;\n /** Play the exit, tear down, restore focus. Idempotent. */\n close(): void;\n isOpen(): boolean;\n}\n\n/** Build a dialog on the family's frame. Appended to <body> at once (so an\n * `open()` on the next line has nothing to wait for); shown only on `open()`. */\nexport function dialogFrame(opts: DialogFrameOptions): DialogFrame {\n const dialog = el(\"dialog\", `shell-dialog ${opts.className ?? \"\"}`.trim());\n const body = el(\"div\", \"shell-dialog-body\");\n const actions = el(\"div\", \"shell-dialog-actions\");\n dialog.append(el(\"h2\", \"shell-dialog-title\", opts.title));\n if (opts.sub !== undefined) dialog.append(el(\"p\", \"shell-dialog-sub\", opts.sub));\n dialog.append(body, actions);\n dialog.setAttribute(\"aria-label\", opts.title);\n document.body.append(dialog);\n\n let state: \"closed\" | \"open\" | \"closing\" = \"closed\";\n let opener: Element | null = null;\n\n const teardown = (): void => {\n dialog.remove();\n // Back to the control that opened this. The platform does it for a plain\n // dialog.close(), but the exit motion ends in a remove(), and a removed\n // dialog restores nothing.\n if (opener instanceof HTMLElement && opener.isConnected) opener.focus();\n opener = null;\n state = \"closed\";\n opts.onClose?.();\n };\n\n const close = (): void => {\n if (state !== \"open\") return;\n state = \"closing\";\n closeWithExit(dialog, () => { if (dialog.open) dialog.close(); teardown(); });\n };\n\n // Escape arrives as the cancel event; route it through the exit motion.\n dialog.addEventListener(\"cancel\", (e) => { e.preventDefault(); close(); });\n // Closed by someone else (a form with method=\"dialog\", or a direct\n // dialog.close()): no motion to play, tear down at once.\n dialog.addEventListener(\"close\", () => { if (state === \"open\") teardown(); });\n\n return {\n dialog, body, actions,\n open() {\n if (state !== \"closed\") return;\n opener = document.activeElement;\n state = \"open\";\n dialog.showModal();\n },\n close,\n isOpen: () => state === \"open\",\n };\n}\n","// ---------------------------------------------------------------------------\n// The kit gallery: pick a starting point by what you want to make.\n//\n// The new-thing moment for the family (New Project, New Box, New Scene), drawn\n// once. Adobe's New Document is the shape: tiles on the left, a details panel\n// on the right, one screen. Not a wizard: a wizard asks for settings before\n// anything has been chosen, and the choice is the part that teaches.\n//\n// Storyletter grew this as `kit-picker.ts` (one component at two scales on\n// <dialog>, \"a shell candidate, held back until it had been used in earnest\")\n// and the kit gallery brief moved it here, 2026-09-27\n// (storylet-studio/design/kit-gallery.md). What a kit IS made of stays in\n// each app: this file knows names, words and pictures, never boxes or scenes.\n//\n// Sections are shelves (\"Start from a kit\", \"Learn from a finished project\"),\n// and each says what its button does, because opening a copy of an example\n// and creating from a kit are different acts behind the same kind of tile.\n// The app's own fields (a project name, version control, a build path) sit in\n// the details panel, shown only for the sections that use them.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\nimport { dialogFrame } from \"./dialog.js\";\n\nexport interface KitGalleryItem<T extends string> {\n id: T;\n /** What you are making, in the author's terms, not the model's. */\n name: string;\n /** What it is FOR: the panel's first line, and the tile's unless `tile` says otherwise. */\n blurb: string;\n /** The tile's line, when the blurb is longer than a tile holds (three\n * lines). A whole short sentence reads better than a long one cut off. */\n tile?: string;\n /** More, for the panel only. */\n detail?: string;\n /** What pressing Play (or Run) shows, said as a promise. */\n play?: string;\n /** The model's parts it puts to work, second and quieter (\"Shows: ...\"). */\n shows?: string;\n /** What lands, in the concrete, as a short list in the panel. */\n lands?: string[];\n /** A word or two on the tile (\"Start here\"). */\n badge?: string;\n /** An illustration: a transparent image whose opaque pixels are the ink.\n * Drawn as a mask in `--kit-art-ink` on `--kit-art-plate`, so one file\n * serves both themes and the app chooses the colours. */\n image?: string;\n}\n\nexport interface KitGallerySection<T extends string> {\n /** Sentence-case label text (\"Start from a kit\"). Absent: no caption. */\n caption?: string;\n /** The consequence of a click, said before the click. */\n note?: string;\n items: KitGalleryItem<T>[];\n /** The primary button's label while an item here is chosen. Default \"Create\". */\n action?: string;\n /** Show the app's detail fields (and ask for the name) for this section.\n * Default true. An example is opened, not named, so its shelf says false. */\n usesDetails?: boolean;\n}\n\nexport interface KitGalleryChoice {\n /** Present when a name was asked for and the chosen section uses details. */\n name?: string;\n}\n\nexport interface KitGalleryOptions<T extends string> {\n /** The dialog's heading: \"New project\", \"New box\", \"New scene\". */\n title: string;\n /** What the thing being made IS, in concrete terms. */\n what?: string;\n sections: KitGallerySection<T>[];\n /** Ask for a name in the details panel, with this placeholder. Required\n * when asked: making a thing without one would have to invent a folder. */\n namePlaceholder?: string;\n /** The name field's label. Default \"Name\". */\n nameLabel?: string;\n /** The app's own fields, below the name (version control, a build path). */\n details?: HTMLElement;\n /** Runs as the name is typed (and once on open, with \"\"), so the app's own\n * fields can follow it: Patterpad's folder preview and default publish path. */\n onNameInput?: (name: string) => void;\n /** The item chosen when the gallery opens. Default: the first. */\n initial?: T;\n onPick: (id: T, choice: KitGalleryChoice) => void;\n /** Runs once the gallery has closed, whichever way. */\n onClose?: () => void;\n}\n\nexport interface KitGallery {\n readonly dialog: HTMLDialogElement;\n close(): void;\n}\n\n/** Draw `image` into an art box as a mask (see kit-gallery.css), or hide the box. */\nfunction setArt(host: HTMLElement, image: string | undefined): void {\n host.replaceChildren();\n host.hidden = image === undefined;\n if (image === undefined) return;\n const ink = el(\"span\", \"kit-gallery-ink\");\n ink.style.setProperty(\"--kit-art\", `url(\"${image.replace(/\"/g, \"%22\")}\")`);\n host.append(ink);\n}\n\n/** One tile: the picture, the name with its badge, the blurb. The gallery's\n * own, and the welcome screen's, so a kit looks the same wherever it is met\n * (welcome.ts draws a tiled group with it; import kit-gallery.css there too). */\nexport function kitTile(item: { name: string; blurb: string; badge?: string; image?: string }, onClick: () => void): HTMLButtonElement {\n const art = el(\"div\", \"kit-gallery-art\");\n setArt(art, item.image);\n const tile = el(\"button\", { className: \"kit-gallery-tile\", onClick },\n art,\n el(\"span\", \"kit-gallery-tile-head\",\n el(\"span\", \"kit-gallery-tile-name\", item.name),\n item.badge !== undefined ? el(\"span\", \"kit-gallery-badge\", item.badge) : null),\n el(\"span\", \"kit-gallery-tile-blurb\", item.blurb)) as HTMLButtonElement;\n tile.type = \"button\";\n return tile;\n}\n\n/** Open the gallery as a modal dialog on the family's frame. */\nexport function openKitGallery<T extends string>(opts: KitGalleryOptions<T>): KitGallery {\n const frame = dialogFrame({ title: opts.title, ...(opts.what !== undefined ? { sub: opts.what } : {}), className: \"kit-gallery\", ...(opts.onClose ? { onClose: opts.onClose } : {}) });\n const { dialog } = frame;\n\n const entries = opts.sections.flatMap((section, s) => section.items.map((item) => ({ item, section, s })));\n if (entries.length === 0) throw new Error(\"openKitGallery: no items to choose from\");\n let chosen = entries.find((e) => e.item.id === opts.initial) ?? entries[0]!;\n\n // --- the details panel ----------------------------------------------------\n const nameInput = opts.namePlaceholder === undefined ? undefined : el(\"input\", { className: \"field kit-gallery-name\" }) as HTMLInputElement;\n if (nameInput) {\n nameInput.placeholder = opts.namePlaceholder!;\n nameInput.setAttribute(\"aria-label\", opts.nameLabel ?? \"Name\");\n }\n const fields = el(\"div\", \"kit-gallery-fields\",\n ...(nameInput ? [el(\"label\", \"kit-gallery-label\", opts.nameLabel ?? \"Name\"), nameInput] : []),\n ...(opts.details ? [opts.details] : []));\n\n const panelArt = el(\"div\", \"kit-gallery-art kit-gallery-panel-art\");\n const panelName = el(\"h3\", \"kit-gallery-panel-name\");\n const panelBlurb = el(\"p\", \"kit-gallery-panel-blurb\");\n const panelDetail = el(\"p\", \"kit-gallery-panel-detail\");\n const panelPlay = el(\"p\", \"kit-gallery-play\");\n const panelShows = el(\"p\", \"kit-gallery-shows\");\n const panelLands = el(\"ul\", \"kit-gallery-lands\");\n const panelLandsCaption = el(\"p\", \"kit-gallery-lands-caption\", \"What you get\");\n const panelNote = el(\"p\", \"kit-gallery-note\");\n const panel = el(\"div\", \"kit-gallery-panel\",\n panelArt, panelName, panelBlurb, panelDetail, panelPlay, panelShows, panelLandsCaption, panelLands, panelNote, fields);\n\n const primary = el(\"button\", { className: \"btn primary\", onClick: () => commit() }) as HTMLButtonElement;\n primary.type = \"button\";\n const cancel = el(\"button\", { className: \"btn\", text: \"Cancel\", onClick: () => frame.close() }) as HTMLButtonElement;\n cancel.type = \"button\";\n\n const setText = (node: HTMLElement, text: string | undefined): void => {\n node.textContent = text ?? \"\";\n node.hidden = text === undefined || text === \"\";\n };\n\n // --- the tiles --------------------------------------------------------------\n const tiles = new Map<T, HTMLButtonElement>();\n const shelves = opts.sections.map((section) => {\n const grid = el(\"div\", \"kit-gallery-grid\", ...section.items.map((item) => {\n const tile = kitTile({ ...item, blurb: item.tile ?? item.blurb }, () => choose(item.id));\n tile.dataset.kit = item.id;\n // Double-click is the desktop's \"choose and go\"; a single click only chooses,\n // so a tile can be read in the panel before anything is made.\n tile.addEventListener(\"dblclick\", () => { choose(item.id); commit(); });\n tiles.set(item.id, tile);\n return tile;\n }));\n return el(\"section\", \"kit-gallery-shelf\",\n section.caption !== undefined ? el(\"h3\", \"kit-gallery-caption\", section.caption) : null,\n section.note !== undefined ? el(\"p\", \"kit-gallery-shelf-note\", section.note) : null,\n grid);\n });\n\n const choose = (id: T): void => {\n const next = entries.find((e) => e.item.id === id);\n if (!next) return;\n chosen = next;\n for (const [tid, tile] of tiles) tile.setAttribute(\"aria-pressed\", String(tid === id));\n const { item, section } = chosen;\n setArt(panelArt, item.image);\n panelName.textContent = item.name;\n setText(panelBlurb, item.blurb);\n setText(panelDetail, item.detail);\n setText(panelPlay, item.play);\n setText(panelShows, item.shows !== undefined ? `Shows: ${item.shows}` : undefined);\n panelLands.replaceChildren(...(item.lands ?? []).map((l) => el(\"li\", undefined, l)));\n panelLands.hidden = (item.lands ?? []).length === 0;\n panelLandsCaption.hidden = panelLands.hidden;\n const details = section.usesDetails !== false;\n fields.hidden = !details || (nameInput === undefined && opts.details === undefined);\n // Without the fields, the shelf's note is what the click will do next\n // (\"a folder you choose\"), so the panel repeats it beside the button.\n setText(panelNote, details ? undefined : section.note);\n primary.textContent = section.action ?? \"Create\";\n };\n\n const commit = (): void => {\n const { item, section } = chosen;\n const details = section.usesDetails !== false;\n let name: string | undefined;\n if (details && nameInput) {\n name = nameInput.value.trim();\n if (name === \"\") { nameInput.focus(); nameInput.classList.add(\"kit-gallery-missing\"); return; }\n }\n frame.close();\n opts.onPick(item.id, name !== undefined ? { name } : {});\n };\n\n if (nameInput) {\n nameInput.addEventListener(\"input\", () => {\n nameInput.classList.remove(\"kit-gallery-missing\");\n opts.onNameInput?.(nameInput.value);\n });\n opts.onNameInput?.(\"\");\n // Enter creates with what is chosen: the author has said what it should be\n // called, and the tile they are looking at is the one they meant.\n nameInput.addEventListener(\"keydown\", (e) => {\n e.stopPropagation();\n if (e.key !== \"Enter\") return;\n e.preventDefault();\n commit();\n });\n }\n\n frame.body.append(el(\"div\", \"kit-gallery-layout\", el(\"div\", \"kit-gallery-shelves\", ...shelves), panel));\n frame.actions.append(cancel, primary);\n dialog.addEventListener(\"click\", (e) => { if (e.target === dialog) frame.close(); });\n\n choose(chosen.item.id);\n frame.open();\n // Focus where the next keystroke is wanted: the name when one is asked for\n // and the opening shelf uses it, otherwise the chosen tile.\n if (nameInput && chosen.section.usesDetails !== false) nameInput.focus();\n else tiles.get(chosen.item.id)?.focus();\n\n return { dialog, close: () => frame.close() };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/tooltip.ts","../src/dom.ts","../src/exit.ts","../src/dialog.ts","../src/kit-gallery.ts"],"names":["el","timer"],"mappings":"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","file":"kit-gallery.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// A single themed tooltip the family controls, replacing native `title`\n// rollovers: those are unstyled OS chrome and appear on the platform's slow\n// (~1s) delay, which is exactly the seam design-language section 4 (\"coherent\n// to the edges\") says not to leave showing.\n//\n// ONE floating bubble, document-DELEGATED: any element carrying a `data-tip`\n// attribute gets it, so a caller just sets `data-tip=\"...\"` anywhere and wires\n// nothing. The bubble anchors above the element (flipping below near the top\n// edge), uses our own snappy delay and the shared panel-enter motion.\n// `initTooltips()` is idempotent, so it is safe to call from more than one\n// place in an app that has several entry points.\n//\n// Lifted from Patterpad's patterpad-surface/web/tooltip.ts, which proved it,\n// with the one app-specific rule (suppressed in Writing View) generalised into\n// the `suppressed` option so the shell knows nothing about either app's modes.\n// ---------------------------------------------------------------------------\n\n// Bold markers: a producer wraps a span in these PRIVATE control chars (U+0002 STX / U+0003 ETX,\n// never present in user text) and the renderer turns it into a <strong>. Lets a data-tip bold part\n// of itself (a name, a \"VO:\" prefix) without any HTML, and each segment is still set via\n// textContent, so there is no injection risk from user content.\nconst B_OPEN = \"\\u0002\";\nconst B_CLOSE = \"\\u0003\";\n\n/** Wrap `s` so it renders bold inside a tooltip (see the marker note above). */\nexport function tipBold(s: string): string { return `${B_OPEN}${s}${B_CLOSE}`; }\n\n/** Render `text` into `host`, turning `\\u0002…\\u0003`-marked spans into <strong>; all set as text (safe). */\nfunction renderTip(host: HTMLElement, text: string): void {\n host.textContent = \"\";\n const re = /\\u0002([^\\u0003]*)\\u0003/g;\n let last = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n if (m.index > last) host.append(text.slice(last, m.index));\n const strong = document.createElement(\"strong\");\n strong.textContent = m[1] ?? \"\";\n host.append(strong);\n last = m.index + m[0].length;\n }\n if (last < text.length) host.append(text.slice(last));\n}\n\nexport interface TooltipOptions {\n /** Called before every show: return true to keep the bubble down. For a mode\n * that wants no chrome at all (Patterpad's Writing View). */\n suppressed?: () => boolean;\n}\n\nlet inited = false;\nlet checked = false; // the one-shot no-host warning below has been scheduled\nlet suppressed: (() => boolean) | undefined;\nconst SHOW_DELAY = 350; // ms - snappier than the OS title delay, slow enough not to flicker on pass-through\nconst EDGE = 6; // viewport inset so the bubble never touches the window edge\nconst GAP = 7; // px between the anchor and the bubble\n\nlet tip: HTMLDivElement | null = null;\nlet timer = 0;\nlet active: HTMLElement | null = null;\n/** The subject of a rect-anchored tip (see `tipAt`), when there is one. */\nlet activeKey: string | null = null;\n\nfunction ensureEl(): HTMLDivElement {\n if (tip) return tip;\n const el = document.createElement(\"div\");\n el.className = \"tooltip\";\n el.setAttribute(\"role\", \"tooltip\");\n el.hidden = true;\n document.body.appendChild(el);\n tip = el;\n return el;\n}\n\n/** A rectangle in VIEWPORT coordinates, for anchoring a tip to something that is\n * not a DOM element: a shape on a canvas, a run of text inside one. */\nexport interface TipRect { left: number; top: number; width: number; height: number }\n\nconst rectOf = (anchor: HTMLElement | TipRect): TipRect =>\n (anchor instanceof HTMLElement ? anchor.getBoundingClientRect() : anchor);\n\n/** Anchor the (already-visible, so measurable) bubble above the anchor, flipping below near the top. */\nfunction place(anchor: HTMLElement | TipRect): void {\n const t = ensureEl();\n const r = rectOf(anchor);\n const tr = t.getBoundingClientRect();\n let top = r.top - tr.height - GAP;\n if (top < EDGE) top = r.top + r.height + GAP; // not enough room above -> below\n let left = r.left + r.width / 2 - tr.width / 2;\n left = Math.max(EDGE, Math.min(left, window.innerWidth - tr.width - EDGE));\n t.style.left = `${Math.round(left)}px`;\n t.style.top = `${Math.round(top)}px`;\n}\n\nfunction show(anchor: HTMLElement): void {\n const text = anchor.dataset[\"tip\"];\n if (!text) return;\n paint(anchor, text);\n}\n\n/** Fill, place and reveal the bubble. The one path: both the delegated\n * `data-tip` route and `tipAt` end up here, so a canvas tip and a button tip\n * are the same bubble with the same look and the same motion. */\nfunction paint(anchor: HTMLElement | TipRect, text: string): void {\n if (suppressed?.()) return;\n const t = ensureEl();\n // A modal <dialog> (settings, a confirm) renders in the browser TOP LAYER, above every normal-flow\n // element whatever its z-index. If the anchor is inside one, move the bubble into that dialog so it\n // shares the top layer; otherwise keep it on <body>. Re-parenting per show also moves it back out\n // once the modal closes.\n const host: HTMLElement = (anchor instanceof HTMLElement ? anchor.closest<HTMLElement>(\"dialog[open]\") : null)\n ?? document.body;\n if (t.parentElement !== host) host.appendChild(t);\n active = anchor instanceof HTMLElement ? anchor : null;\n renderTip(t, text);\n t.hidden = false;\n t.classList.remove(\"show\"); void t.offsetWidth; // restart the enter animation\n place(anchor); // measure + position now it has content + is visible\n t.classList.add(\"show\");\n}\n\nfunction hide(): void {\n if (timer) { window.clearTimeout(timer); timer = 0; }\n active = null;\n activeKey = null;\n if (tip) { tip.hidden = true; tip.classList.remove(\"show\"); }\n}\n\nfunction schedule(anchor: HTMLElement): void {\n if (anchor === active) return;\n if (timer) window.clearTimeout(timer);\n timer = window.setTimeout(() => show(anchor), SHOW_DELAY);\n}\n\n/**\n * Show a tip for something that has no DOM node of its own: a card on a canvas,\n * a zone, a pin. The caller says WHERE (a viewport rectangle) and WHAT, and\n * identifies the subject with `key`.\n *\n * The key is what makes this usable from a canvas. Re-calling with the same key\n * re-places the bubble WITHOUT restarting the delay, so a tip can follow its\n * shape while the camera moves; calling with a different key starts the wait\n * again, so sweeping the pointer across a dense board does not strobe.\n *\n * The bubble is ordinary DOM, so it holds its size whatever the canvas is\n * zoomed to. That is the point of it: the label it stands in for is the one\n * thing on a canvas that must not shrink with everything else.\n */\nexport function tipAt(key: string, rect: TipRect, text: string): void {\n if (key === activeKey) {\n // Already up for this subject: move it, do not re-animate it.\n if (tip && !tip.hidden) { renderTip(tip, text); place(rect); }\n return;\n }\n // A different subject: down first, then wait again. Leaving the old bubble up\n // during the wait would put one card's name over another card, which is worse\n // than no name at all - and it matches the delegated route, where leaving an\n // element hides the tip before the next one is scheduled.\n hide();\n activeKey = key;\n timer = window.setTimeout(() => { timer = 0; paint(rect, text); }, SHOW_DELAY);\n}\n\n/** Take any tip down: the pointer has left, or a gesture has started. */\nexport function hideTip(): void { hide(); }\n\n/**\n * Mount the renderer because something is about to speak the grammar.\n *\n * The themed tooltip is not a feature a host turns on; it is the renderer for a grammar the shell's own\n * components already speak, and `data-tip` with nothing listening draws NOTHING - not the themed\n * bubble, not the platform one. That silence cost Patterpad two releases of a pin with no tooltip, and\n * cost it again when a `title` was converted to `data-tip` in a window that had never mounted this.\n *\n * So `el` calls this whenever it sets a tip. Mounting a delegated listener is the same class of act as\n * `confirmDialog` appending to `document.body`, which this package has always done.\n */\nexport function ensureTooltipHost(): void { initTooltips(); }\n\n/**\n * One deferred check that an app writing `data-tip` ITSELF has a host, warning if not.\n *\n * `ensureTooltipHost` covers anything built through `el`. It cannot cover a `data-tip` written straight\n * into markup or set by hand, which is the case that leaves a whole window of dead tips with no shell\n * component involved - so that one gets a warning rather than silence. Scheduled once, after the\n * current task, so it sees a rendered document rather than an empty one.\n */\nexport function checkTooltipHost(): void {\n if (checked) return;\n checked = true;\n setTimeout(() => {\n if (inited || typeof document === \"undefined\") return;\n if (document.querySelector(\"[data-tip]\")) {\n console.warn(\"app-shell: something set `data-tip` but no tooltip host is mounted, so those rollovers will not draw. Call initTooltips() once per window.\");\n }\n }, 0);\n}\n\nconst tipAncestor = (n: EventTarget | null): HTMLElement | null =>\n (n instanceof Element ? n.closest<HTMLElement>(\"[data-tip]\") : null);\n\n/**\n * Wire the one delegated controller. Mounting happens once; OPTIONS do not.\n *\n * An explicit call is always authoritative about its options, even after something else has already\n * mounted the listener. That ordering is not hypothetical, and Storyletter caught it before it shipped:\n * once components self-mount (see `ensureTooltipHost`), a host's own\n * `initTooltips({ suppressed })` could land second and be dropped by an early return, silently taking\n * a rule like \"no tooltips in Writing View\" with it. Nothing would fail and nothing would show.\n *\n * Components therefore call this with NO arguments: they mount the renderer without claiming the\n * options, and the host keeps the last word on behaviour.\n */\nexport function initTooltips(opts: TooltipOptions = {}): void {\n if (opts.suppressed !== undefined) suppressed = opts.suppressed; // an explicit call always wins\n if (inited) return;\n inited = true;\n\n document.addEventListener(\"pointerover\", (e) => {\n const el = tipAncestor(e.target);\n if (el) schedule(el);\n });\n document.addEventListener(\"pointerout\", (e) => {\n const el = tipAncestor(e.target);\n if (!el) return;\n const to = (e as PointerEvent).relatedTarget;\n if (to instanceof Node && el.contains(to)) return; // still inside the same anchor\n hide();\n });\n // Keyboard parity: reveal on focus, dismiss on blur.\n document.addEventListener(\"focusin\", (e) => { const el = tipAncestor(e.target); if (el) schedule(el); });\n document.addEventListener(\"focusout\", () => hide());\n // It's ANCHORED (not cursor-following), so anything that moves the layout or context dismisses it.\n document.addEventListener(\"pointerdown\", () => hide(), true);\n document.addEventListener(\"scroll\", () => { if (active) hide(); }, true);\n document.addEventListener(\"keydown\", (e) => { if (e.key === \"Escape\") hide(); });\n window.addEventListener(\"blur\", () => hide());\n}\n","// ---------------------------------------------------------------------------\n// `el`: the tiny tag-typed element factory both apps build their imperative,\n// idempotent views with.\n//\n// A SUPERSET of the two apps' prior signatures, so neither rewrites call sites:\n// - Patterpad: el(tag, cls?, text?) (positional)\n// - Storylet Studio: el(tag, { className, text, ... }, ...kids) (options bag)\n// A string second argument is the className; an object is the props bag; string\n// children become text nodes. So `el(\"div\", \"c\", \"t\")` and\n// `el(\"div\", { className: \"c\" }, \"t\")` produce identical DOM.\n// ---------------------------------------------------------------------------\n\nimport { ensureTooltipHost, checkTooltipHost } from \"./tooltip.js\";\nimport { iconNode, iconNameOfGlyph, isIconName, type IconName } from \"./icons.js\";\n\nexport type Child = Node | string | null | undefined;\n\nexport interface ElProps {\n className?: string;\n text?: string;\n /** A NATIVE rollover. Prefer `tip`: `title` is OS chrome on the platform's own\n * slow delay, which is the seam the themed tooltip exists to close. */\n title?: string;\n /** A themed rollover (`data-tip`, picked up by the delegated controller that\n * `initTooltips()` wires). Also the accessible name, since the elements that\n * want one are usually icon buttons with no text of their own. */\n tip?: string;\n onClick?: (event: MouseEvent) => void;\n}\n\nexport function el<K extends keyof HTMLElementTagNameMap>(\n tag: K,\n classNameOrProps?: string | ElProps,\n ...children: Child[]\n): HTMLElementTagNameMap[K] {\n const node = document.createElement(tag);\n const props: ElProps = typeof classNameOrProps === \"string\"\n ? { className: classNameOrProps }\n : classNameOrProps ?? {};\n if (props.className) node.className = props.className;\n if (props.text !== undefined && props.text !== null) node.textContent = props.text;\n if (props.title !== undefined) node.title = props.title;\n if (props.tip !== undefined) {\n node.dataset[\"tip\"] = props.tip;\n node.setAttribute(\"aria-label\", props.tip);\n // Setting a tip mounts the renderer. Without this, `data-tip` draws nothing at all in a window whose\n // host never called `initTooltips()`, which is a silent failure rather than a degraded one. No\n // options passed: the host stays authoritative about behaviour (see tooltip.ts).\n ensureTooltipHost();\n }\n if (props.onClick) node.addEventListener(\"click\", props.onClick as EventListener);\n for (const child of children) {\n if (child === null || child === undefined) continue;\n node.append(child);\n }\n checkTooltipHost(); // one-shot: warns if the APP wrote data-tip and nothing mounted a host\n return node;\n}\n\n/** A small square icon button (the move/delete controls list editors share).\n * Its rollover is the THEMED one (`data-tip`), so an app that has called\n * `initTooltips()` gets our bubble rather than the platform's.\n *\n * `name` is a word from the vocabulary (\"up\", \"close\"), drawn at 14px. A\n * typed glyph from the deprecated `icon` table (\"↑\", \"✕\") is accepted for\n * now and drawn as the word it stood for, so an app that has not moved its\n * call sites yet still gets the drawn set; any other string is set as text,\n * which is the old behaviour and on its way out. */\nexport function iconBtn(name: IconName | (string & {}), title: string, onClick: () => void, disabled = false, danger = false): HTMLButtonElement {\n const b = el(\"button\", `shell-icon${danger ? \" danger\" : \"\"}`);\n b.type = \"button\"; b.dataset[\"tip\"] = title; b.setAttribute(\"aria-label\", title);\n const word = isIconName(name) ? name : iconNameOfGlyph(name);\n if (word) b.append(iconNode(word)); else b.textContent = name;\n b.disabled = disabled;\n b.addEventListener(\"click\", onClick);\n return b;\n}\n\n/**\n * A captioned field: `<label class=\"shell-labelled\"><span class=\"shell-fieldcap\">…</span>control</label>`.\n *\n * The label is pointed at the field it captions, EXPLICITLY, and never at a button. A `<label>` with no\n * `for` forwards a click to its first labelable descendant, and buttons are labelable - so a caption\n * wrapped around a control containing buttons (`tagChips`, whose every chip carries a remove button)\n * turns every click on the row's dead space into a press of the FIRST button in it. That shipped:\n * patterkit/patter#44, where clicking a Game Data list value anywhere but its ✕ deleted the first value\n * in the list. Reported as \"clicking a value removes the wrong one\", which is not a wrong-index bug at\n * all - the click never reached the chip.\n *\n * When the control holds no labelable field (a chips editor with the add input removed, say), this is a\n * plain `<div>`: no caption behaviour is better than a caption that presses something.\n */\nexport function labelled(label: string, control: HTMLElement): HTMLElement {\n const target = control.matches(\"input, select, textarea\")\n ? control\n : control.querySelector<HTMLElement>(\"input:not([type=button]):not([type=submit]), select, textarea\");\n const w = el(target ? \"label\" : \"div\", \"shell-labelled\");\n w.append(el(\"span\", \"shell-fieldcap\", label), control);\n if (target) {\n if (!target.id) target.id = `shell-f-${Math.random().toString(36).slice(2, 9)}`;\n (w as HTMLLabelElement).htmlFor = target.id;\n }\n return w;\n}\n\n/** Swap item `i` with its neighbour `i + delta` IN PLACE (the up/down reorder list editors share);\n * a no-op when the target is out of range. Returns whether anything moved. */\nexport function moveItem<T>(arr: T[], i: number, delta: number): boolean {\n const j = i + delta;\n if (j < 0 || j >= arr.length) return false;\n [arr[i], arr[j]] = [arr[j]!, arr[i]!];\n return true;\n}\n\n/** The icon size inside a chip's remove / move buttons: the chip is set at\n * 0.76rem, so its controls draw smaller than a toolbar button's 14px. */\nconst CHIP_ICON = 10;\n\n/** A tag-style editor for a string-list field (an enum's allowed values / flags): removable chips + an\n * add input (Enter or \",\" commits; blank / duplicate ignored). Mutates `holder.values` in place (read\n * back on save). `onChange` fires after any add / remove so callers can refresh a dependent control. */\nexport function tagChips(holder: { values?: string[] }, onChange?: () => void): HTMLElement {\n const wrap = el(\"div\", \"shell-tags\");\n const input = el(\"input\", \"shell-tag-input\");\n // Sentence-cased like every other hint (the microcopy ruling, 2026-08-28):\n // lowercase fragments read as adrift, and a placeholder is a hint speaking.\n input.type = \"text\"; input.placeholder = \"Add value\"; input.spellcheck = false;\n const makeChip = (v: string): HTMLElement => {\n const chip = el(\"span\", \"shell-tag\", v);\n const x = el(\"button\", \"shell-tag-x\", iconNode(\"close\", CHIP_ICON));\n x.type = \"button\"; x.dataset[\"tip\"] = `Remove ${v}`; x.setAttribute(\"aria-label\", `Remove ${v}`);\n x.addEventListener(\"click\", () => { holder.values = (holder.values ?? []).filter((o) => o !== v); chip.remove(); onChange?.(); });\n chip.append(x);\n return chip;\n };\n const commit = (): void => {\n const v = input.value.trim();\n if (v && !(holder.values ?? []).includes(v)) { (holder.values ??= []).push(v); wrap.insertBefore(makeChip(v), input); onChange?.(); }\n input.value = \"\";\n };\n input.addEventListener(\"keydown\", (e) => { if (e.key === \"Enter\" || e.key === \",\") { e.preventDefault(); commit(); } });\n input.addEventListener(\"blur\", commit);\n for (const v of holder.values ?? []) wrap.append(makeChip(v));\n wrap.append(input);\n return wrap;\n}\n\n/**\n * An ORDERED chips editor for a string-list field whose order means something\n * (a quality's stages: `advance()` walks them, so 2 comes after 1): numbered\n * chips, each with move-earlier / move-later / remove, plus an add input\n * (Enter or \",\" commits; blank / duplicate ignored). Mutates `holder.stages`\n * in place; `onChange` fires after any add / move / remove.\n *\n * Patterpad's, lifted (ui-review-2026-09, finding 3); Storyletter had been\n * faking it with `tagChips` over a `values` holder, which loses the order\n * controls. Same class family as `tagChips` (`.shell-tags`), so it is styled by\n * settings.css already.\n */\nexport function stageChips(holder: { stages?: string[] }, onChange?: () => void): HTMLElement {\n const wrap = el(\"div\", \"shell-tags shell-stages\");\n const input = el(\"input\", \"shell-tag-input\");\n input.type = \"text\"; input.placeholder = \"Add stage\"; input.spellcheck = false;\n const control = (name: IconName, tip: string, disabled: boolean, onClick: () => void): HTMLButtonElement => {\n const b = el(\"button\", \"shell-tag-x shell-tag-move\", iconNode(name, CHIP_ICON));\n b.type = \"button\"; b.dataset[\"tip\"] = tip; b.setAttribute(\"aria-label\", tip);\n b.disabled = disabled;\n b.addEventListener(\"click\", onClick);\n return b;\n };\n const rebuild = (): void => {\n for (const c of Array.from(wrap.children)) if (c !== input) c.remove();\n const stages = holder.stages ?? [];\n stages.forEach((v, i) => {\n const chip = el(\"span\", \"shell-tag\", `${i + 1}. ${v}`);\n chip.dataset[\"stage\"] = v;\n chip.append(\n control(\"back\", `Move ${v} earlier`, i === 0,\n () => { if (moveItem(stages, i, -1)) { rebuild(); onChange?.(); } }),\n control(\"forward\", `Move ${v} later`, i === stages.length - 1,\n () => { if (moveItem(stages, i, 1)) { rebuild(); onChange?.(); } }),\n );\n const x = el(\"button\", \"shell-tag-x\", iconNode(\"close\", CHIP_ICON));\n x.type = \"button\"; x.dataset[\"tip\"] = `Remove ${v}`; x.setAttribute(\"aria-label\", `Remove ${v}`);\n x.addEventListener(\"click\", () => { holder.stages = (holder.stages ?? []).filter((o) => o !== v); rebuild(); onChange?.(); });\n chip.append(x);\n wrap.insertBefore(chip, input);\n });\n };\n const commit = (): void => {\n const v = input.value.trim();\n if (v && !(holder.stages ?? []).includes(v)) { (holder.stages ??= []).push(v); rebuild(); onChange?.(); }\n input.value = \"\";\n };\n input.addEventListener(\"keydown\", (e) => { if (e.key === \"Enter\" || e.key === \",\") { e.preventDefault(); commit(); } });\n input.addEventListener(\"blur\", commit);\n wrap.append(input);\n rebuild();\n return wrap;\n}\n\n// --- drag-to-reorder ---------------------------------------------------------\n// One drag at a time per window, shared across every wired list, so the\n// midpoint test knows what is being carried without a dataTransfer round trip.\nlet reorderDragId: string | null = null;\nconst clearDropMarks = (host: ParentNode): void =>\n host.querySelectorAll(\".drop-before, .drop-after\").forEach((e) => e.classList.remove(\"drop-before\", \"drop-after\"));\n\n/**\n * Make `el` a drag source and a drop target for HTML5 drag-to-reorder among\n * its siblings. While carried it wears `.dragging`; a hovered target wears\n * `.drop-before` or `.drop-after` by the midpoint test on `axis` (\"y\" for\n * rows, \"x\" for a wrapping grid); a drop calls `onMove(draggedId, before,\n * targetId)` and the caller reorders its model and re-renders.\n *\n * Storyletter's `wireCardDrag`, lifted (ui-review-2026-09, finding 30);\n * Patterpad's `wireSceneDrag` is the same idea fixed on \"y\". The marks are\n * classes only: the app draws them (Patterpad's `box-shadow: 0 -2px 0 0\n * var(--accent)` pair is the family's look).\n */\nexport function wireReorder(el: HTMLElement, id: string, axis: \"x\" | \"y\", onMove: (id: string, before: boolean, targetId: string) => void): void {\n el.draggable = true;\n el.dataset[\"reorderId\"] = id;\n const host = (): ParentNode => el.parentElement ?? el;\n el.addEventListener(\"dragstart\", (e) => {\n reorderDragId = id; el.classList.add(\"dragging\");\n if (e.dataTransfer) { e.dataTransfer.effectAllowed = \"move\"; e.dataTransfer.setData(\"text/plain\", id); }\n });\n el.addEventListener(\"dragend\", () => { reorderDragId = null; el.classList.remove(\"dragging\"); clearDropMarks(host()); });\n el.addEventListener(\"dragover\", (e) => {\n if (!reorderDragId || reorderDragId === id) return;\n e.preventDefault();\n if (e.dataTransfer) e.dataTransfer.dropEffect = \"move\";\n const r = el.getBoundingClientRect();\n const before = axis === \"y\" ? e.clientY < r.top + r.height / 2 : e.clientX < r.left + r.width / 2;\n clearDropMarks(host());\n el.classList.add(before ? \"drop-before\" : \"drop-after\");\n });\n el.addEventListener(\"dragleave\", () => el.classList.remove(\"drop-before\", \"drop-after\"));\n el.addEventListener(\"drop\", (e) => {\n e.preventDefault();\n const before = el.classList.contains(\"drop-before\");\n clearDropMarks(host());\n if (reorderDragId && reorderDragId !== id) onMove(reorderDragId, before, id);\n });\n}\n\n// --- metadata and breadcrumbs ---------------------------------------------------\n// \"Icons are drawn, and so are separators\" (design-language.md section 4,\n// 2026-09-16): metadata is never joined with \"·\" and a breadcrumb is never a\n// string with \"›\" in it. Both apps had been building exactly those strings\n// (ui-review-2026-09, marker A); these two helpers make the same lines as DOM,\n// with the separator drawn by CSS (controls.css) or by the icon vocabulary.\n\n/**\n * A metadata line: `parts` as spans in a flex row, separated by a drawn\n * 3px disc (a `::before` on every part after the first, in controls.css),\n * so `metaLine([\"6 runs\", \"200 max steps\", \"seed 4\"])` is what\n * \"6 runs · 200 max steps · seed 4\" used to be, without the typed dot.\n * Empty, null and undefined parts are skipped, so a caller can pass an\n * optional segment without branching.\n */\nexport function metaLine(parts: (string | Node | null | undefined)[]): HTMLElement {\n const row = el(\"span\", \"shell-meta\");\n for (const part of parts) {\n if (part === null || part === undefined || part === \"\") continue;\n const seg = el(\"span\", \"shell-meta-part\");\n seg.append(part);\n row.append(seg);\n }\n return row;\n}\n\nexport interface Crumb {\n label: string;\n /** Makes the crumb a button. Ignored on the last crumb, which is where the\n * reader already is. */\n onClick?: (event: MouseEvent) => void;\n}\n\n/**\n * A breadcrumb trail: each crumb a span, or a button when it carries\n * `onClick` and is not the last, with `iconNode(\"forward\", 12)` drawn between\n * them. `breadcrumb([\"Box\", \"Deck\", \"Card\"])` is what \"Box › Deck › Card\"\n * used to be; the chevrons are the family's own drawing, not a symbol-font\n * character.\n */\nexport function breadcrumb(parts: (string | Crumb)[]): HTMLElement {\n const trail = el(\"nav\", \"shell-crumbs\");\n trail.setAttribute(\"aria-label\", \"Where this is\");\n parts.forEach((part, i) => {\n const crumb: Crumb = typeof part === \"string\" ? { label: part } : part;\n const last = i === parts.length - 1;\n if (i > 0) {\n const sep = iconNode(\"forward\", 12);\n sep.classList.add(\"shell-crumb-sep\");\n trail.append(sep);\n }\n if (crumb.onClick && !last) {\n const b = el(\"button\", { className: \"shell-crumb shell-crumb-link\", text: crumb.label, onClick: crumb.onClick });\n b.type = \"button\";\n trail.append(b);\n } else {\n const s = el(\"span\", \"shell-crumb\", crumb.label);\n if (last) s.setAttribute(\"aria-current\", \"location\");\n trail.append(s);\n }\n });\n return trail;\n}\n","// ---------------------------------------------------------------------------\n// The exit half of the panel motion vocabulary.\n//\n// Enters are pure CSS: they run on insertion. Exits cannot be, because the node\n// has to OUTLIVE the close for the animation to play and then be torn down. So\n// every closing surface routes its teardown through here instead of removing\n// itself directly.\n//\n// Patterpad's `closeWithExit`, lifted. It was already app-agnostic, and the\n// shell had grown a second copy of the same twelve lines inside `confirm.ts`,\n// which is the drift this package exists to stop: one motion vocabulary, drawn\n// one way. `tokens.css` already carried the keyframes and the durations; this\n// is the missing half that plays them.\n// ---------------------------------------------------------------------------\n\n/**\n * Play the close animation on `element`, then run `done` exactly once.\n *\n * `done` does the real teardown: remove the node, drop listeners, unmount\n * whatever was inside it. Under `prefers-reduced-motion` the duration reads 0\n * and `done` runs synchronously, so reduced motion means instant rather than\n * an invisible wait.\n */\nexport function closeWithExit(element: HTMLElement, done: () => void): void {\n element.classList.add(\"closing\");\n // getComputedStyle flushes pending style, so this reflects the `.closing`\n // rule we just added rather than the state before it.\n const seconds = parseFloat(getComputedStyle(element).animationDuration) || 0;\n if (seconds === 0) { done(); return; }\n\n let fired = false;\n const finish = (): void => {\n if (fired) return;\n fired = true;\n element.removeEventListener(\"animationend\", onEnd);\n clearTimeout(timer);\n done();\n };\n // Only OUR animation ending counts. Content inside the panel (rows, pills,\n // spinners) raises its own animationend, and an inner one firing first would\n // tear the panel down mid-fade.\n const onEnd = (e: AnimationEvent): void => { if (e.target === element) finish(); };\n element.addEventListener(\"animationend\", onEnd);\n // Fallback: animationend is missed if the element is hidden or the animation\n // is interrupted, and a panel that never tears down is worse than an abrupt one.\n const timer = setTimeout(finish, seconds * 1000 + 120);\n}\n","// ---------------------------------------------------------------------------\n// dialogFrame: the one modal skeleton every dialog in the family sits on.\n//\n// Title, an optional sub line, a body the caller fills, an actions row the\n// caller fills, on a native <dialog> so the focus trap and Escape come from the\n// platform. Four scrims and three panel drawings had grown across five shell\n// files and a dozen app dialogs (ui-review-2026-09, findings 9 and 30); this is\n// the frame they all sit on now, and the shell's own dialogs (confirm, identity,\n// about, notes) were moved onto it first, which is how it is proven.\n//\n// What the frame owns: the backdrop, the panel, the enter and exit motion\n// (closeWithExit), Escape, and giving focus back to whatever had it when the\n// dialog opened. What it does not own: the content classes, the buttons'\n// meaning, and when to close. The caller keeps those.\n//\n// Styles ship as dialog.css. The action buttons are expected to wear `.btn`\n// from controls.css; the frame does not draw buttons of its own.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\nimport { closeWithExit } from \"./exit.js\";\n\nexport interface DialogFrameOptions {\n /** The heading. Also the dialog's accessible name. */\n title: string;\n /** One plain sentence under the heading, if the title needs one. */\n sub?: string;\n /** Extra class(es) on the <dialog>, for the caller's own content rules and\n * width (`--dialog-width` on the class; see dialog.css). */\n className?: string;\n /** Runs once, after the dialog has left the document, however it closed. */\n onClose?: () => void;\n}\n\nexport interface DialogFrame {\n readonly dialog: HTMLDialogElement;\n /** Fill this with the content. */\n readonly body: HTMLElement;\n /** Fill this with the buttons, right-aligned; empty, it draws nothing. */\n readonly actions: HTMLElement;\n /** Show modally. Records what had focus so `close()` can give it back. */\n open(): void;\n /** Play the exit, tear down, restore focus. Idempotent. */\n close(): void;\n isOpen(): boolean;\n}\n\n/** Build a dialog on the family's frame. Appended to <body> at once (so an\n * `open()` on the next line has nothing to wait for); shown only on `open()`. */\nexport function dialogFrame(opts: DialogFrameOptions): DialogFrame {\n const dialog = el(\"dialog\", `shell-dialog ${opts.className ?? \"\"}`.trim());\n const body = el(\"div\", \"shell-dialog-body\");\n const actions = el(\"div\", \"shell-dialog-actions\");\n dialog.append(el(\"h2\", \"shell-dialog-title\", opts.title));\n if (opts.sub !== undefined) dialog.append(el(\"p\", \"shell-dialog-sub\", opts.sub));\n dialog.append(body, actions);\n dialog.setAttribute(\"aria-label\", opts.title);\n document.body.append(dialog);\n\n let state: \"closed\" | \"open\" | \"closing\" = \"closed\";\n let opener: Element | null = null;\n\n const teardown = (): void => {\n dialog.remove();\n // Back to the control that opened this. The platform does it for a plain\n // dialog.close(), but the exit motion ends in a remove(), and a removed\n // dialog restores nothing.\n if (opener instanceof HTMLElement && opener.isConnected) opener.focus();\n opener = null;\n state = \"closed\";\n opts.onClose?.();\n };\n\n const close = (): void => {\n if (state !== \"open\") return;\n state = \"closing\";\n closeWithExit(dialog, () => { if (dialog.open) dialog.close(); teardown(); });\n };\n\n // Escape arrives as the cancel event; route it through the exit motion.\n dialog.addEventListener(\"cancel\", (e) => { e.preventDefault(); close(); });\n // Closed by someone else (a form with method=\"dialog\", or a direct\n // dialog.close()): no motion to play, tear down at once.\n dialog.addEventListener(\"close\", () => { if (state === \"open\") teardown(); });\n\n return {\n dialog, body, actions,\n open() {\n if (state !== \"closed\") return;\n opener = document.activeElement;\n state = \"open\";\n dialog.showModal();\n },\n close,\n isOpen: () => state === \"open\",\n };\n}\n","// ---------------------------------------------------------------------------\n// The kit gallery: pick a starting point by what you want to make.\n//\n// The new-thing moment for the family (New Project, New Box, New Scene), drawn\n// once. Adobe's New Document is the shape: tiles on the left, a details panel\n// on the right, one screen. Not a wizard: a wizard asks for settings before\n// anything has been chosen, and the choice is the part that teaches.\n//\n// Storyletter grew this as `kit-picker.ts` (one component at two scales on\n// <dialog>, \"a shell candidate, held back until it had been used in earnest\")\n// and the kit gallery brief moved it here, 2026-09-27\n// (storylet-studio/design/kit-gallery.md). What a kit IS made of stays in\n// each app: this file knows names, words and pictures, never boxes or scenes.\n//\n// Sections are shelves (\"Start from a kit\", \"Learn from a finished project\"),\n// and each says what its button does, because opening a copy of an example\n// and creating from a kit are different acts behind the same kind of tile.\n// The app's own fields (a project name, version control, a build path) sit in\n// the details panel, shown only for the sections that use them.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\nimport { dialogFrame } from \"./dialog.js\";\n\nexport interface KitGalleryItem<T extends string> {\n id: T;\n /** What you are making, in the author's terms, not the model's. */\n name: string;\n /** What it is FOR: the panel's first line, and the tile's unless `tile` says otherwise. */\n blurb: string;\n /** The tile's line, when the blurb is longer than a tile holds (three\n * lines). A whole short sentence reads better than a long one cut off. */\n tile?: string;\n /** More, for the panel only. */\n detail?: string;\n /** What pressing Play (or Run) shows, said as a promise. */\n play?: string;\n /** The model's parts it puts to work, second and quieter (\"Shows: ...\"). */\n shows?: string;\n /** What lands, in the concrete, as a short list in the panel. */\n lands?: string[];\n /** A word or two on the tile (\"Start here\"). */\n badge?: string;\n /** An illustration: a transparent image whose opaque pixels are the ink.\n * Drawn as a mask in `--kit-art-ink` on `--kit-art-plate`, so one file\n * serves both themes and the app chooses the colours. */\n image?: string;\n}\n\nexport interface KitGallerySection<T extends string> {\n /** Sentence-case label text (\"Start from a kit\"). Absent: no caption. */\n caption?: string;\n /** The consequence of a click, said before the click. */\n note?: string;\n items: KitGalleryItem<T>[];\n /** The primary button's label while an item here is chosen. Default \"Create\". */\n action?: string;\n /** Show the app's detail fields (and ask for the name) for this section.\n * Default true. An example is opened, not named, so its shelf says false. */\n usesDetails?: boolean;\n}\n\nexport interface KitGalleryChoice {\n /** Present when a name was asked for and the chosen section uses details. */\n name?: string;\n}\n\nexport interface KitGalleryOptions<T extends string> {\n /** The dialog's heading: \"New project\", \"New box\", \"New scene\". */\n title: string;\n /** What the thing being made IS, in concrete terms. */\n what?: string;\n sections: KitGallerySection<T>[];\n /** Ask for a name in the details panel, with this placeholder. Required\n * when asked: making a thing without one would have to invent a folder. */\n namePlaceholder?: string;\n /** The name field's label. Default \"Name\". */\n nameLabel?: string;\n /** The app's own fields, below the name (version control, a build path). */\n details?: HTMLElement;\n /** Runs whenever an item is chosen, the opening one included, so the app can show or hide its\n * own fields for it (New Scene's Speaker, for a kit with lines). */\n onChoose?: (id: T) => void;\n /** Checked before `onPick`, after the name: return the app's field that still needs filling (it\n * is focused and marked, and nothing is made), or null when all is well. */\n validate?: (id: T) => HTMLElement | null;\n /** Runs as the name is typed (and once on open, with \"\"), so the app's own\n * fields can follow it: Patterpad's folder preview and default publish path. */\n onNameInput?: (name: string) => void;\n /** The item chosen when the gallery opens. Default: the first. */\n initial?: T;\n onPick: (id: T, choice: KitGalleryChoice) => void;\n /** Runs once the gallery has closed, whichever way. */\n onClose?: () => void;\n}\n\nexport interface KitGallery {\n readonly dialog: HTMLDialogElement;\n close(): void;\n}\n\n/** Draw `image` into an art box as a mask (see kit-gallery.css), or hide the box. */\nfunction setArt(host: HTMLElement, image: string | undefined): void {\n host.replaceChildren();\n host.hidden = image === undefined;\n if (image === undefined) return;\n const ink = el(\"span\", \"kit-gallery-ink\");\n ink.style.setProperty(\"--kit-art\", `url(\"${image.replace(/\"/g, \"%22\")}\")`);\n host.append(ink);\n}\n\n/** One tile: the picture, the name with its badge, the blurb. The gallery's\n * own, and the welcome screen's, so a kit looks the same wherever it is met\n * (welcome.ts draws a tiled group with it; import kit-gallery.css there too). */\nexport function kitTile(item: { name: string; blurb: string; badge?: string; image?: string }, onClick: () => void): HTMLButtonElement {\n const art = el(\"div\", \"kit-gallery-art\");\n setArt(art, item.image);\n const tile = el(\"button\", { className: \"kit-gallery-tile\", onClick },\n art,\n el(\"span\", \"kit-gallery-tile-head\",\n el(\"span\", \"kit-gallery-tile-name\", item.name),\n item.badge !== undefined ? el(\"span\", \"kit-gallery-badge\", item.badge) : null),\n el(\"span\", \"kit-gallery-tile-blurb\", item.blurb)) as HTMLButtonElement;\n tile.type = \"button\";\n return tile;\n}\n\n/** Open the gallery as a modal dialog on the family's frame. */\nexport function openKitGallery<T extends string>(opts: KitGalleryOptions<T>): KitGallery {\n const frame = dialogFrame({ title: opts.title, ...(opts.what !== undefined ? { sub: opts.what } : {}), className: \"kit-gallery\", ...(opts.onClose ? { onClose: opts.onClose } : {}) });\n const { dialog } = frame;\n\n const entries = opts.sections.flatMap((section, s) => section.items.map((item) => ({ item, section, s })));\n if (entries.length === 0) throw new Error(\"openKitGallery: no items to choose from\");\n let chosen = entries.find((e) => e.item.id === opts.initial) ?? entries[0]!;\n\n // --- the details panel ----------------------------------------------------\n const nameInput = opts.namePlaceholder === undefined ? undefined : el(\"input\", { className: \"field kit-gallery-name\" }) as HTMLInputElement;\n if (nameInput) {\n nameInput.placeholder = opts.namePlaceholder!;\n nameInput.setAttribute(\"aria-label\", opts.nameLabel ?? \"Name\");\n }\n const fields = el(\"div\", \"kit-gallery-fields\",\n ...(nameInput ? [el(\"label\", \"kit-gallery-label\", opts.nameLabel ?? \"Name\"), nameInput] : []),\n ...(opts.details ? [opts.details] : []));\n\n const panelArt = el(\"div\", \"kit-gallery-art kit-gallery-panel-art\");\n const panelName = el(\"h3\", \"kit-gallery-panel-name\");\n const panelBlurb = el(\"p\", \"kit-gallery-panel-blurb\");\n const panelDetail = el(\"p\", \"kit-gallery-panel-detail\");\n const panelPlay = el(\"p\", \"kit-gallery-play\");\n const panelShows = el(\"p\", \"kit-gallery-shows\");\n const panelLands = el(\"ul\", \"kit-gallery-lands\");\n const panelLandsCaption = el(\"p\", \"kit-gallery-lands-caption\", \"What you get\");\n const panelNote = el(\"p\", \"kit-gallery-note\");\n const panel = el(\"div\", \"kit-gallery-panel\",\n panelArt, panelName, panelBlurb, panelDetail, panelPlay, panelShows, panelLandsCaption, panelLands, panelNote, fields);\n\n const primary = el(\"button\", { className: \"btn primary\", onClick: () => commit() }) as HTMLButtonElement;\n primary.type = \"button\";\n const cancel = el(\"button\", { className: \"btn\", text: \"Cancel\", onClick: () => frame.close() }) as HTMLButtonElement;\n cancel.type = \"button\";\n\n const setText = (node: HTMLElement, text: string | undefined): void => {\n node.textContent = text ?? \"\";\n node.hidden = text === undefined || text === \"\";\n };\n\n // --- the tiles --------------------------------------------------------------\n const tiles = new Map<T, HTMLButtonElement>();\n const shelves = opts.sections.map((section) => {\n const grid = el(\"div\", \"kit-gallery-grid\", ...section.items.map((item) => {\n const tile = kitTile({ ...item, blurb: item.tile ?? item.blurb }, () => choose(item.id));\n tile.dataset.kit = item.id;\n // Double-click is the desktop's \"choose and go\"; a single click only chooses,\n // so a tile can be read in the panel before anything is made.\n tile.addEventListener(\"dblclick\", () => { choose(item.id); commit(); });\n tiles.set(item.id, tile);\n return tile;\n }));\n return el(\"section\", \"kit-gallery-shelf\",\n section.caption !== undefined ? el(\"h3\", \"kit-gallery-caption\", section.caption) : null,\n section.note !== undefined ? el(\"p\", \"kit-gallery-shelf-note\", section.note) : null,\n grid);\n });\n\n const choose = (id: T): void => {\n const next = entries.find((e) => e.item.id === id);\n if (!next) return;\n chosen = next;\n for (const [tid, tile] of tiles) tile.setAttribute(\"aria-pressed\", String(tid === id));\n const { item, section } = chosen;\n setArt(panelArt, item.image);\n panelName.textContent = item.name;\n setText(panelBlurb, item.blurb);\n setText(panelDetail, item.detail);\n setText(panelPlay, item.play);\n setText(panelShows, item.shows !== undefined ? `Shows: ${item.shows}` : undefined);\n panelLands.replaceChildren(...(item.lands ?? []).map((l) => el(\"li\", undefined, l)));\n panelLands.hidden = (item.lands ?? []).length === 0;\n panelLandsCaption.hidden = panelLands.hidden;\n const details = section.usesDetails !== false;\n fields.hidden = !details || (nameInput === undefined && opts.details === undefined);\n // Without the fields, the shelf's note is what the click will do next\n // (\"a folder you choose\"), so the panel repeats it beside the button.\n setText(panelNote, details ? undefined : section.note);\n primary.textContent = section.action ?? \"Create\";\n opts.onChoose?.(item.id);\n };\n\n const commit = (): void => {\n const { item, section } = chosen;\n const details = section.usesDetails !== false;\n let name: string | undefined;\n if (details && nameInput) {\n name = nameInput.value.trim();\n if (name === \"\") { nameInput.focus(); nameInput.classList.add(\"kit-gallery-missing\"); return; }\n }\n const missing = opts.validate?.(item.id) ?? null;\n if (missing) {\n missing.classList.add(\"kit-gallery-missing\");\n missing.addEventListener(\"input\", () => missing.classList.remove(\"kit-gallery-missing\"), { once: true });\n missing.focus();\n return;\n }\n frame.close();\n opts.onPick(item.id, name !== undefined ? { name } : {});\n };\n\n if (nameInput) {\n nameInput.addEventListener(\"input\", () => {\n nameInput.classList.remove(\"kit-gallery-missing\");\n opts.onNameInput?.(nameInput.value);\n });\n opts.onNameInput?.(\"\");\n // Enter creates with what is chosen: the author has said what it should be\n // called, and the tile they are looking at is the one they meant.\n nameInput.addEventListener(\"keydown\", (e) => {\n e.stopPropagation();\n if (e.key !== \"Enter\") return;\n e.preventDefault();\n commit();\n });\n }\n\n frame.body.append(el(\"div\", \"kit-gallery-layout\", el(\"div\", \"kit-gallery-shelves\", ...shelves), panel));\n frame.actions.append(cancel, primary);\n dialog.addEventListener(\"click\", (e) => { if (e.target === dialog) frame.close(); });\n\n choose(chosen.item.id);\n frame.open();\n // Focus where the next keystroke is wanted: the name when one is asked for\n // and the opening shelf uses it, otherwise the chosen tile.\n if (nameInput && chosen.section.usesDetails !== false) nameInput.focus();\n else tiles.get(chosen.item.id)?.focus();\n\n return { dialog, close: () => frame.close() };\n}\n"]}
|
package/dist/kit-gallery.css
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/* src/kit-gallery.css */
|
|
2
2
|
.kit-gallery {
|
|
3
3
|
--dialog-width: 880px;
|
|
4
|
-
--kit-art-ink: var(--ink);
|
|
5
|
-
--kit-art-plate: transparent;
|
|
6
4
|
}
|
|
7
5
|
.kit-gallery .shell-dialog-body {
|
|
8
6
|
overflow: hidden;
|
|
@@ -98,7 +96,7 @@
|
|
|
98
96
|
aspect-ratio: 5 / 3;
|
|
99
97
|
overflow: hidden;
|
|
100
98
|
border-radius: var(--radius-sm);
|
|
101
|
-
background: var(--kit-art-plate);
|
|
99
|
+
background: var(--kit-art-plate, transparent);
|
|
102
100
|
}
|
|
103
101
|
.kit-gallery-art[hidden] {
|
|
104
102
|
display: none;
|
|
@@ -106,7 +104,7 @@
|
|
|
106
104
|
.kit-gallery-ink {
|
|
107
105
|
position: absolute;
|
|
108
106
|
inset: 0;
|
|
109
|
-
background: var(--kit-art-ink);
|
|
107
|
+
background: var(--kit-art-ink, var(--ink));
|
|
110
108
|
-webkit-mask: var(--kit-art) center / cover no-repeat;
|
|
111
109
|
mask: var(--kit-art) center / cover no-repeat;
|
|
112
110
|
}
|
|
@@ -157,7 +155,7 @@
|
|
|
157
155
|
font: 600 13px/1.3 var(--font-ui);
|
|
158
156
|
color: var(--ink);
|
|
159
157
|
}
|
|
160
|
-
.kit-gallery
|
|
158
|
+
.kit-gallery .kit-gallery-missing {
|
|
161
159
|
border-color: var(--danger);
|
|
162
160
|
}
|
|
163
161
|
@media (max-width: 760px) {
|
package/dist/kit-gallery.css.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/kit-gallery.css"],"sourcesContent":["/* ---------------------------------------------------------------------------\n The kit gallery (kit-gallery.ts): tiles on the left, a details panel on the\n right, on the dialog frame (dialog.css) with `.btn` from controls.css and\n `.field` for the name. Reads the palette (--surface, --ink, --muted,\n --line, --accent) over the shared tokens.\n\n Pictures are masks: the image's opaque pixels are drawn in --kit-art-ink on\n --kit-art-plate
|
|
1
|
+
{"version":3,"sources":["../src/kit-gallery.css"],"sourcesContent":["/* ---------------------------------------------------------------------------\n The kit gallery (kit-gallery.ts): tiles on the left, a details panel on the\n right, on the dialog frame (dialog.css) with `.btn` from controls.css and\n `.field` for the name. Reads the palette (--surface, --ink, --muted,\n --line, --accent) over the shared tokens.\n\n Pictures are masks: the image's opaque pixels are drawn in --kit-art-ink on\n --kit-art-plate, which an app sets on :root (the welcome's tiles sit outside\n the dialog, so the gallery never sets them itself). The defaults, the app's\n --ink on no plate, suit a light theme. A dark theme should NOT invert an engraving, which turns it into a photographic\n negative; it sets a paper-toned plate and a dark ink instead, so the\n picture is always dark on light and only its frame follows the theme.\n --------------------------------------------------------------------------- */\n\n.kit-gallery { --dialog-width: 880px; }\n.kit-gallery .shell-dialog-body { overflow: hidden; }\n\n.kit-gallery-layout {\n display: grid; grid-template-columns: minmax(0, 1fr) 288px; gap: 18px;\n min-height: 0; max-height: 64vh;\n}\n.kit-gallery-shelves { overflow-y: auto; min-height: 0; padding: 2px; }\n.kit-gallery-panel {\n overflow-y: auto; min-height: 0;\n display: flex; flex-direction: column; gap: 6px;\n padding: 2px 2px 2px 18px; border-left: 1px solid var(--line);\n}\n\n/* A shelf per job (start / learn), captioned like the welcome's groups:\n sentence-case label text, never an overline. */\n.kit-gallery-shelf + .kit-gallery-shelf { margin-top: 18px; }\n.kit-gallery-caption { margin: 0 0 6px; color: var(--muted); font: 600 13px/1.3 var(--font-ui); }\n.kit-gallery-shelf-note { margin: -2px 0 8px; color: var(--muted); font: 12.5px/1.45 var(--font-ui); }\n\n.kit-gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(176px, 1fr)); gap: 8px; }\n.kit-gallery-tile {\n display: flex; flex-direction: column; align-items: stretch; gap: 4px;\n padding: 8px; text-align: left; cursor: pointer;\n background: none; color: var(--ink); font: inherit;\n border: 1px solid var(--line); border-radius: var(--radius-md);\n transition: background var(--dur-quick) var(--ease-standard), border-color var(--dur-quick) var(--ease-standard);\n}\n.kit-gallery-tile:hover { background: color-mix(in oklab, var(--accent) 6%, transparent); }\n.kit-gallery-tile[aria-pressed=\"true\"] {\n border-color: var(--accent);\n background: color-mix(in oklab, var(--accent) 10%, transparent);\n}\n.kit-gallery-tile-head { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }\n.kit-gallery-tile-name { font: 600 14px/1.3 var(--font-ui); }\n/* Three lines on the tile; the panel says the rest. */\n.kit-gallery-tile-blurb {\n color: var(--muted); font: 12.5px/1.4 var(--font-ui);\n display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3; overflow: hidden;\n}\n.kit-gallery-badge {\n padding: 1px 6px; border-radius: 999px;\n background: color-mix(in oklab, var(--accent) 16%, transparent); color: var(--ink);\n font: 600 11px/1.5 var(--font-ui);\n}\n\n/* The picture: a plate, and the ink drawn through the image as a mask. */\n.kit-gallery-art {\n position: relative; aspect-ratio: 5 / 3; overflow: hidden;\n border-radius: var(--radius-sm); background: var(--kit-art-plate, transparent);\n}\n.kit-gallery-art[hidden] { display: none; }\n.kit-gallery-ink {\n position: absolute; inset: 0; background: var(--kit-art-ink, var(--ink));\n -webkit-mask: var(--kit-art) center / cover no-repeat;\n mask: var(--kit-art) center / cover no-repeat;\n}\n\n/* The details panel: the chosen kit said in full, then the app's fields. */\n.kit-gallery-panel-name { margin: 2px 0 0; font: 600 16px/1.3 var(--font-ui); }\n.kit-gallery-panel-blurb, .kit-gallery-panel-detail { margin: 0; font: 13.5px/1.5 var(--font-ui); color: var(--ink); }\n.kit-gallery-play { margin: 0; font: 13.5px/1.5 var(--font-ui); color: var(--ink); }\n.kit-gallery-shows, .kit-gallery-note { margin: 0; font: 12.5px/1.45 var(--font-ui); color: var(--muted); }\n.kit-gallery-lands-caption { margin: 6px 0 0; font: 600 13px/1.3 var(--font-ui); color: var(--muted); }\n.kit-gallery-lands { margin: 0; padding-left: 18px; font: 13px/1.5 var(--font-ui); color: var(--ink); }\n.kit-gallery-panel [hidden] { display: none; }\n.kit-gallery-fields { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; padding-top: 10px; border-top: 1px solid var(--line); }\n.kit-gallery-label { font: 600 13px/1.3 var(--font-ui); color: var(--ink); }\n.kit-gallery .kit-gallery-missing { border-color: var(--danger); }\n\n/* Narrow windows: the panel goes under the tiles. */\n@media (max-width: 760px) {\n .kit-gallery-layout { grid-template-columns: minmax(0, 1fr); max-height: none; overflow-y: auto; }\n .kit-gallery-panel { border-left: none; border-top: 1px solid var(--line); padding: 12px 2px 2px; overflow: visible; }\n .kit-gallery-shelves { overflow: visible; }\n}\n"],"mappings":"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","names":[]}
|
package/dist/kit-gallery.d.cts
CHANGED
|
@@ -51,6 +51,12 @@ interface KitGalleryOptions<T extends string> {
|
|
|
51
51
|
nameLabel?: string;
|
|
52
52
|
/** The app's own fields, below the name (version control, a build path). */
|
|
53
53
|
details?: HTMLElement;
|
|
54
|
+
/** Runs whenever an item is chosen, the opening one included, so the app can show or hide its
|
|
55
|
+
* own fields for it (New Scene's Speaker, for a kit with lines). */
|
|
56
|
+
onChoose?: (id: T) => void;
|
|
57
|
+
/** Checked before `onPick`, after the name: return the app's field that still needs filling (it
|
|
58
|
+
* is focused and marked, and nothing is made), or null when all is well. */
|
|
59
|
+
validate?: (id: T) => HTMLElement | null;
|
|
54
60
|
/** Runs as the name is typed (and once on open, with ""), so the app's own
|
|
55
61
|
* fields can follow it: Patterpad's folder preview and default publish path. */
|
|
56
62
|
onNameInput?: (name: string) => void;
|
package/dist/kit-gallery.d.ts
CHANGED
|
@@ -51,6 +51,12 @@ interface KitGalleryOptions<T extends string> {
|
|
|
51
51
|
nameLabel?: string;
|
|
52
52
|
/** The app's own fields, below the name (version control, a build path). */
|
|
53
53
|
details?: HTMLElement;
|
|
54
|
+
/** Runs whenever an item is chosen, the opening one included, so the app can show or hide its
|
|
55
|
+
* own fields for it (New Scene's Speaker, for a kit with lines). */
|
|
56
|
+
onChoose?: (id: T) => void;
|
|
57
|
+
/** Checked before `onPick`, after the name: return the app's field that still needs filling (it
|
|
58
|
+
* is focused and marked, and nothing is made), or null when all is well. */
|
|
59
|
+
validate?: (id: T) => HTMLElement | null;
|
|
54
60
|
/** Runs as the name is typed (and once on open, with ""), so the app's own
|
|
55
61
|
* fields can follow it: Patterpad's folder preview and default publish path. */
|
|
56
62
|
onNameInput?: (name: string) => void;
|
package/dist/kit-gallery.js
CHANGED