@wildwinter/app-shell 0.46.4 → 0.47.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/LICENSES.md +5 -4
- package/dist/{chunk-EHR2KBZC.js → chunk-5SQHL4YV.js} +3 -3
- package/dist/{chunk-EHR2KBZC.js.map → chunk-5SQHL4YV.js.map} +1 -1
- package/dist/{chunk-J6RSONXM.js → chunk-FPXEQKWM.js} +4 -4
- package/dist/{chunk-J6RSONXM.js.map → chunk-FPXEQKWM.js.map} +1 -1
- package/dist/{chunk-R3NXPHRZ.js → chunk-IFWLNIBR.js} +13 -5
- package/dist/chunk-IFWLNIBR.js.map +1 -0
- package/dist/{chunk-CUFREPJ5.js → chunk-IK3LWBQL.js} +4 -4
- package/dist/{chunk-CUFREPJ5.js.map → chunk-IK3LWBQL.js.map} +1 -1
- package/dist/{chunk-WKPKBAXS.js → chunk-NKL6DNBZ.js} +4 -4
- package/dist/{chunk-WKPKBAXS.js.map → chunk-NKL6DNBZ.js.map} +1 -1
- package/dist/{chunk-UG2P777F.js → chunk-QLVBKME7.js} +34 -6
- package/dist/chunk-QLVBKME7.js.map +1 -0
- package/dist/{chunk-P6WLAEKZ.js → chunk-QVACRWG7.js} +13 -3
- package/dist/chunk-QVACRWG7.js.map +1 -0
- package/dist/{chunk-2ECJUTSX.js → chunk-T25WXWPB.js} +3 -2
- package/dist/chunk-T25WXWPB.js.map +1 -0
- package/dist/{chunk-YRFJLJUH.js → chunk-XR4DRDMA.js} +3 -3
- package/dist/{chunk-YRFJLJUH.js.map → chunk-XR4DRDMA.js.map} +1 -1
- package/dist/{chunk-G2JT3HZH.js → chunk-XXDJCOXN.js} +9 -9
- package/dist/{chunk-G2JT3HZH.js.map → chunk-XXDJCOXN.js.map} +1 -1
- package/dist/context-menu.cjs.map +1 -1
- package/dist/context-menu.js +2 -2
- package/dist/dialog.cjs.map +1 -1
- package/dist/dialog.js +2 -2
- package/dist/index.cjs +80 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +85 -12
- package/dist/index.d.ts +85 -12
- package/dist/index.js +34 -20
- package/dist/index.js.map +1 -1
- package/dist/kit-gallery.cjs.map +1 -1
- package/dist/kit-gallery.js +3 -3
- package/dist/link-status.cjs +12 -5
- package/dist/link-status.cjs.map +1 -1
- package/dist/link-status.d.cts +1 -1
- package/dist/link-status.d.ts +1 -1
- package/dist/link-status.js +3 -3
- package/dist/menu.cjs +1 -0
- package/dist/menu.cjs.map +1 -1
- package/dist/menu.d.cts +8 -4
- package/dist/menu.d.ts +8 -4
- package/dist/menu.js +1 -1
- package/dist/{property-list-D3VZCJzi.d.cts → property-list-5mS4J2uy.d.cts} +8 -2
- package/dist/{property-list-D3VZCJzi.d.ts → property-list-5mS4J2uy.d.ts} +8 -2
- package/dist/property-list.cjs +10 -2
- package/dist/property-list.cjs.map +1 -1
- package/dist/property-list.d.cts +1 -1
- package/dist/property-list.d.ts +1 -1
- package/dist/property-list.js +2 -2
- package/dist/settings.css +54 -0
- package/dist/settings.css.map +1 -1
- package/dist/updater-view.cjs.map +1 -1
- package/dist/updater-view.js +3 -3
- package/dist/util.cjs +11 -0
- package/dist/util.cjs.map +1 -1
- package/dist/util.d.cts +18 -1
- package/dist/util.d.ts +18 -1
- package/dist/util.js +1 -1
- package/dist/welcome.cjs.map +1 -1
- package/dist/welcome.js +4 -4
- package/package.json +1 -1
- package/dist/chunk-2ECJUTSX.js.map +0 -1
- package/dist/chunk-P6WLAEKZ.js.map +0 -1
- package/dist/chunk-R3NXPHRZ.js.map +0 -1
- package/dist/chunk-UG2P777F.js.map +0 -1
package/dist/welcome.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tooltip.ts","../src/dom.ts","../src/kit-gallery.ts","../src/welcome.ts"],"names":["el"],"mappings":"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","file":"welcome.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 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 /** What it has, in a word or two each (\"Drawn map\", \"Copies\"): pills on the tile and in the\n * panel, so kits compare at a glance before any is chosen. The author's ask, 2026-09-27, in\n * place of pictures, which were ruled out the same day. */\n features?: 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}\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/** A kit's features as pills, or null when it lists none. */\nfunction featurePills(features: string[] | undefined): HTMLElement | null {\n if (!features || features.length === 0) return null;\n return el(\"span\", \"kit-gallery-features\", ...features.map((f) => el(\"span\", \"kit-gallery-feature\", f)));\n}\n\n/** One tile: the name with its badge, the blurb, the feature pills. 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; features?: string[] }, onClick: () => void): HTMLButtonElement {\n const tile = el(\"button\", { className: \"kit-gallery-tile\", onClick },\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),\n featurePills(item.features)) 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 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 panelFeatures = el(\"div\", \"kit-gallery-panel-features\");\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 panelName, panelFeatures, panelBlurb, panelDetail, panelPlay, 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 panelName.textContent = item.name;\n setText(panelBlurb, item.blurb);\n setText(panelDetail, item.detail);\n setText(panelPlay, item.play);\n const pills = featurePills(item.features);\n panelFeatures.replaceChildren(...(pills ? [pills] : []));\n panelFeatures.hidden = pills === null;\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","// ---------------------------------------------------------------------------\n// The welcome screen: what an app shows with no project open. Title, one line\n// under it, the actions (Open, New), an optional teaching line, optional\n// captioned groups (Storyletter's shipped examples), and the recents.\n//\n// Both apps drew this with the same twelve CSS rules under two class prefixes\n// (`.recent-name` / `.wel-recent-name`), four of them byte-identical. The CSS\n// here is Patterpad's; the captioned groups are Storyletter's, as an optional\n// slot, drawn as a plain list of rows (name + hint) rather than cards.\n//\n// The whole screen is a drag region (the window has no title bar to grab\n// here) and every control opts out. Styles ship as welcome.css.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\nimport { kitTile } from \"./kit-gallery.js\";\n\nexport interface WelcomeAction {\n label: string;\n onClick: () => void;\n /** The one accent button (Open a project…). */\n primary?: boolean;\n}\n\nexport interface WelcomeRecent {\n /** What the project calls itself, with the folder stem as the fallback. */\n name: string;\n /** Beside the name, because two projects may share a name and the folder\n * is how you tell them apart. */\n path: string;\n onOpen: () => void;\n}\n\nexport interface WelcomeGroupItem {\n name: string;\n /** What choosing it gets you, said before the click. */\n hint?: string;\n /** Tiled groups only: a word or two beside the name (\"Start here\"). */\n badge?: string;\n /** Tiled groups only: the kit's feature pills, as the gallery draws them. */\n features?: string[];\n onOpen: () => void;\n}\n\nexport interface WelcomeGroup {\n /** Sentence-case label text (\"Learn from a finished project\"); never an overline. */\n caption: string;\n /** The consequence of a click, said before the click (\"Each opens as your\n * own copy, in a folder you choose.\"). */\n note?: string;\n items: WelcomeGroupItem[];\n /** Draw the items as the kit gallery's tiles rather than rows (the kit\n * gallery brief, section 6: the welcome shows the kits and the examples as\n * the gallery shows them). Needs kit-gallery.css beside welcome.css. */\n tiles?: boolean;\n}\n\nexport interface WelcomeOptions {\n title: string;\n sub: string;\n actions: WelcomeAction[];\n recents: WelcomeRecent[];\n groups?: WelcomeGroup[];\n /** The teaching door: a line under the actions, quieter than a button\n * (\"New to Patter? Take the interactive tour\"). The app builds and styles\n * it; a text action in it is a link, not bare muted text. */\n tourLine?: HTMLElement;\n /** The recents caption. Default \"Recent\". */\n recentsCaption?: string;\n /** How many recents to show. Default 5. */\n maxRecents?: number;\n /** A sentence about why the last open failed, under everything. */\n error?: string;\n}\n\nexport interface Welcome {\n el: HTMLElement;\n /** Redraw the recents (the list changes as projects open and close). */\n setRecents(recents: WelcomeRecent[]): void;\n setError(error: string | undefined): void;\n}\n\n/** Mount the welcome into `host` (replacing its children). */\nexport function mountWelcome(host: HTMLElement, opts: WelcomeOptions): Welcome {\n const caption = (text: string): HTMLElement => el(\"h2\", \"welcome-caption\", text);\n const group = (label: string, ...body: (Node | null)[]): HTMLElement =>\n el(\"section\", \"welcome-group\", caption(label), ...body);\n\n const actions = el(\"div\", \"welcome-actions\");\n for (const a of opts.actions) {\n const b = el(\"button\", { className: `btn${a.primary ? \" primary\" : \"\"}`, text: a.label, onClick: a.onClick });\n b.type = \"button\";\n actions.append(b);\n }\n\n const groups = (opts.groups ?? []).map((g) => group(g.caption,\n g.note !== undefined ? el(\"p\", \"welcome-note\", g.note) : null,\n g.tiles\n ? el(\"div\", \"kit-gallery-grid welcome-tiles\", ...g.items.map((item) => kitTile({\n name: item.name, blurb: item.hint ?? \"\",\n ...(item.badge !== undefined ? { badge: item.badge } : {}),\n ...(item.features !== undefined ? { features: item.features } : {}),\n }, item.onOpen)))\n : el(\"div\", \"welcome-list\", ...g.items.map((item) => {\n const b = el(\"button\", { className: \"welcome-item\", onClick: item.onOpen },\n el(\"span\", \"welcome-item-name\", item.name),\n item.hint !== undefined ? el(\"span\", \"welcome-item-hint\", item.hint) : null);\n b.type = \"button\";\n return b;\n })),\n ));\n\n const recentsHost = el(\"div\", \"welcome-recents-slot\");\n const drawRecents = (recents: WelcomeRecent[]): void => {\n const shown = recents.slice(0, opts.maxRecents ?? 5);\n recentsHost.replaceChildren();\n if (shown.length === 0) return;\n recentsHost.append(group(opts.recentsCaption ?? \"Recent\",\n el(\"div\", \"welcome-recents\", ...shown.map((r) => {\n const b = el(\"button\", { className: \"welcome-recent\", onClick: r.onOpen },\n el(\"span\", \"welcome-recent-name\", r.name),\n el(\"span\", \"welcome-recent-path\", r.path));\n b.type = \"button\";\n return b;\n })),\n ));\n };\n drawRecents(opts.recents);\n\n const error = el(\"p\", \"welcome-error\");\n const setError = (text: string | undefined): void => {\n error.textContent = text ?? \"\";\n error.hidden = text === undefined || text === \"\";\n };\n setError(opts.error);\n\n const root = el(\"div\", \"welcome\", el(\"div\", \"welcome-card\",\n el(\"h1\", \"welcome-title\", opts.title),\n el(\"p\", \"welcome-sub\", opts.sub),\n actions,\n opts.tourLine ? el(\"div\", \"welcome-line\", opts.tourLine) : null,\n ...groups,\n recentsHost,\n error,\n ));\n host.replaceChildren(root);\n return { el: root, setRecents: drawRecents, setError };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/tooltip.ts","../src/dom.ts","../src/kit-gallery.ts","../src/welcome.ts"],"names":["el"],"mappings":"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","file":"welcome.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/** The sentence under a settings row: plain text, or nodes when it carries\n * markup (a `code` for a file name, a `b` for an option's name). */\nexport type FieldHint = string | Node | Array<string | Node>;\n\n/**\n * A captioned field, the family's settings row (the 2026-10 review, ruling C):\n * the caption on the left and the control inline beside it,\n * `<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. The same rule makes a control GROUP safe to caption: a field\n * with its own Choose… or Import… button beside it (`.shell-inline`) captions the field, and the button\n * stays a button.\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 *\n * With a `hint`, the row and the sentence under it come back together in a `.shell-field`, the hint as\n * the field's description (`aria-describedby`) rather than inside the label, where it would become part\n * of the field's NAME and be read out every time it takes focus. Without one, the row is exactly what it\n * always was.\n */\nexport function labelled(label: string, control: HTMLElement, hint?: FieldHint): 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 = uid();\n (w as HTMLLabelElement).htmlFor = target.id;\n }\n if (hint === undefined) return w;\n const note = el(\"p\", \"shell-fieldhint\");\n note.append(...(Array.isArray(hint) ? hint : [hint]));\n note.id = uid();\n if (target) {\n const by = target.getAttribute(\"aria-describedby\");\n target.setAttribute(\"aria-describedby\", by ? `${by} ${note.id}` : note.id);\n }\n return el(\"div\", \"shell-field\", w, note);\n}\n\nconst uid = (): string => `shell-f-${Math.random().toString(36).slice(2, 9)}`;\n\nexport interface LabelledToggleOptions {\n checked?: boolean;\n /** The sentence under the row: what turning it on does. */\n hint?: FieldHint;\n /** An option that only means something while the row above it is set (Patterpad's \"Embed source\n * language for debug\" under an IDs-only build): indented under that row with a rule, so it reads as\n * belonging to it rather than as a peer. */\n sub?: boolean;\n disabled?: boolean;\n /** Fired on every flip, with the new state. The input is handed back too, for a caller that reads it\n * on Save instead. */\n onChange?: (checked: boolean) => void;\n}\n\n/**\n * The settings toggle, `labelled`'s partner (ruling C): the caption on the left, the checkbox beside it\n * where any other row has its control, and the optional hint under the row. One shape in both apps, so\n * a switch in one app's settings looks like a switch in the other's. Clicking the caption flips it, as\n * the label is the checkbox's.\n *\n * Returns the outermost row (to append, hide, or indent) and the input (to read or listen to).\n */\nexport function labelledToggle(label: string, opts: LabelledToggleOptions = {}): { row: HTMLElement; input: HTMLInputElement } {\n const input = el(\"input\", \"shell-toggle-input\");\n input.type = \"checkbox\";\n input.checked = opts.checked ?? false;\n input.disabled = opts.disabled ?? false;\n const { onChange } = opts;\n if (onChange) input.addEventListener(\"change\", () => onChange(input.checked));\n const row = labelled(label, input, opts.hint);\n // On the caption-and-box line itself, whether or not a hint wraps it.\n (row.matches(\".shell-labelled\") ? row : row.querySelector(\".shell-labelled\"))?.classList.add(\"shell-toggle\");\n if (opts.sub) row.classList.add(\"shell-suboption\");\n return { row, input };\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 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 /** What it has, in a word or two each (\"Drawn map\", \"Copies\"): pills on the tile and in the\n * panel, so kits compare at a glance before any is chosen. The author's ask, 2026-09-27, in\n * place of pictures, which were ruled out the same day. */\n features?: 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}\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/** A kit's features as pills, or null when it lists none. */\nfunction featurePills(features: string[] | undefined): HTMLElement | null {\n if (!features || features.length === 0) return null;\n return el(\"span\", \"kit-gallery-features\", ...features.map((f) => el(\"span\", \"kit-gallery-feature\", f)));\n}\n\n/** One tile: the name with its badge, the blurb, the feature pills. 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; features?: string[] }, onClick: () => void): HTMLButtonElement {\n const tile = el(\"button\", { className: \"kit-gallery-tile\", onClick },\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),\n featurePills(item.features)) 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 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 panelFeatures = el(\"div\", \"kit-gallery-panel-features\");\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 panelName, panelFeatures, panelBlurb, panelDetail, panelPlay, 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 panelName.textContent = item.name;\n setText(panelBlurb, item.blurb);\n setText(panelDetail, item.detail);\n setText(panelPlay, item.play);\n const pills = featurePills(item.features);\n panelFeatures.replaceChildren(...(pills ? [pills] : []));\n panelFeatures.hidden = pills === null;\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","// ---------------------------------------------------------------------------\n// The welcome screen: what an app shows with no project open. Title, one line\n// under it, the actions (Open, New), an optional teaching line, optional\n// captioned groups (Storyletter's shipped examples), and the recents.\n//\n// Both apps drew this with the same twelve CSS rules under two class prefixes\n// (`.recent-name` / `.wel-recent-name`), four of them byte-identical. The CSS\n// here is Patterpad's; the captioned groups are Storyletter's, as an optional\n// slot, drawn as a plain list of rows (name + hint) rather than cards.\n//\n// The whole screen is a drag region (the window has no title bar to grab\n// here) and every control opts out. Styles ship as welcome.css.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\nimport { kitTile } from \"./kit-gallery.js\";\n\nexport interface WelcomeAction {\n label: string;\n onClick: () => void;\n /** The one accent button (Open a project…). */\n primary?: boolean;\n}\n\nexport interface WelcomeRecent {\n /** What the project calls itself, with the folder stem as the fallback. */\n name: string;\n /** Beside the name, because two projects may share a name and the folder\n * is how you tell them apart. */\n path: string;\n onOpen: () => void;\n}\n\nexport interface WelcomeGroupItem {\n name: string;\n /** What choosing it gets you, said before the click. */\n hint?: string;\n /** Tiled groups only: a word or two beside the name (\"Start here\"). */\n badge?: string;\n /** Tiled groups only: the kit's feature pills, as the gallery draws them. */\n features?: string[];\n onOpen: () => void;\n}\n\nexport interface WelcomeGroup {\n /** Sentence-case label text (\"Learn from a finished project\"); never an overline. */\n caption: string;\n /** The consequence of a click, said before the click (\"Each opens as your\n * own copy, in a folder you choose.\"). */\n note?: string;\n items: WelcomeGroupItem[];\n /** Draw the items as the kit gallery's tiles rather than rows (the kit\n * gallery brief, section 6: the welcome shows the kits and the examples as\n * the gallery shows them). Needs kit-gallery.css beside welcome.css. */\n tiles?: boolean;\n}\n\nexport interface WelcomeOptions {\n title: string;\n sub: string;\n actions: WelcomeAction[];\n recents: WelcomeRecent[];\n groups?: WelcomeGroup[];\n /** The teaching door: a line under the actions, quieter than a button\n * (\"New to Patter? Take the interactive tour\"). The app builds and styles\n * it; a text action in it is a link, not bare muted text. */\n tourLine?: HTMLElement;\n /** The recents caption. Default \"Recent\". */\n recentsCaption?: string;\n /** How many recents to show. Default 5. */\n maxRecents?: number;\n /** A sentence about why the last open failed, under everything. */\n error?: string;\n}\n\nexport interface Welcome {\n el: HTMLElement;\n /** Redraw the recents (the list changes as projects open and close). */\n setRecents(recents: WelcomeRecent[]): void;\n setError(error: string | undefined): void;\n}\n\n/** Mount the welcome into `host` (replacing its children). */\nexport function mountWelcome(host: HTMLElement, opts: WelcomeOptions): Welcome {\n const caption = (text: string): HTMLElement => el(\"h2\", \"welcome-caption\", text);\n const group = (label: string, ...body: (Node | null)[]): HTMLElement =>\n el(\"section\", \"welcome-group\", caption(label), ...body);\n\n const actions = el(\"div\", \"welcome-actions\");\n for (const a of opts.actions) {\n const b = el(\"button\", { className: `btn${a.primary ? \" primary\" : \"\"}`, text: a.label, onClick: a.onClick });\n b.type = \"button\";\n actions.append(b);\n }\n\n const groups = (opts.groups ?? []).map((g) => group(g.caption,\n g.note !== undefined ? el(\"p\", \"welcome-note\", g.note) : null,\n g.tiles\n ? el(\"div\", \"kit-gallery-grid welcome-tiles\", ...g.items.map((item) => kitTile({\n name: item.name, blurb: item.hint ?? \"\",\n ...(item.badge !== undefined ? { badge: item.badge } : {}),\n ...(item.features !== undefined ? { features: item.features } : {}),\n }, item.onOpen)))\n : el(\"div\", \"welcome-list\", ...g.items.map((item) => {\n const b = el(\"button\", { className: \"welcome-item\", onClick: item.onOpen },\n el(\"span\", \"welcome-item-name\", item.name),\n item.hint !== undefined ? el(\"span\", \"welcome-item-hint\", item.hint) : null);\n b.type = \"button\";\n return b;\n })),\n ));\n\n const recentsHost = el(\"div\", \"welcome-recents-slot\");\n const drawRecents = (recents: WelcomeRecent[]): void => {\n const shown = recents.slice(0, opts.maxRecents ?? 5);\n recentsHost.replaceChildren();\n if (shown.length === 0) return;\n recentsHost.append(group(opts.recentsCaption ?? \"Recent\",\n el(\"div\", \"welcome-recents\", ...shown.map((r) => {\n const b = el(\"button\", { className: \"welcome-recent\", onClick: r.onOpen },\n el(\"span\", \"welcome-recent-name\", r.name),\n el(\"span\", \"welcome-recent-path\", r.path));\n b.type = \"button\";\n return b;\n })),\n ));\n };\n drawRecents(opts.recents);\n\n const error = el(\"p\", \"welcome-error\");\n const setError = (text: string | undefined): void => {\n error.textContent = text ?? \"\";\n error.hidden = text === undefined || text === \"\";\n };\n setError(opts.error);\n\n const root = el(\"div\", \"welcome\", el(\"div\", \"welcome-card\",\n el(\"h1\", \"welcome-title\", opts.title),\n el(\"p\", \"welcome-sub\", opts.sub),\n actions,\n opts.tourLine ? el(\"div\", \"welcome-line\", opts.tourLine) : null,\n ...groups,\n recentsHost,\n error,\n ));\n host.replaceChildren(root);\n return { el: root, setRecents: drawRecents, setError };\n}\n"]}
|
package/dist/welcome.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { mountWelcome } from './chunk-
|
|
2
|
-
import './chunk-
|
|
3
|
-
import './chunk-
|
|
4
|
-
import './chunk-
|
|
1
|
+
export { mountWelcome } from './chunk-IK3LWBQL.js';
|
|
2
|
+
import './chunk-FPXEQKWM.js';
|
|
3
|
+
import './chunk-XR4DRDMA.js';
|
|
4
|
+
import './chunk-QLVBKME7.js';
|
|
5
5
|
import './chunk-IU7EY6BI.js';
|
|
6
6
|
//# sourceMappingURL=welcome.js.map
|
|
7
7
|
//# sourceMappingURL=welcome.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wildwinter/app-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": "The shared shell for the editor family (Patterpad, Storylet Studio): the app-agnostic design-token grammar, the per-entity colour hash, and the vanilla-TS DOM primitive. Two palettes, one grammar - each app supplies its own palette/fonts over these tokens.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/menu.ts"],"names":[],"mappings":"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","file":"chunk-2ECJUTSX.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Shell-owned menu naming: the labels + accelerators for the pane-frame menu\n// items, single-sourced so every app in the family names them identically\n// (and a rename here flows to all of them). Pure data - no electron, no DOM -\n// so a main-process menu builder can import it. Each app builds its own\n// MenuItem from these and wires the click to its shell controller.\n// ---------------------------------------------------------------------------\n\nimport type { RecentProject } from \"./app-store.js\";\n\nexport interface MenuLabel {\n label: string;\n /** Electron accelerator string, when the item has one. */\n accelerator?: string;\n}\n\n/**\n * The Edit menu items the family shares.\n *\n * Undo and Redo are here and the MECHANISM is not, which is the whole finding\n * about undo: Patterpad's is ProseMirror's document history, Storyletter's is a\n * replay of file bytes, and those have nothing in common but this. See\n * `design/shared-shell.md`.\n *\n * The rule both apps discovered separately, and the reason these are spelled\n * out rather than left as Electron's `role: \"undo\"`: the native role runs a DOM\n * text-field undo, which knows nothing about either app's model and will\n * silently undo the wrong thing.\n */\nexport const EDIT_MENU: {\n undo: MenuLabel;\n redo: MenuLabel;\n duplicate: MenuLabel;\n find: MenuLabel;\n /** Replace is Find's second tab, on the platform's key for it. */\n replace: { label: string; acceleratorMac: string; acceleratorOther: string };\n} = {\n undo: { label: \"Undo\", accelerator: \"CmdOrCtrl+Z\" },\n redo: { label: \"Redo\", accelerator: \"Shift+CmdOrCtrl+Z\" },\n duplicate: { label: \"Duplicate\", accelerator: \"CmdOrCtrl+D\" },\n find: { label: \"Find…\", accelerator: \"CmdOrCtrl+F\" },\n replace: { label: \"Replace…\", acceleratorMac: \"Cmd+Alt+F\", acceleratorOther: \"Ctrl+H\" },\n};\n\n/**\n * Back / Forward: navigation HISTORY, the other axis from Up a Level's\n * hierarchy. One string cannot serve both platforms - the mac pair is Xcode's\n * (Cmd+[ is already Up a Level in this family, so the browser pair is taken),\n * and the Alt pair is what every Windows browser and Explorer trained - so the\n * menu builder picks by platform.\n */\nexport const GO_MENU: {\n back: { label: string; acceleratorMac: string; acceleratorOther: string };\n forward: { label: string; acceleratorMac: string; acceleratorOther: string };\n} = {\n back: { label: \"Back\", acceleratorMac: \"Ctrl+Cmd+Left\", acceleratorOther: \"Alt+Left\" },\n forward: { label: \"Forward\", acceleratorMac: \"Ctrl+Cmd+Right\", acceleratorOther: \"Alt+Right\" },\n};\n\n/** The pane-frame menu items (a View menu, conventionally). */\nexport const PANE_MENU: {\n showNav: MenuLabel;\n showInspector: MenuLabel;\n resetView: MenuLabel;\n} = {\n showNav: { label: \"Show Navigator\", accelerator: \"CmdOrCtrl+1\" },\n showInspector: { label: \"Show Inspector\", accelerator: \"CmdOrCtrl+2\" },\n resetView: { label: \"Reset View\" },\n};\n\n/**\n * The Help menu items the family shares, and the one platform rule that goes\n * with them.\n *\n * Lifted from Patterpad, which is the only app that has ever had these: a\n * second app writing its own updater or About is pure waste (`design/\n * shared-shell.md`), and a suite is recognised by finding Check for Updates\n * where you left it. Storyletter will grow both; until it does, these labels\n * are the agreement rather than a description of two implementations.\n *\n * PLACEMENT, which differs by platform and is easy to get wrong:\n * - macOS keeps About in the APP menu (the one named after the product), and\n * Help carries documentation + Check for Updates only.\n * - Windows and Linux have no app menu, so About goes at the foot of Help,\n * after a separator. That is the conventional home there.\n */\nexport const HELP_MENU: {\n checkForUpdates: MenuLabel;\n} = {\n checkForUpdates: { label: \"Check for Updates\\u2026\" },\n};\n\n/** The app-menu items the family shares (macOS; the identity item also belongs in File elsewhere). */\nexport const APP_MENU: {\n userInfo: MenuLabel;\n} = {\n /** Name + optional email, used to sign edits and comments. One label across the suite. */\n userInfo: { label: \"User Information\\u2026\" },\n};\n\n/** What an app must tell the shell to build its named menu items. */\nexport interface MenuNaming {\n /** The product, as it appears in menus: \"Patterpad\", \"Storyletter\". */\n appName: string;\n /** The product's own documentation URL. Omit while an app has no docs yet and\n * the item is rendered disabled rather than opening a broken link. */\n docsUrl?: string;\n /** The suite's documentation home, shared by every app in the family. */\n suiteDocsUrl?: string;\n /** The suite, as it appears in \"<suite> Documentation Home\". Defaults to \"Patter\". */\n suiteName?: string;\n}\n\n/** A named menu item: the label, plus the URL it opens (absent = nothing wired yet). */\nexport interface NamedMenuItem extends MenuLabel {\n url?: string;\n /** False when the app has not supplied a URL, so the builder can render it\n * disabled instead of opening nothing. A placeholder, honestly labelled. */\n ready: boolean;\n}\n\n/**\n * The menu labels that carry a product or suite name, so no app spells its own\n * About or Documentation item differently from the rest of the family.\n *\n * An app with no documentation site yet still gets the ITEM, marked\n * `ready: false`, so the menu has its family shape from day one and wiring the\n * URL later is a one-line change rather than a menu redesign.\n */\nexport function namedMenuItems(naming: MenuNaming): {\n about: NamedMenuItem;\n docs: NamedMenuItem;\n suiteDocs: NamedMenuItem;\n} {\n const suite = naming.suiteName ?? \"Patter\";\n return {\n about: { label: `About ${naming.appName}`, ready: true },\n docs: {\n label: `${naming.appName} Documentation`,\n ...(naming.docsUrl ? { url: naming.docsUrl } : {}),\n ready: naming.docsUrl !== undefined,\n },\n suiteDocs: {\n label: `${suite} Documentation Home`,\n ...(naming.suiteDocsUrl ? { url: naming.suiteDocsUrl } : {}),\n ready: naming.suiteDocsUrl !== undefined,\n },\n };\n}\n\n// --- the rest of the spine (ui-review-2026-09, finding 12) -----------------------\n// Each app used roughly half of what is above and hand-typed the other half,\n// and the copies had already drifted (Review Feedback's accelerator spelled\n// two ways; \"No Recent Projects\" vs \"No recent projects\"). Labels are Title\n// Case, as the platform's menus are; an item that opens something ends in the\n// command ellipsis.\n\n/** The File menu items the family shares. */\nexport const FILE_MENU: {\n newProject: MenuLabel;\n openProject: MenuLabel;\n openRecent: MenuLabel;\n /** The empty Open Recent submenu's one disabled item. */\n noRecents: MenuLabel;\n clearRecents: MenuLabel;\n closeProject: MenuLabel;\n save: MenuLabel;\n saveAs: MenuLabel;\n projectSettings: MenuLabel;\n} = {\n newProject: { label: \"New Project…\", accelerator: \"CmdOrCtrl+N\" },\n openProject: { label: \"Open Project…\", accelerator: \"CmdOrCtrl+O\" },\n openRecent: { label: \"Open Recent\" },\n noRecents: { label: \"No Recent Projects\" },\n clearRecents: { label: \"Clear Recents\" },\n closeProject: { label: \"Close Project\" },\n save: { label: \"Save\", accelerator: \"CmdOrCtrl+S\" },\n saveAs: { label: \"Save As…\", accelerator: \"Shift+CmdOrCtrl+S\" },\n projectSettings: { label: \"Project Settings…\", accelerator: \"CmdOrCtrl+,\" },\n};\n\n/**\n * The Play menu's shared item. The play SURFACE itself (Patterpad's \"Play\n * Scene\" on Cmd+P, Storyletter's \"The Board\" on Cmd+T) is app-named and\n * app-keyed on purpose: the two are not the same act, so the family reserves\n * neither key.\n */\nexport const PLAY_MENU: {\n liveLink: MenuLabel;\n} = {\n liveLink: { label: \"Live Link\" },\n};\n\n/** The Review menu: examining what you have built rather than building or\n * playing it. Review Feedback's accelerator is spelled ONCE, here. */\nexport const REVIEW_MENU: {\n reviewFeedback: MenuLabel;\n nextFeedback: MenuLabel;\n previousFeedback: MenuLabel;\n coverageTest: MenuLabel;\n findPropertyUsage: MenuLabel;\n showResolvedComments: MenuLabel;\n} = {\n reviewFeedback: { label: \"Review Feedback\", accelerator: \"CmdOrCtrl+Shift+R\" },\n nextFeedback: { label: \"Next Feedback\", accelerator: \"F8\" },\n previousFeedback: { label: \"Previous Feedback\", accelerator: \"Shift+F8\" },\n coverageTest: { label: \"Coverage Test…\", accelerator: \"Shift+CmdOrCtrl+C\" },\n findPropertyUsage: { label: \"Find Property Usage…\" },\n showResolvedComments: { label: \"Show Resolved Comments\" },\n};\n\n/** The Publish menu: everything that turns the project into something you\n * hand to others (\"Publish\", never \"Export\"). */\nexport const PUBLISH_MENU: {\n playableHtml: MenuLabel;\n bundle: MenuLabel;\n autoRebuild: MenuLabel;\n} = {\n playableHtml: { label: \"Publish Playable HTML…\" },\n bundle: { label: \"Publish Bundle\", accelerator: \"Shift+CmdOrCtrl+B\" },\n autoRebuild: { label: \"Auto Rebuild\" },\n};\n\n/** The View menu items beyond the pane toggles (PANE_MENU) and history\n * (GO_MENU). Cmd+[ is family-reserved for Up a Level, which is why Back\n * takes Xcode's pair on the mac. */\nexport const VIEW_MENU: {\n projectOverview: MenuLabel;\n upALevel: MenuLabel;\n colourTheme: MenuLabel;\n} = {\n projectOverview: { label: \"Project Overview\" },\n upALevel: { label: \"Up a Level\", accelerator: \"CmdOrCtrl+[\" },\n colourTheme: { label: \"Colour Theme\" },\n};\n\n/** A path with the home directory as `~`, so a recents entry stays readable\n * at menu width. `home` is `app.getPath(\"home\")`; this file stays\n * electron-free. */\nexport function tildePath(path: string, home: string): string {\n if (path === home) return \"~\";\n const sep = home.includes(\"\\\\\") ? \"\\\\\" : \"/\";\n return path.startsWith(home + sep) ? `~${path.slice(home.length)}` : path;\n}\n\n/** As much of Electron's MenuItemConstructorOptions as a recents entry needs;\n * structurally assignable to it. */\nexport interface RecentsMenuItem {\n label?: string;\n sublabel?: string;\n toolTip?: string;\n enabled?: boolean;\n type?: \"separator\";\n click?: () => void;\n}\n\nexport interface RecentsSubmenuOptions {\n onOpen: (path: string) => void;\n /** Offered after a separator when there is anything to clear. */\n onClear?: () => void;\n /** `app.getPath(\"home\")`, for the `~` abbreviation. */\n home: string;\n /** Defaults to process.platform; a seam for the tests. */\n platform?: string;\n}\n\n/**\n * The Open Recent submenu: the fifteen lines both apps had.\n *\n * Shows WHERE each recent lives. macOS renders `sublabel` as a dimmed second\n * line and `toolTip` on hover; native menus elsewhere render neither, so\n * there the path is folded into the label itself. Empty, it is the one\n * disabled \"No Recent Projects\" item.\n */\nexport function recentsSubmenu(recents: RecentProject[], opts: RecentsSubmenuOptions): RecentsMenuItem[] {\n const isMac = (opts.platform ?? process.platform) === \"darwin\";\n if (recents.length === 0) return [{ label: FILE_MENU.noRecents.label, enabled: false }];\n const items: RecentsMenuItem[] = recents.map((r) => {\n const shown = tildePath(r.path, opts.home);\n const name = r.name ?? r.path.split(/[\\\\/]/).filter(Boolean).pop() ?? r.path;\n return {\n label: isMac ? name : `${name} (${shown})`,\n sublabel: shown,\n toolTip: r.path,\n click: () => opts.onOpen(r.path),\n };\n });\n if (opts.onClear) items.push({ type: \"separator\" }, { label: FILE_MENU.clearRecents.label, click: opts.onClear });\n return items;\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/util.ts"],"names":[],"mappings":"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","file":"chunk-P6WLAEKZ.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The small idioms both apps wrote by hand and never gave a name: the plural\n// suffix (about forty sites), the debounce (eight copies), \"is the focus in a\n// field\" (four copies that disagreed about <select>), the count with grouping\n// separators, clipboard copy with the `.copied` revert (three timings), and\n// relative time (two wordings). One answer each, here.\n//\n// DOM-free except where the idiom is about the DOM (isEditableTarget,\n// copyWithFeedback), so main-process code can import the rest.\n// ---------------------------------------------------------------------------\n\n/** \"1 link\" / \"3 links\". The plural form defaults to `noun + \"s\"`; pass the\n * irregular one (\"entry\", \"entries\") when that is wrong. */\nexport function plural(n: number, noun: string, pluralForm = `${noun}s`): string {\n return `${n} ${n === 1 ? noun : pluralForm}`;\n}\n\nexport interface Debounced<A extends unknown[]> {\n (...args: A): void;\n /** Drop the pending call. */\n cancel(): void;\n /** Run the pending call now, if there is one. */\n flush(): void;\n}\n\n/** Trailing-edge debounce: the call runs `ms` after the LAST call. */\nexport function debounce<A extends unknown[]>(fn: (...args: A) => void, ms: number): Debounced<A> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let pending: A | undefined;\n const run = (): void => {\n timer = undefined;\n if (pending === undefined) return;\n const args = pending;\n pending = undefined;\n fn(...args);\n };\n const debounced = ((...args: A): void => {\n pending = args;\n if (timer !== undefined) clearTimeout(timer);\n timer = setTimeout(run, ms);\n }) as Debounced<A>;\n debounced.cancel = () => { if (timer !== undefined) clearTimeout(timer); timer = undefined; pending = undefined; };\n debounced.flush = () => { if (timer !== undefined) clearTimeout(timer); run(); };\n return debounced;\n}\n\n/**\n * Is the keyboard's focus in a field? The one agreed answer: an input, a\n * textarea, a select, or anything contenteditable (the attribute is read as\n * well as `isContentEditable`, which jsdom does not implement). A window-level\n * shortcut checks this before acting, so a bare letter typed into a field\n * stays a letter.\n */\nexport function isEditableTarget(t: EventTarget | null | undefined): boolean {\n if (!(t instanceof Element)) return false;\n if (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t instanceof HTMLSelectElement) return true;\n if (t instanceof HTMLElement && t.isContentEditable === true) return true;\n const attr = t.getAttribute(\"contenteditable\");\n return attr !== null && attr !== \"false\";\n}\n\n/** A count for reading: grouping separators in the user's locale. */\nexport function formatCount(n: number): string {\n return n.toLocaleString();\n}\n\nconst copyTimers = new WeakMap<Element, ReturnType<typeof setTimeout>>();\n\n/**\n * Copy `text` to the clipboard and light `el` with `.copied` for `ms` (the\n * family's 1000). A second click restarts the timer rather than stacking a\n * second revert under the first. Resolves when the clipboard write has.\n */\nexport function copyWithFeedback(el: Element, text: string, ms = 1000): Promise<void> {\n const write = typeof navigator !== \"undefined\" && navigator.clipboard\n ? navigator.clipboard.writeText(text).catch(() => {})\n : Promise.resolve();\n el.classList.add(\"copied\");\n const prior = copyTimers.get(el);\n if (prior !== undefined) clearTimeout(prior);\n copyTimers.set(el, setTimeout(() => { el.classList.remove(\"copied\"); copyTimers.delete(el); }, ms));\n return write;\n}\n\n/**\n * How long ago, in one wording: \"just now\", \"4 minutes ago\", \"an hour ago\",\n * \"3 hours ago\", \"yesterday\", \"5 days ago\", and past a week the date itself.\n * Coarse on purpose: the question a reader has is whether the thing is from\n * this sitting, not how many seconds ago it was. A future date is \"just now\".\n */\nexport function relativeTime(date: Date | number | string, now: number = Date.now()): string {\n const then = date instanceof Date ? date.getTime() : new Date(date).getTime();\n if (!Number.isFinite(then)) return \"\";\n const mins = Math.floor((now - then) / 60000);\n if (mins < 1) return \"just now\";\n if (mins < 60) return `${plural(mins, \"minute\")} ago`;\n const hours = Math.floor(mins / 60);\n if (hours === 1) return \"an hour ago\";\n if (hours < 24) return `${hours} hours ago`;\n const days = Math.floor(hours / 24);\n if (days === 1) return \"yesterday\";\n if (days < 7) return `${days} days ago`;\n return new Date(then).toLocaleDateString(undefined, { day: \"numeric\", month: \"short\", year: \"numeric\" });\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/settings.ts","../src/property-names.ts","../src/property-name-field.ts","../src/property-list.ts"],"names":[],"mappings":"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","file":"chunk-R3NXPHRZ.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The shared settings surface: a native <dialog> with a left-rail of grouped\n// tabs and one panel per section (mountSettingsDialog), plus the list-editing\n// core those sections lean on (expandableRow, focusNewRow, dupGuard). The app\n// supplies the sections; each holds its own state and reports the first invalid\n// control so the Save gate can block + jump. Framework here, content in the app.\n//\n// Lifted from Patterpad's Project Settings (settings-list.ts + the inlined\n// dialog logic in its renderer), with neutral class names. Styling ships as\n// settings.css.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\n\n// --- list-editing core --------------------------------------------------------\n\n/** A compact list row: a single `.set-rowline` (inline controls) plus, when\n * `details` are given, a disclosure that reveals a `.set-details` panel below,\n * so a list scans like a table with secondary fields tucked away. `name` is\n * what the row declares (a property's name, a field's), stamped on it as\n * `data-name` so `revealRow` can land on it. */\nexport function expandableRow(opts: { line: HTMLElement[]; details?: HTMLElement[]; name?: string }): HTMLElement {\n const row = el(\"div\", \"set-row\");\n if (opts.name !== undefined) row.dataset.name = opts.name;\n const line = el(\"div\", \"set-rowline\");\n if (opts.details && opts.details.length) {\n const details = el(\"div\", \"set-details\"); details.hidden = true;\n for (const d of opts.details) details.append(d);\n // The triangle is drawn by CSS off `.open` (a text glyph stays hairline-thin).\n const toggle = el(\"button\", \"set-expand\"); toggle.type = \"button\";\n // `data-tip`, not `title`: this package ships a themed tooltip and nothing\n // else in it raises an OS rollover. A lone `title` here would be the one\n // place a host's chrome broke character, and on the platform's own delay.\n toggle.dataset.tip = \"More\"; toggle.setAttribute(\"aria-label\", \"More\");\n toggle.setAttribute(\"aria-expanded\", \"false\");\n toggle.addEventListener(\"click\", () => {\n const open = details.hidden;\n details.hidden = !open;\n toggle.setAttribute(\"aria-expanded\", String(open));\n toggle.classList.toggle(\"open\", open);\n });\n line.append(toggle);\n opts.line.forEach((c) => line.append(c));\n row.append(line, details);\n } else {\n opts.line.forEach((c) => line.append(c));\n row.append(line);\n }\n return row;\n}\n\n/**\n * Land on one row of a list by the `name` it was built with: open its details\n * (an enum's values sit behind the disclosure, and they are usually what the\n * jump was for), bring it to the middle of its scroller, and light it for a\n * moment (`.set-row.landed` in settings.css). \"Go to definition\" in both apps\n * used to open the page a property is declared on and stop there, with the\n * declaration below the fold and nothing marking it (Storyletter, 2026-09-14).\n *\n * Returns false when no row carries the name, so a host whose page fills in\n * asynchronously can ask again on a later frame; asking again is the host's,\n * since only the host knows what opens on its own time. The row is matched by\n * reading the stamp rather than by an attribute selector, so a name needs no\n * escaping.\n */\nexport function revealRow(within: ParentNode, name: string): boolean {\n const row = [...within.querySelectorAll<HTMLElement>(\".set-row\")].find((r) => r.dataset.name === name);\n if (!row) return false;\n const toggle = row.querySelector<HTMLButtonElement>(\".set-rowline > .set-expand\");\n if (toggle && toggle.getAttribute(\"aria-expanded\") !== \"true\") toggle.click();\n const still = typeof matchMedia === \"function\" && matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n row.scrollIntoView({ block: \"center\", behavior: still ? \"auto\" : \"smooth\" });\n // Restarted, so landing on the same row twice lights it twice.\n row.classList.remove(\"landed\");\n void row.offsetWidth;\n row.classList.add(\"landed\");\n row.addEventListener(\"animationend\", () => row.classList.remove(\"landed\"), { once: true });\n return true;\n}\n\n/**\n * `revealRow`, retried over the next frames until the row exists or the tries\n * run out: for a page that fills in on its own time after a navigation. Both\n * apps hand-rolled this as `landOn` with the same twelve tries\n * (ui-review-2026-09, finding 29). Resolves true when it landed, false when it\n * gave up, and giving up is quiet: a name no page shows as a row is not a fault.\n */\nexport function revealRowWhenReady(within: ParentNode, name: string, tries = 12): Promise<boolean> {\n return new Promise((resolve) => {\n const attempt = (left: number): void => {\n if (revealRow(within, name)) { resolve(true); return; }\n if (left <= 0) { resolve(false); return; }\n requestAnimationFrame(() => attempt(left - 1));\n };\n attempt(tries);\n });\n}\n\n/** After a \"+ Add\" re-renders a list, bring the new (last) row into view and\n * focus its name input (`.set-name`, else the first input). */\nexport function focusNewRow(listEl: HTMLElement | null | undefined): void {\n const last = listEl?.lastElementChild;\n if (!(last instanceof HTMLElement)) return;\n last.scrollIntoView({ block: \"nearest\" });\n const name = last.querySelector<HTMLInputElement>(\"input.set-name\") ?? last.querySelector<HTMLInputElement>(\"input\");\n name?.focus();\n}\n\nexport interface DupGuard {\n /** Forget the previous render's inputs (call at the top of render). */\n reset(): void;\n /** Register a name input; `key` returns the comparison string (default the\n * input's value). Re-checks live on input. `also` = extra inputs that change the key. */\n track(input: HTMLInputElement, key?: () => string, also?: HTMLInputElement[]): void;\n /** Re-paint duplicates; true if any two non-blank keys collide. */\n check(): boolean;\n /** The first input currently flagged (after a fresh check), or null. */\n firstDuplicate(): HTMLInputElement | null;\n}\n\nconst DUP_MESSAGE = \"Another row already has this name.\";\n\n/** Case-insensitive duplicate-name detection: paints clashing inputs `.invalid`\n * and lets a Save gate block + jump via `firstDuplicate()`. */\nexport function dupGuard(): DupGuard {\n const items: Array<{ input: HTMLInputElement; key: () => string }> = [];\n const check = (): boolean => {\n const counts = new Map<string, number>();\n for (const it of items) { const k = it.key().trim().toLowerCase(); if (k) counts.set(k, (counts.get(k) ?? 0) + 1); }\n let any = false;\n for (const it of items) {\n const k = it.key().trim().toLowerCase();\n const dup = !!k && (counts.get(k) ?? 0) > 1;\n it.input.classList.toggle(\"invalid\", dup);\n // The reason rides `data-tip` (the themed rollover, and what the Save\n // gate's message channel reads), never `title`.\n if (dup) { it.input.dataset[\"tip\"] = DUP_MESSAGE; any = true; }\n else if (it.input.dataset[\"tip\"] === DUP_MESSAGE) delete it.input.dataset[\"tip\"];\n }\n return any;\n };\n return {\n reset() { items.length = 0; },\n track(input, key = () => input.value, also) {\n items.push({ input, key });\n input.addEventListener(\"input\", check);\n for (const a of also ?? []) a.addEventListener(\"input\", check);\n },\n check,\n firstDuplicate() { check(); return items.find((it) => it.input.classList.contains(\"invalid\"))?.input ?? null; },\n };\n}\n\n// --- the settings dialog ------------------------------------------------------\n\nexport interface SettingsSectionHandle {\n /** The first invalid control to jump to, blocking Save (e.g. a duplicate\n * name). Return `{ el, message }` to say WHICH fault in the error line;\n * a bare element says it through its own `data-tip` (the dup guard's, a\n * property-name field's), and the line falls back to a generic sentence\n * when it has neither. */\n firstInvalid?(): HTMLElement | { el: HTMLElement; message?: string } | null;\n}\n\nexport interface SettingsSection {\n id: string;\n label: string;\n /** Optional left-rail group heading above this tab. */\n group?: string;\n /** Build the section body into `host`; called fresh on each open so it reads\n * the current data. Return a handle (for the Save gate). */\n mount(host: HTMLElement): SettingsSectionHandle;\n /** Why this tab cannot be used right now, or null when it can. A reason\n * renders the tab dimmed and inert with the reason as its rollover\n * (Patterpad's Audio tab while the project is not voiced). Read on open\n * and on `refreshTabs()`, so a control inside the dialog can flip it. */\n disabled?: () => string | null;\n}\n\nexport interface SettingsDialogOptions {\n title: string;\n sections: SettingsSection[];\n /** Fired when Save is clicked and every section validates. The app reads its\n * own section state and persists. May be async; the dialog stays open until\n * it resolves, then closes. */\n onSave: () => void | Promise<void>;\n}\n\nexport interface SettingsDialog {\n /** Open (optionally on a given tab id). Sections re-mount from current data. */\n open(tabId?: string): void;\n /** Re-read every section's `disabled()`; a tab that has just become\n * disabled while showing hands over to the first usable one. */\n refreshTabs(): void;\n destroy(): void;\n}\n\n/** Build a settings dialog. The frame owns the modal, the tab rail, save-on-Save\n * (with a validation gate), and Cancel; the sections own their content + state. */\nexport function mountSettingsDialog(opts: SettingsDialogOptions): SettingsDialog {\n const dialog = el(\"dialog\", \"settings-dialog\");\n const form = el(\"form\", \"settings-form\"); form.method = \"dialog\";\n const tabs = el(\"nav\", \"settings-tabs\");\n const panels = el(\"div\", \"settings-panels\");\n const error = el(\"p\", \"settings-error\"); error.hidden = true;\n const cancel = el(\"button\", \"settings-cancel\", \"Cancel\"); cancel.type = \"button\"; cancel.value = \"cancel\";\n const save = el(\"button\", \"settings-save primary\", \"Save\"); save.value = \"save\";\n\n const panelFor = new Map<string, HTMLElement>();\n const tabFor = new Map<string, HTMLButtonElement>();\n const handles = new Map<string, SettingsSectionHandle>();\n let active = opts.sections[0]?.id ?? \"\";\n\n let lastGroup: string | undefined;\n for (const s of opts.sections) {\n if (s.group && s.group !== lastGroup) { tabs.append(el(\"div\", \"settings-group\", s.group)); lastGroup = s.group; }\n const tab = el(\"button\", \"settings-tab\", s.label); tab.type = \"button\"; tab.dataset[\"tab\"] = s.id;\n // A disabled tab is inert by class, not by the `disabled` attribute: a\n // disabled button swallows the pointer, and the reason has to be readable\n // on hover.\n tab.addEventListener(\"click\", () => { if (!tab.classList.contains(\"is-disabled\")) showTab(s.id); });\n tabs.append(tab); tabFor.set(s.id, tab);\n const panel = el(\"section\", \"settings-panel\"); panel.dataset[\"panel\"] = s.id; panel.hidden = true;\n panels.append(panel); panelFor.set(s.id, panel);\n }\n\n function showTab(id: string): void {\n active = id;\n for (const [sid, t] of tabFor) t.classList.toggle(\"active\", sid === id);\n for (const [sid, p] of panelFor) p.hidden = sid !== id;\n }\n\n const usable = (s: SettingsSection): boolean => !tabFor.get(s.id)?.classList.contains(\"is-disabled\");\n function refreshTabs(): void {\n for (const s of opts.sections) {\n const tab = tabFor.get(s.id)!;\n const reason = s.disabled?.() ?? null;\n tab.classList.toggle(\"is-disabled\", reason !== null);\n if (reason !== null) { tab.dataset[\"tip\"] = reason; tab.setAttribute(\"aria-disabled\", \"true\"); }\n else { delete tab.dataset[\"tip\"]; tab.removeAttribute(\"aria-disabled\"); }\n }\n if (!opts.sections.some((s) => s.id === active && usable(s))) {\n const first = opts.sections.find(usable);\n if (first) showTab(first.id);\n }\n }\n\n form.append(\n el(\"h2\", \"settings-title\", opts.title),\n el(\"div\", \"settings-body\", tabs, panels),\n el(\"div\", \"settings-actions\", error, cancel, save),\n );\n dialog.append(form);\n document.body.append(dialog);\n\n // Enter should not submit while editing a field; only the Save button does.\n form.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\" && (e.target as HTMLElement)?.tagName === \"INPUT\") e.preventDefault();\n });\n\n cancel.addEventListener(\"click\", () => dialog.close(\"cancel\"));\n save.addEventListener(\"click\", (e) => {\n e.preventDefault();\n error.hidden = true;\n for (const s of opts.sections) {\n const found = handles.get(s.id)?.firstInvalid?.();\n if (!found) continue;\n const bad = found instanceof HTMLElement ? found : found.el;\n const message = (found instanceof HTMLElement ? undefined : found.message) ?? bad.dataset[\"tip\"];\n showTab(s.id);\n bad.focus();\n // The field's own rollover says which fault it is, so the line repeats it\n // rather than inventing a summary; the generic sentence is the fallback.\n error.textContent = message || \"Fix the highlighted fields first.\";\n error.hidden = false;\n return;\n }\n void (async () => { await opts.onSave(); dialog.close(\"save\"); })();\n });\n\n return {\n open(tabId) {\n handles.clear();\n for (const s of opts.sections) {\n const panel = panelFor.get(s.id)!;\n panel.replaceChildren();\n handles.set(s.id, s.mount(panel));\n }\n error.hidden = true;\n showTab(tabId ?? active ?? opts.sections[0]?.id ?? \"\");\n refreshTabs(); // a disabled tab asked for by id hands over to the first usable one\n dialog.showModal();\n },\n refreshTabs,\n destroy() { dialog.remove(); },\n };\n}\n","// ---------------------------------------------------------------------------\n// Property names: what an author may call a value the story reads and writes.\n//\n// This belongs to the shell for the same reason gameIds do (see ids.ts): it\n// follows from the SHAPE these apps share - a project of data files, authored in\n// an editor, compiled for something else to run - rather than from any one\n// subject. Nothing here needs the words card, beat, hand or scene.\n//\n// UNLIKE gameIds, the rule is not a house style. It is dictated by the\n// expression language both families embed (`@wildwinter/expr`), and every clause\n// below is a failure observed by compiling one:\n//\n// parser.ts tokenises an identifier as /[a-zA-Z_][a-zA-Z0-9_]*/ and then\n// FOLDS it: `name: propTok.value.toLowerCase()`.\n//\n// @patter.is_night -> [\"sv\",\"patter\",\"is_night\"] reachable\n// @patter.isNight -> [\"sv\",\"patter\",\"isnight\"] folded: a declaration\n// spelled `isNight` is\n// one nothing can reach\n// @patter.is-night -> [\"bin\",\"-\",[\"sv\",\"patter\",\"is\"], NOT an error: it is\n// [\"s\",\"night\"]] SUBTRACTION. A hyphen\n// silently compiles to\n// a different meaning\n// @patter.is night -> parse error\n// @patter.9lives -> parse error (a name cannot start with a digit)\n// @patter.not -> parse error (`true false and or not` are keywords)\n//\n// The hyphen line is why this is worth a shared rule rather than a per-app\n// check: every other violation is loud, and that one quietly means something\n// else. It is also why a property name is NOT a gameId with different spelling -\n// a gameId is built out of hyphens, and here a hyphen is an operator.\n//\n// DEPENDENCY-FREE on purpose, ids.ts's reasoning verbatim: the apps' compilers,\n// CLIs and embedded runtimes need these rules far from any UI, so they keep\n// their own copies and assert parity with these in a test.\n// ---------------------------------------------------------------------------\n\n/** The five words `@wildwinter/expr` lexes as keywords, so a property may not be\n * called any of them: the reference does not resolve, it fails to parse. Kept\n * here as a copy for the same reason the rules are (no dependency), and each\n * app probes the real parser to prove the list is still true. */\nexport const RESERVED_PROPERTY_NAMES: readonly string[] = [\"true\", \"false\", \"and\", \"or\", \"not\"];\n\n/** What an editor should tell an author, in one line. */\nexport const PROPERTY_NAME_HINT =\n \"Lower case letters, digits, and underscores. It can't start with a digit. No hyphens or spaces.\";\n\n/**\n * Coerce a label into a legal property name: lower case, apostrophes dropped,\n * runs of anything else to a single underscore, no trailing underscore, an\n * underscore in front if it would otherwise start with a digit, and one behind\n * if it would otherwise be a keyword.\n *\n * A LEADING underscore is kept where the author typed one, because `_private` is\n * a legal name and a meaningful choice; only the ends that would be illegal are\n * touched.\n *\n * May return \"\", when there was nothing usable in the input. Callers treat that\n * as \"no name yet\" rather than as a name.\n */\nexport function propertyNameify(text: string): string {\n const trimmed = text.trim();\n // An underscore the author actually typed is kept; one that is only the ghost of\n // leading punctuation is not. That is the difference between `_private` and `!gold`.\n const deliberateLeading = trimmed.startsWith(\"_\");\n let out = trimmed.toLowerCase().replace(/['’]/g, \"\")\n .replace(/[^a-z0-9_]+/g, \"_\").replace(/_+/g, \"_\").replace(/^_+|_+$/g, \"\");\n if (out === \"\") return \"\";\n if (deliberateLeading || /^[0-9]/.test(out)) out = `_${out}`;\n if (RESERVED_PROPERTY_NAMES.includes(out)) out = `${out}_`;\n return out;\n}\n\n/**\n * Is this a name the expression language can actually reach? Lower case letters,\n * digits and underscores, not starting with a digit, and not a keyword.\n *\n * \"\" is NOT legal: an unnamed declaration is an unfinished row, which is a\n * different thing from an illegal one, and callers distinguish them.\n */\nexport function isValidPropertyName(name: string): boolean {\n return /^[a-z_][a-z0-9_]*$/.test(name) && !RESERVED_PROPERTY_NAMES.includes(name);\n}\n\n/**\n * The one thing that is only a case difference, e.g. `isNight` -> `isnight`.\n *\n * Worth its own question because it is the only violation that can be repaired\n * without guessing at intent: the language already folds every REFERENCE, so\n * folding the declaration to match changes nothing observable, where inventing\n * an underscore for a space is a decision about what the author meant. Loaders\n * fold this class silently and report the rest.\n */\nexport function isCaseOnlyPropertyName(name: string): boolean {\n return !isValidPropertyName(name) && isValidPropertyName(name.toLowerCase());\n}\n","// ---------------------------------------------------------------------------\n// The manners for a field that names a declared property.\n//\n// The RULE is property-names.ts, which is dependency-free because each app's\n// domain layer keeps its own copy of it. This is the other half: how a field\n// BEHAVES while somebody types a name into it, which only UI touches, so the\n// shell owns it outright.\n//\n// The manners are the gameId editor's, adopted rather than re-argued (see\n// id-editor.ts): an illegal name is marked and blocks Save, and Tab coerces it,\n// in the field, where it can be seen and undone. Nothing is rewritten as it is\n// typed. Patterpad's first pass at this folded every keystroke to lower case;\n// that is the manner the id editor had already rejected, on the grounds that an\n// app which silently renames a thing has made a decision that was not its to\n// make, and a property name is exactly that kind of thing: the author is naming\n// something their own conditions will call.\n//\n// It arrived here as two copies, one per app, written in one session and already\n// carrying the same forty lines of fault wording. Lifting them before they could\n// drift is the whole reason this file exists.\n// ---------------------------------------------------------------------------\n\nimport { isValidPropertyName, propertyNameify, RESERVED_PROPERTY_NAMES } from \"./property-names.js\";\n\n/** Re-check callbacks for bound REFERENCE fields, so a host can say \"the declarations\n * changed\" without holding a handle per row. */\nconst refSyncs = new WeakMap<HTMLInputElement, () => void>();\n\n/**\n * One line saying what is wrong with this name, or undefined when nothing is.\n *\n * Each branch names what actually HAPPENS rather than reciting the rule, because\n * the person reading it is usually somebody wondering why a condition does\n * nothing. Kept to one line: it lands in a rollover on the field, and hosts\n * repeat it in the Save error rather than inventing a summary of their own.\n */\nexport function propertyNameProblem(name: string): string | undefined {\n if (isValidPropertyName(name)) return undefined;\n const suggestion = propertyNameify(name);\n const tail = suggestion ? ` Try \"${suggestion}\", or press Tab.` : \"\";\n if (RESERVED_PROPERTY_NAMES.includes(name.toLowerCase())) {\n return `Can't be used. \"${name.toLowerCase()}\" is a keyword in expressions.${tail}`;\n }\n // The one that is not an error anywhere else: `@scope.a-b` compiles to a-minus-b.\n if (name.includes(\"-\")) return `Can't be used. A hyphen reads as subtraction in an expression.${tail}`;\n if (/^[0-9]/.test(name)) return `Can't be used. A name can't start with a digit.${tail}`;\n if (name !== name.toLowerCase()) {\n return `Can't be used. Expressions fold names, so this would look for \"${name.toLowerCase()}\".${tail}`;\n }\n return `Can't be used. Only lower case letters, digits, and underscores.${tail}`;\n}\n\n/**\n * Wire a name field: mark it live, explain the fault in its rollover, and coerce\n * on Tab. `onCommit` receives what the author typed, VERBATIM, on every input.\n *\n * `.illegal` rather than dupGuard's `.invalid`, because both can be true of one\n * field and `dupGuard.check()` owns that class: it would clear this marking every\n * time it ran, including at the moment of a blocked Save.\n *\n * An empty field is not marked. An unfinished row is a different thing from an\n * illegal one, and hosts prune blank names on save.\n */\nexport function bindPropertyName(\n input: HTMLInputElement,\n onCommit: (value: string) => void,\n opts: { hint?: string } = {},\n): void {\n const hint = opts.hint;\n const sync = (): void => {\n const v = input.value.trim();\n const problem = v === \"\" ? undefined : propertyNameProblem(v);\n input.classList.toggle(\"illegal\", !!problem);\n if (problem) input.title = problem;\n else if (input.title.startsWith(\"Can't be used\")) {\n if (hint) input.title = hint;\n else input.removeAttribute(\"title\");\n }\n };\n input.addEventListener(\"input\", () => { onCommit(input.value); sync(); });\n input.addEventListener(\"keydown\", (e) => {\n // The opt-in coercion, the id editor's gesture. Shift+Tab is left alone: it is\n // how somebody leaves a field they have not finished with.\n if (e.key !== \"Tab\" || e.shiftKey) return;\n const typed = input.value.trim();\n if (typed === \"\" || isValidPropertyName(typed)) return;\n const coerced = propertyNameify(typed);\n if (!coerced || coerced === input.value) return;\n e.preventDefault();\n input.value = coerced;\n onCommit(coerced);\n sync();\n });\n sync();\n}\n\n/**\n * The first field in `host` holding a name no expression could reach, for a Save\n * gate. Hosts hand this back beside `dupGuard.firstDuplicate()`: two faults, two\n * sets of words, one gate.\n */\nexport function firstIllegalPropertyName(host: ParentNode): HTMLInputElement | null {\n return host.querySelector<HTMLInputElement>(\"input.illegal\");\n}\n\n// ---------------------------------------------------------------------------\n// Reference fields, which are a different job from declaration fields.\n//\n// A declaration NAMES a property; a reference POINTS at one. The expression\n// parser folds every reference, so `@world.isNight` reaches `isnight` perfectly\n// well and complaining about the capital would be inventing a rule the language\n// does not have. Everything else still bites, and harder: no declaration may\n// contain a hyphen, so `is-night` in a reference can never match anything that\n// exists.\n//\n// And a reference has a fault a declaration cannot have: naming something that\n// was never declared. That is not a grammar question, so the host supplies the\n// list. The cost of getting it wrong is quiet - a coverage driver pointed at a\n// property nobody declared feeds a value nobody reads, the branches it was meant\n// to exercise stay unexercised, and the report shows them as never hit, which\n// reads as \"this content is unreachable\" rather than \"that name is a typo\".\n// ---------------------------------------------------------------------------\n\nexport interface PropertyRefOptions {\n /** The names that exist, read fresh on every check so a declaration added in the\n * same dialog clears the complaint. Compared case-insensitively, since the\n * parser folds. */\n known: () => string[];\n /** The scope token this field addresses, for the wording (\"no @world property...\"). */\n scope?: string;\n /** Shown on the field when there is nothing to complain about. */\n hint?: string;\n}\n\n/** The closest known name, when one is close enough to be worth naming. Deliberately\n * narrow: a wrong suggestion is worse than none, so it wants a shared prefix or a\n * single-character slip, not the least-bad match in the list. */\nfunction nearest(name: string, known: string[]): string | undefined {\n const n = name.toLowerCase();\n const byPrefix = known.filter((k) => k.toLowerCase().startsWith(n) || n.startsWith(k.toLowerCase()));\n if (byPrefix.length === 1) return byPrefix[0];\n let best: string | undefined;\n let bestScore = 3; // strictly better than \"three edits away\"\n for (const k of known) {\n const a = k.toLowerCase();\n if (Math.abs(a.length - n.length) > 2) continue;\n // Levenshtein, small strings, no need for a matrix beyond two rows.\n let prev = Array.from({ length: n.length + 1 }, (_, i) => i);\n for (let i = 1; i <= a.length; i++) {\n const row = [i];\n for (let j = 1; j <= n.length; j++) {\n row[j] = Math.min(prev[j]! + 1, row[j - 1]! + 1, prev[j - 1]! + (a[i - 1] === n[j - 1] ? 0 : 1));\n }\n prev = row;\n }\n const d = prev[n.length]!;\n if (d < bestScore) { bestScore = d; best = k; }\n }\n return best;\n}\n\n/** Why this reference cannot be used, or undefined when it can. */\nexport function propertyRefProblem(name: string, opts: PropertyRefOptions): string | undefined {\n const scope = opts.scope ? `@${opts.scope}` : \"that scope\";\n // Case is NOT a fault here: the parser folds references.\n const folded = name.toLowerCase();\n if (!isValidPropertyName(folded)) {\n const suggestion = propertyNameify(name);\n const tail = suggestion ? ` Try \"${suggestion}\", or press Tab.` : \"\";\n if (RESERVED_PROPERTY_NAMES.includes(folded)) return `Can't be used. \"${folded}\" is a keyword in expressions.${tail}`;\n if (name.includes(\"-\")) return `Can't be used. A hyphen reads as subtraction in an expression.${tail}`;\n if (/^[0-9]/.test(name)) return `Can't be used. A name can't start with a digit.${tail}`;\n return `Can't be used. Only lower case letters, digits, and underscores.${tail}`;\n }\n const known = opts.known();\n if (known.some((k) => k.toLowerCase() === folded)) return undefined;\n const near = nearest(folded, known);\n if (near) return `No ${scope} property is called \"${name}\". Did you mean \"${near}\"?`;\n if (known.length === 0) return `No ${scope} properties are declared yet. Declare one above first.`;\n const shown = known.slice(0, 3).map((k) => `\"${k}\"`).join(\", \");\n const more = known.length > 3 ? `, and ${known.length - 3} more` : \"\";\n return `No ${scope} property is called \"${name}\". Pick one of ${shown}${more}.`;\n}\n\n/**\n * Wire a field that POINTS at a declared property: same manners as a declaration\n * field (mark, do not rewrite, Tab coerces), plus the declared names offered as a\n * datalist, because the rule is declare-then-reference and a picker makes that\n * painless rather than pedantic.\n *\n * Marks `.illegal` for an undeclared name too, which gates Save wherever the host\n * already asks `firstIllegalPropertyName`.\n */\nexport function bindPropertyRef(\n input: HTMLInputElement,\n onCommit: (value: string) => void,\n opts: PropertyRefOptions,\n): void {\n const list = document.createElement(\"datalist\");\n list.id = `shell-propref-${Math.random().toString(36).slice(2, 9)}`;\n input.setAttribute(\"list\", list.id);\n\n // Attached lazily, not once at bind. A datalist has to be IN the document to do anything, and it is\n // attached as a sibling of the input - so binding a field that has not been appended yet (which is\n // the ordinary order for a builder that makes its nodes and then mounts them) silently produced no\n // autocomplete at all: no error, nothing to see. This re-tries on every sync, so it heals as soon as\n // the field reaches the tree.\n const attach = (): void => {\n if (!list.isConnected && input.parentNode) input.after(list);\n };\n\n const sync = (): void => {\n attach();\n const known = opts.known();\n list.replaceChildren(...known.map((k) => { const o = document.createElement(\"option\"); o.value = k; return o; }));\n const v = input.value.trim();\n const problem = v === \"\" ? undefined : propertyRefProblem(v, opts);\n input.classList.toggle(\"illegal\", !!problem);\n if (problem) input.title = problem;\n else if (input.title.startsWith(\"Can't be used\") || input.title.startsWith(\"No \")) {\n if (opts.hint) input.title = opts.hint;\n else input.removeAttribute(\"title\");\n }\n };\n refSyncs.set(input, sync);\n\n input.addEventListener(\"input\", () => { onCommit(input.value); sync(); });\n input.addEventListener(\"keydown\", (e) => {\n if (e.key !== \"Tab\" || e.shiftKey) return;\n const typed = input.value.trim();\n // Only a GRAMMAR fault is coercible. An undeclared name is not something Tab can\n // fix, and a legal reference in the wrong case is not a fault at all.\n if (typed === \"\" || isValidPropertyName(typed.toLowerCase())) return;\n const coerced = propertyNameify(typed);\n if (!coerced || coerced === input.value) return;\n e.preventDefault();\n input.value = coerced;\n onCommit(coerced);\n sync();\n });\n sync();\n}\n\n/**\n * Re-check every bound reference field inside `host`. Hosts call this when the\n * DECLARATIONS change, since a reference's validity depends on a list it does not\n * own: rename a property and the driver pointing at it is wrong from that moment,\n * with nothing typed into it.\n */\nexport function revalidatePropertyRefs(host: ParentNode): void {\n for (const input of host.querySelectorAll(\"input\")) refSyncs.get(input)?.();\n}\n","// ---------------------------------------------------------------------------\n// THE property declaration list: name, type, default, values or stages,\n// purpose, in a row per declaration, with reorder, remove, add-then-focus and\n// the duplicate / illegal-name gates. Patterpad's settings-properties.ts and\n// Storyletter's prop-list.ts were built from the same shell primitives in the\n// same order (dupGuard, bindPropertyName, a type select over the same six\n// types, defaultControl, three iconBtns on moveItem, labelled tagChips and\n// stageChips, expandableRow, focusNewRow); this is the one list, and each\n// app's extras (Shared, Temporary, Read-only, Durable) arrive through\n// `extraLine` / `extraDetails`.\n//\n// `defaultControl` was four copies (three inside Patterpad), and the two apps'\n// option labels had already parted (\"(unset)\" vs \"(none)\"). One label set now.\n//\n// Storage of the default differs by app: Patterpad's model holds a typed\n// value (true, 3), Storyletter's DTO holds the string the field showed. The\n// list mutates `decls` in place either way; `typed` picks the encoding.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\nimport { iconBtn, labelled, moveItem, stageChips, tagChips } from \"./dom.js\";\nimport { dupGuard, expandableRow, focusNewRow } from \"./settings.js\";\nimport type { SettingsSectionHandle } from \"./settings.js\";\nimport { bindPropertyName, firstIllegalPropertyName } from \"./property-name-field.js\";\nimport { PROPERTY_NAME_HINT } from \"./property-names.js\";\n\n/** The six types both apps declare. */\nexport type PropertyListType = \"number\" | \"boolean\" | \"string\" | \"enum\" | \"flags\" | \"quality\";\n\n/** The shape a row edits. Apps' DTOs carry more (shared, writable, durable);\n * those ride through untouched. */\nexport interface PropertyDeclLike {\n name: string;\n type: string;\n default?: unknown;\n /** enum / flags */\n values?: string[];\n /** quality: the ladder, in order */\n stages?: string[];\n purpose?: string;\n}\n\n/** The type list, in the family's order, with the words the select shows. */\nexport const PROPERTY_TYPES: ReadonlyArray<readonly [PropertyListType, string]> = [\n [\"number\", \"Number\"], [\"boolean\", \"True / False\"], [\"string\", \"Text\"],\n [\"enum\", \"List\"], [\"flags\", \"Flags\"], [\"quality\", \"Quality\"],\n];\n\nexport interface DefaultControlOptions {\n /**\n * How the default is stored. `false` (default): the string the field shows,\n * \"\" for none (Storyletter's DTO). `true`: a typed value (boolean, number,\n * string), and none DELETES the key (Patterpad's model).\n */\n typed?: boolean;\n}\n\nconst option = (value: string, label: string, selected: boolean): HTMLOptionElement => {\n const o = el(\"option\", undefined, label);\n o.value = value; o.selected = selected;\n return o;\n};\n\nconst NONE = \"(none)\";\n\n/**\n * The type-appropriate control for a declaration's DEFAULT: a true / false\n * picker for a boolean, the value list for an enum, the stage list for a\n * quality (blank = the first rung), a note for flags (a set starts empty), a\n * number or text field otherwise. The one label for \"no default\" is \"(none)\".\n */\nexport function defaultControl(p: PropertyDeclLike, onChange?: () => void, opts: DefaultControlOptions = {}): HTMLElement {\n const typed = opts.typed === true;\n const write = (raw: string): void => {\n if (raw === \"\") { if (typed) delete p.default; else p.default = \"\"; }\n else if (typed && p.type === \"boolean\") p.default = raw === \"true\";\n else if (typed && p.type === \"number\") p.default = Number(raw);\n else p.default = raw;\n onChange?.();\n };\n const current = p.default === undefined || p.default === null ? \"\" : String(p.default);\n if (p.type === \"boolean\") {\n const sel = el(\"select\", \"field set-default\");\n sel.append(option(\"\", NONE, current === \"\"), option(\"true\", \"True\", current === \"true\"), option(\"false\", \"False\", current === \"false\"));\n sel.addEventListener(\"change\", () => write(sel.value));\n return sel;\n }\n if (p.type === \"enum\") {\n const sel = el(\"select\", \"field set-default\");\n sel.append(option(\"\", NONE, current === \"\"));\n for (const v of p.values ?? []) sel.append(option(v, v, current === v));\n sel.addEventListener(\"change\", () => write(sel.value));\n return sel;\n }\n if (p.type === \"quality\") {\n // The default is a STAGE; blank means the first rung, so say so rather\n // than \"(none)\".\n const sel = el(\"select\", \"field set-default\");\n sel.append(option(\"\", \"(first stage)\", current === \"\"));\n for (const v of p.stages ?? []) sel.append(option(v, v, current === v));\n sel.addEventListener(\"change\", () => write(sel.value));\n return sel;\n }\n if (p.type === \"flags\") {\n // Flags hold a SET of values, so there is no single default: it starts empty.\n const s = el(\"span\", \"set-flagnote\", \"Starts empty\");\n s.dataset[\"tip\"] = \"A flags property starts with none set.\";\n return s;\n }\n const input = el(\"input\", \"field set-default\");\n input.type = p.type === \"number\" ? \"number\" : \"text\";\n input.placeholder = \"Default\";\n input.value = current;\n input.addEventListener(\"input\", () => write(input.value));\n return input;\n}\n\nexport interface PropertyListOptions<T extends PropertyDeclLike> extends DefaultControlOptions {\n /** Called after every mutation; a centre editor feeds its autosave here. */\n onChange?: () => void;\n /** The add button's label. Default \"+ Add property\". */\n addLabel?: string;\n /** The empty sentence. Default \"No properties yet.\" */\n emptyText?: string;\n /** The type select's entries; default `PROPERTY_TYPES`. */\n types?: ReadonlyArray<readonly [string, string]>;\n /** A fresh declaration for the add button. Default: a blank of the first type. */\n newDecl?: () => T;\n /** Controls on the row's line, between the default and the movers\n * (Storyletter's \"uses\" affordance). */\n extraLine?: (decl: T, index: number) => (HTMLElement | null)[];\n /** Fields behind the disclosure, before Values / Stages / Purpose\n * (Shared, Temporary, Read-only, Durable). */\n extraDetails?: (decl: T, index: number) => (HTMLElement | null)[];\n /** Offer the Purpose field. Default true. */\n purpose?: boolean;\n}\n\nexport interface PropertyListHandle extends SettingsSectionHandle {\n /** Both faults, for the Save gate: a duplicate first, then an illegal name. */\n firstInvalid(): HTMLInputElement | null;\n /** The first name that clashes with another, or null. */\n firstDuplicate(): HTMLInputElement | null;\n /** The first name no expression could reach, or null. */\n firstIllegalName(): HTMLInputElement | null;\n /** Redraw from `decls` (after an outside change). */\n render(): void;\n}\n\n/** Mount the list into `host`. Mutates `decls` in place; the caller reads\n * them back on save (pruning blank names is the caller's, since only it\n * knows its DTO). */\nexport function mountPropertyList<T extends PropertyDeclLike>(host: HTMLElement, decls: T[], opts: PropertyListOptions<T> = {}): PropertyListHandle {\n const changed = (): void => opts.onChange?.();\n const guard = dupGuard();\n const types = opts.types ?? PROPERTY_TYPES;\n const list = el(\"div\", \"set-list\");\n const keep = (parts: (HTMLElement | null)[] | undefined): HTMLElement[] =>\n (parts ?? []).filter((p): p is HTMLElement => p !== null);\n\n const row = (p: T, i: number): HTMLElement => {\n const name = el(\"input\", \"field set-name\");\n name.type = \"text\"; name.placeholder = \"Property name\"; name.value = p.name; name.spellcheck = false;\n name.dataset[\"tip\"] = PROPERTY_NAME_HINT;\n bindPropertyName(name, (v) => { p.name = v; changed(); }, { hint: PROPERTY_NAME_HINT });\n guard.track(name);\n\n const type = el(\"select\", \"field set-type\");\n for (const [v, label] of types) type.append(option(v, label, v === p.type));\n type.addEventListener(\"change\", () => {\n p.type = type.value;\n if (opts.typed) delete p.default; else p.default = \"\";\n if (p.type === \"enum\" || p.type === \"flags\") p.values ??= []; else delete p.values;\n if (p.type === \"quality\") p.stages ??= []; else delete p.stages;\n render(); changed();\n });\n\n // The default control is rebuilt in place when the values change, so a\n // new value becomes selectable as the default.\n let def = defaultControl(p, changed, opts);\n const refreshDefault = (): void => { const fresh = defaultControl(p, changed, opts); def.replaceWith(fresh); def = fresh; changed(); };\n\n const up = iconBtn(\"up\", \"Move up\", () => { if (moveItem(decls, i, -1)) { render(); changed(); } }, i === 0);\n const down = iconBtn(\"down\", \"Move down\", () => { if (moveItem(decls, i, 1)) { render(); changed(); } }, i === decls.length - 1);\n const del = iconBtn(\"close\", \"Delete property\", () => { decls.splice(i, 1); render(); changed(); }, false, true);\n\n const details: HTMLElement[] = keep(opts.extraDetails?.(p, i));\n if (p.type === \"enum\" || p.type === \"flags\") details.push(labelled(\"Values\", tagChips(p, refreshDefault)));\n // A quality's ladder, IN ORDER: the chips carry movers because position is the meaning.\n if (p.type === \"quality\") details.push(labelled(\"Stages (in order)\", stageChips(p, refreshDefault)));\n if (opts.purpose !== false) {\n const purpose = el(\"input\", \"field set-purpose\");\n purpose.type = \"text\"; purpose.placeholder = \"What this property is for\"; purpose.value = p.purpose ?? \"\";\n purpose.addEventListener(\"input\", () => {\n if (purpose.value.trim()) p.purpose = purpose.value; else delete p.purpose;\n changed();\n });\n details.push(labelled(\"Purpose\", purpose));\n }\n // Stamped with the declared name so \"Go to definition\" lands on the row (revealRow).\n return expandableRow({ line: [name, type, def, ...keep(opts.extraLine?.(p, i)), up, down, del], details, name: p.name });\n };\n\n const render = (): void => {\n guard.reset();\n list.replaceChildren();\n if (decls.length === 0) list.append(el(\"p\", \"empty\", opts.emptyText ?? \"No properties yet.\"));\n else decls.forEach((p, i) => list.append(row(p, i)));\n guard.check();\n };\n render();\n\n const add = el(\"button\", \"btn set-add\", opts.addLabel ?? \"+ Add property\");\n add.type = \"button\";\n add.addEventListener(\"click\", () => {\n decls.push(opts.newDecl ? opts.newDecl() : ({ name: \"\", type: types[0]?.[0] ?? \"string\", ...(opts.typed ? {} : { default: \"\" }) } as unknown as T));\n render(); changed(); focusNewRow(list);\n });\n host.append(list, add);\n\n return {\n firstDuplicate: () => guard.firstDuplicate(),\n firstIllegalName: () => firstIllegalPropertyName(host),\n // The Save gate takes both faults; the field's own rollover says which.\n firstInvalid: () => guard.firstDuplicate() ?? firstIllegalPropertyName(host),\n render,\n };\n}\n"]}
|