@wildwinter/app-shell 0.46.2 → 0.46.4

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tooltip.ts","../src/dom.ts","../src/exit.ts","../src/dialog.ts","../src/updater-view.ts"],"names":["el","timer"],"mappings":"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","file":"updater-view.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// A single themed tooltip the family controls, replacing native `title`\n// rollovers: those are unstyled OS chrome and appear on the platform's slow\n// (~1s) delay, which is exactly the seam design-language section 4 (\"coherent\n// to the edges\") says not to leave showing.\n//\n// ONE floating bubble, document-DELEGATED: any element carrying a `data-tip`\n// attribute gets it, so a caller just sets `data-tip=\"...\"` anywhere and wires\n// nothing. The bubble anchors above the element (flipping below near the top\n// edge), uses our own snappy delay and the shared panel-enter motion.\n// `initTooltips()` is idempotent, so it is safe to call from more than one\n// place in an app that has several entry points.\n//\n// Lifted from Patterpad's patterpad-surface/web/tooltip.ts, which proved it,\n// with the one app-specific rule (suppressed in Writing View) generalised into\n// the `suppressed` option so the shell knows nothing about either app's modes.\n// ---------------------------------------------------------------------------\n\n// Bold markers: a producer wraps a span in these PRIVATE control chars (U+0002 STX / U+0003 ETX,\n// never present in user text) and the renderer turns it into a <strong>. Lets a data-tip bold part\n// of itself (a name, a \"VO:\" prefix) without any HTML, and each segment is still set via\n// textContent, so there is no injection risk from user content.\nconst B_OPEN = \"\\u0002\";\nconst B_CLOSE = \"\\u0003\";\n\n/** Wrap `s` so it renders bold inside a tooltip (see the marker note above). */\nexport function tipBold(s: string): string { return `${B_OPEN}${s}${B_CLOSE}`; }\n\n/** Render `text` into `host`, turning `\\u0002…\\u0003`-marked spans into <strong>; all set as text (safe). */\nfunction renderTip(host: HTMLElement, text: string): void {\n host.textContent = \"\";\n const re = /\\u0002([^\\u0003]*)\\u0003/g;\n let last = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n if (m.index > last) host.append(text.slice(last, m.index));\n const strong = document.createElement(\"strong\");\n strong.textContent = m[1] ?? \"\";\n host.append(strong);\n last = m.index + m[0].length;\n }\n if (last < text.length) host.append(text.slice(last));\n}\n\nexport interface TooltipOptions {\n /** Called before every show: return true to keep the bubble down. For a mode\n * that wants no chrome at all (Patterpad's Writing View). */\n suppressed?: () => boolean;\n}\n\nlet inited = false;\nlet checked = false; // the one-shot no-host warning below has been scheduled\nlet suppressed: (() => boolean) | undefined;\nconst SHOW_DELAY = 350; // ms - snappier than the OS title delay, slow enough not to flicker on pass-through\nconst EDGE = 6; // viewport inset so the bubble never touches the window edge\nconst GAP = 7; // px between the anchor and the bubble\n\nlet tip: HTMLDivElement | null = null;\nlet timer = 0;\nlet active: HTMLElement | null = null;\n/** The subject of a rect-anchored tip (see `tipAt`), when there is one. */\nlet activeKey: string | null = null;\n\nfunction ensureEl(): HTMLDivElement {\n if (tip) return tip;\n const el = document.createElement(\"div\");\n el.className = \"tooltip\";\n el.setAttribute(\"role\", \"tooltip\");\n el.hidden = true;\n document.body.appendChild(el);\n tip = el;\n return el;\n}\n\n/** A rectangle in VIEWPORT coordinates, for anchoring a tip to something that is\n * not a DOM element: a shape on a canvas, a run of text inside one. */\nexport interface TipRect { left: number; top: number; width: number; height: number }\n\nconst rectOf = (anchor: HTMLElement | TipRect): TipRect =>\n (anchor instanceof HTMLElement ? anchor.getBoundingClientRect() : anchor);\n\n/** Anchor the (already-visible, so measurable) bubble above the anchor, flipping below near the top. */\nfunction place(anchor: HTMLElement | TipRect): void {\n const t = ensureEl();\n const r = rectOf(anchor);\n const tr = t.getBoundingClientRect();\n let top = r.top - tr.height - GAP;\n if (top < EDGE) top = r.top + r.height + GAP; // not enough room above -> below\n let left = r.left + r.width / 2 - tr.width / 2;\n left = Math.max(EDGE, Math.min(left, window.innerWidth - tr.width - EDGE));\n t.style.left = `${Math.round(left)}px`;\n t.style.top = `${Math.round(top)}px`;\n}\n\nfunction show(anchor: HTMLElement): void {\n const text = anchor.dataset[\"tip\"];\n if (!text) return;\n paint(anchor, text);\n}\n\n/** Fill, place and reveal the bubble. The one path: both the delegated\n * `data-tip` route and `tipAt` end up here, so a canvas tip and a button tip\n * are the same bubble with the same look and the same motion. */\nfunction paint(anchor: HTMLElement | TipRect, text: string): void {\n if (suppressed?.()) return;\n const t = ensureEl();\n // A modal <dialog> (settings, a confirm) renders in the browser TOP LAYER, above every normal-flow\n // element whatever its z-index. If the anchor is inside one, move the bubble into that dialog so it\n // shares the top layer; otherwise keep it on <body>. Re-parenting per show also moves it back out\n // once the modal closes.\n const host: HTMLElement = (anchor instanceof HTMLElement ? anchor.closest<HTMLElement>(\"dialog[open]\") : null)\n ?? document.body;\n if (t.parentElement !== host) host.appendChild(t);\n active = anchor instanceof HTMLElement ? anchor : null;\n renderTip(t, text);\n t.hidden = false;\n t.classList.remove(\"show\"); void t.offsetWidth; // restart the enter animation\n place(anchor); // measure + position now it has content + is visible\n t.classList.add(\"show\");\n}\n\nfunction hide(): void {\n if (timer) { window.clearTimeout(timer); timer = 0; }\n active = null;\n activeKey = null;\n if (tip) { tip.hidden = true; tip.classList.remove(\"show\"); }\n}\n\nfunction schedule(anchor: HTMLElement): void {\n if (anchor === active) return;\n if (timer) window.clearTimeout(timer);\n timer = window.setTimeout(() => show(anchor), SHOW_DELAY);\n}\n\n/**\n * Show a tip for something that has no DOM node of its own: a card on a canvas,\n * a zone, a pin. The caller says WHERE (a viewport rectangle) and WHAT, and\n * identifies the subject with `key`.\n *\n * The key is what makes this usable from a canvas. Re-calling with the same key\n * re-places the bubble WITHOUT restarting the delay, so a tip can follow its\n * shape while the camera moves; calling with a different key starts the wait\n * again, so sweeping the pointer across a dense board does not strobe.\n *\n * The bubble is ordinary DOM, so it holds its size whatever the canvas is\n * zoomed to. That is the point of it: the label it stands in for is the one\n * thing on a canvas that must not shrink with everything else.\n */\nexport function tipAt(key: string, rect: TipRect, text: string): void {\n if (key === activeKey) {\n // Already up for this subject: move it, do not re-animate it.\n if (tip && !tip.hidden) { renderTip(tip, text); place(rect); }\n return;\n }\n // A different subject: down first, then wait again. Leaving the old bubble up\n // during the wait would put one card's name over another card, which is worse\n // than no name at all - and it matches the delegated route, where leaving an\n // element hides the tip before the next one is scheduled.\n hide();\n activeKey = key;\n timer = window.setTimeout(() => { timer = 0; paint(rect, text); }, SHOW_DELAY);\n}\n\n/** Take any tip down: the pointer has left, or a gesture has started. */\nexport function hideTip(): void { hide(); }\n\n/**\n * Mount the renderer because something is about to speak the grammar.\n *\n * The themed tooltip is not a feature a host turns on; it is the renderer for a grammar the shell's own\n * components already speak, and `data-tip` with nothing listening draws NOTHING - not the themed\n * bubble, not the platform one. That silence cost Patterpad two releases of a pin with no tooltip, and\n * cost it again when a `title` was converted to `data-tip` in a window that had never mounted this.\n *\n * So `el` calls this whenever it sets a tip. Mounting a delegated listener is the same class of act as\n * `confirmDialog` appending to `document.body`, which this package has always done.\n */\nexport function ensureTooltipHost(): void { initTooltips(); }\n\n/**\n * One deferred check that an app writing `data-tip` ITSELF has a host, warning if not.\n *\n * `ensureTooltipHost` covers anything built through `el`. It cannot cover a `data-tip` written straight\n * into markup or set by hand, which is the case that leaves a whole window of dead tips with no shell\n * component involved - so that one gets a warning rather than silence. Scheduled once, after the\n * current task, so it sees a rendered document rather than an empty one.\n */\nexport function checkTooltipHost(): void {\n if (checked) return;\n checked = true;\n setTimeout(() => {\n if (inited || typeof document === \"undefined\") return;\n if (document.querySelector(\"[data-tip]\")) {\n console.warn(\"app-shell: something set `data-tip` but no tooltip host is mounted, so those rollovers will not draw. Call initTooltips() once per window.\");\n }\n }, 0);\n}\n\nconst tipAncestor = (n: EventTarget | null): HTMLElement | null =>\n (n instanceof Element ? n.closest<HTMLElement>(\"[data-tip]\") : null);\n\n/**\n * Wire the one delegated controller. Mounting happens once; OPTIONS do not.\n *\n * An explicit call is always authoritative about its options, even after something else has already\n * mounted the listener. That ordering is not hypothetical, and Storyletter caught it before it shipped:\n * once components self-mount (see `ensureTooltipHost`), a host's own\n * `initTooltips({ suppressed })` could land second and be dropped by an early return, silently taking\n * a rule like \"no tooltips in Writing View\" with it. Nothing would fail and nothing would show.\n *\n * Components therefore call this with NO arguments: they mount the renderer without claiming the\n * options, and the host keeps the last word on behaviour.\n */\nexport function initTooltips(opts: TooltipOptions = {}): void {\n if (opts.suppressed !== undefined) suppressed = opts.suppressed; // an explicit call always wins\n if (inited) return;\n inited = true;\n\n document.addEventListener(\"pointerover\", (e) => {\n const el = tipAncestor(e.target);\n if (el) schedule(el);\n });\n document.addEventListener(\"pointerout\", (e) => {\n const el = tipAncestor(e.target);\n if (!el) return;\n const to = (e as PointerEvent).relatedTarget;\n if (to instanceof Node && el.contains(to)) return; // still inside the same anchor\n hide();\n });\n // Keyboard parity: reveal on focus, dismiss on blur.\n document.addEventListener(\"focusin\", (e) => { const el = tipAncestor(e.target); if (el) schedule(el); });\n document.addEventListener(\"focusout\", () => hide());\n // It's ANCHORED (not cursor-following), so anything that moves the layout or context dismisses it.\n document.addEventListener(\"pointerdown\", () => hide(), true);\n document.addEventListener(\"scroll\", () => { if (active) hide(); }, true);\n document.addEventListener(\"keydown\", (e) => { if (e.key === \"Escape\") hide(); });\n window.addEventListener(\"blur\", () => hide());\n}\n","// ---------------------------------------------------------------------------\n// `el`: the tiny tag-typed element factory both apps build their imperative,\n// idempotent views with.\n//\n// A SUPERSET of the two apps' prior signatures, so neither rewrites call sites:\n// - Patterpad: el(tag, cls?, text?) (positional)\n// - Storylet Studio: el(tag, { className, text, ... }, ...kids) (options bag)\n// A string second argument is the className; an object is the props bag; string\n// children become text nodes. So `el(\"div\", \"c\", \"t\")` and\n// `el(\"div\", { className: \"c\" }, \"t\")` produce identical DOM.\n// ---------------------------------------------------------------------------\n\nimport { ensureTooltipHost, checkTooltipHost } from \"./tooltip.js\";\nimport { iconNode, iconNameOfGlyph, isIconName, type IconName } from \"./icons.js\";\n\nexport type Child = Node | string | null | undefined;\n\nexport interface ElProps {\n className?: string;\n text?: string;\n /** A NATIVE rollover. Prefer `tip`: `title` is OS chrome on the platform's own\n * slow delay, which is the seam the themed tooltip exists to close. */\n title?: string;\n /** A themed rollover (`data-tip`, picked up by the delegated controller that\n * `initTooltips()` wires). Also the accessible name, since the elements that\n * want one are usually icon buttons with no text of their own. */\n tip?: string;\n onClick?: (event: MouseEvent) => void;\n}\n\nexport function el<K extends keyof HTMLElementTagNameMap>(\n tag: K,\n classNameOrProps?: string | ElProps,\n ...children: Child[]\n): HTMLElementTagNameMap[K] {\n const node = document.createElement(tag);\n const props: ElProps = typeof classNameOrProps === \"string\"\n ? { className: classNameOrProps }\n : classNameOrProps ?? {};\n if (props.className) node.className = props.className;\n if (props.text !== undefined && props.text !== null) node.textContent = props.text;\n if (props.title !== undefined) node.title = props.title;\n if (props.tip !== undefined) {\n node.dataset[\"tip\"] = props.tip;\n node.setAttribute(\"aria-label\", props.tip);\n // Setting a tip mounts the renderer. Without this, `data-tip` draws nothing at all in a window whose\n // host never called `initTooltips()`, which is a silent failure rather than a degraded one. No\n // options passed: the host stays authoritative about behaviour (see tooltip.ts).\n ensureTooltipHost();\n }\n if (props.onClick) node.addEventListener(\"click\", props.onClick as EventListener);\n for (const child of children) {\n if (child === null || child === undefined) continue;\n node.append(child);\n }\n checkTooltipHost(); // one-shot: warns if the APP wrote data-tip and nothing mounted a host\n return node;\n}\n\n/** A small square icon button (the move/delete controls list editors share).\n * Its rollover is the THEMED one (`data-tip`), so an app that has called\n * `initTooltips()` gets our bubble rather than the platform's.\n *\n * `name` is a word from the vocabulary (\"up\", \"close\"), drawn at 14px. A\n * typed glyph from the deprecated `icon` table (\"↑\", \"✕\") is accepted for\n * now and drawn as the word it stood for, so an app that has not moved its\n * call sites yet still gets the drawn set; any other string is set as text,\n * which is the old behaviour and on its way out. */\nexport function iconBtn(name: IconName | (string & {}), title: string, onClick: () => void, disabled = false, danger = false): HTMLButtonElement {\n const b = el(\"button\", `shell-icon${danger ? \" danger\" : \"\"}`);\n b.type = \"button\"; b.dataset[\"tip\"] = title; b.setAttribute(\"aria-label\", title);\n const word = isIconName(name) ? name : iconNameOfGlyph(name);\n if (word) b.append(iconNode(word)); else b.textContent = name;\n b.disabled = disabled;\n b.addEventListener(\"click\", onClick);\n return b;\n}\n\n/**\n * A captioned field: `<label class=\"shell-labelled\"><span class=\"shell-fieldcap\">…</span>control</label>`.\n *\n * The label is pointed at the field it captions, EXPLICITLY, and never at a button. A `<label>` with no\n * `for` forwards a click to its first labelable descendant, and buttons are labelable - so a caption\n * wrapped around a control containing buttons (`tagChips`, whose every chip carries a remove button)\n * turns every click on the row's dead space into a press of the FIRST button in it. That shipped:\n * patterkit/patter#44, where clicking a Game Data list value anywhere but its ✕ deleted the first value\n * in the list. Reported as \"clicking a value removes the wrong one\", which is not a wrong-index bug at\n * all - the click never reached the chip.\n *\n * When the control holds no labelable field (a chips editor with the add input removed, say), this is a\n * plain `<div>`: no caption behaviour is better than a caption that presses something.\n */\nexport function labelled(label: string, control: HTMLElement): HTMLElement {\n const target = control.matches(\"input, select, textarea\")\n ? control\n : control.querySelector<HTMLElement>(\"input:not([type=button]):not([type=submit]), select, textarea\");\n const w = el(target ? \"label\" : \"div\", \"shell-labelled\");\n w.append(el(\"span\", \"shell-fieldcap\", label), control);\n if (target) {\n if (!target.id) target.id = `shell-f-${Math.random().toString(36).slice(2, 9)}`;\n (w as HTMLLabelElement).htmlFor = target.id;\n }\n return w;\n}\n\n/** Swap item `i` with its neighbour `i + delta` IN PLACE (the up/down reorder list editors share);\n * a no-op when the target is out of range. Returns whether anything moved. */\nexport function moveItem<T>(arr: T[], i: number, delta: number): boolean {\n const j = i + delta;\n if (j < 0 || j >= arr.length) return false;\n [arr[i], arr[j]] = [arr[j]!, arr[i]!];\n return true;\n}\n\n/** The icon size inside a chip's remove / move buttons: the chip is set at\n * 0.76rem, so its controls draw smaller than a toolbar button's 14px. */\nconst CHIP_ICON = 10;\n\n/** A tag-style editor for a string-list field (an enum's allowed values / flags): removable chips + an\n * add input (Enter or \",\" commits; blank / duplicate ignored). Mutates `holder.values` in place (read\n * back on save). `onChange` fires after any add / remove so callers can refresh a dependent control. */\nexport function tagChips(holder: { values?: string[] }, onChange?: () => void): HTMLElement {\n const wrap = el(\"div\", \"shell-tags\");\n const input = el(\"input\", \"shell-tag-input\");\n // Sentence-cased like every other hint (the microcopy ruling, 2026-08-28):\n // lowercase fragments read as adrift, and a placeholder is a hint speaking.\n input.type = \"text\"; input.placeholder = \"Add value\"; input.spellcheck = false;\n const makeChip = (v: string): HTMLElement => {\n const chip = el(\"span\", \"shell-tag\", v);\n const x = el(\"button\", \"shell-tag-x\", iconNode(\"close\", CHIP_ICON));\n x.type = \"button\"; x.dataset[\"tip\"] = `Remove ${v}`; x.setAttribute(\"aria-label\", `Remove ${v}`);\n x.addEventListener(\"click\", () => { holder.values = (holder.values ?? []).filter((o) => o !== v); chip.remove(); onChange?.(); });\n chip.append(x);\n return chip;\n };\n const commit = (): void => {\n const v = input.value.trim();\n if (v && !(holder.values ?? []).includes(v)) { (holder.values ??= []).push(v); wrap.insertBefore(makeChip(v), input); onChange?.(); }\n input.value = \"\";\n };\n input.addEventListener(\"keydown\", (e) => { if (e.key === \"Enter\" || e.key === \",\") { e.preventDefault(); commit(); } });\n input.addEventListener(\"blur\", commit);\n for (const v of holder.values ?? []) wrap.append(makeChip(v));\n wrap.append(input);\n return wrap;\n}\n\n/**\n * An ORDERED chips editor for a string-list field whose order means something\n * (a quality's stages: `advance()` walks them, so 2 comes after 1): numbered\n * chips, each with move-earlier / move-later / remove, plus an add input\n * (Enter or \",\" commits; blank / duplicate ignored). Mutates `holder.stages`\n * in place; `onChange` fires after any add / move / remove.\n *\n * Patterpad's, lifted (ui-review-2026-09, finding 3); Storyletter had been\n * faking it with `tagChips` over a `values` holder, which loses the order\n * controls. Same class family as `tagChips` (`.shell-tags`), so it is styled by\n * settings.css already.\n */\nexport function stageChips(holder: { stages?: string[] }, onChange?: () => void): HTMLElement {\n const wrap = el(\"div\", \"shell-tags shell-stages\");\n const input = el(\"input\", \"shell-tag-input\");\n input.type = \"text\"; input.placeholder = \"Add stage\"; input.spellcheck = false;\n const control = (name: IconName, tip: string, disabled: boolean, onClick: () => void): HTMLButtonElement => {\n const b = el(\"button\", \"shell-tag-x shell-tag-move\", iconNode(name, CHIP_ICON));\n b.type = \"button\"; b.dataset[\"tip\"] = tip; b.setAttribute(\"aria-label\", tip);\n b.disabled = disabled;\n b.addEventListener(\"click\", onClick);\n return b;\n };\n const rebuild = (): void => {\n for (const c of Array.from(wrap.children)) if (c !== input) c.remove();\n const stages = holder.stages ?? [];\n stages.forEach((v, i) => {\n const chip = el(\"span\", \"shell-tag\", `${i + 1}. ${v}`);\n chip.dataset[\"stage\"] = v;\n chip.append(\n control(\"back\", `Move ${v} earlier`, i === 0,\n () => { if (moveItem(stages, i, -1)) { rebuild(); onChange?.(); } }),\n control(\"forward\", `Move ${v} later`, i === stages.length - 1,\n () => { if (moveItem(stages, i, 1)) { rebuild(); onChange?.(); } }),\n );\n const x = el(\"button\", \"shell-tag-x\", iconNode(\"close\", CHIP_ICON));\n x.type = \"button\"; x.dataset[\"tip\"] = `Remove ${v}`; x.setAttribute(\"aria-label\", `Remove ${v}`);\n x.addEventListener(\"click\", () => { holder.stages = (holder.stages ?? []).filter((o) => o !== v); rebuild(); onChange?.(); });\n chip.append(x);\n wrap.insertBefore(chip, input);\n });\n };\n const commit = (): void => {\n const v = input.value.trim();\n if (v && !(holder.stages ?? []).includes(v)) { (holder.stages ??= []).push(v); rebuild(); onChange?.(); }\n input.value = \"\";\n };\n input.addEventListener(\"keydown\", (e) => { if (e.key === \"Enter\" || e.key === \",\") { e.preventDefault(); commit(); } });\n input.addEventListener(\"blur\", commit);\n wrap.append(input);\n rebuild();\n return wrap;\n}\n\n// --- drag-to-reorder ---------------------------------------------------------\n// One drag at a time per window, shared across every wired list, so the\n// midpoint test knows what is being carried without a dataTransfer round trip.\nlet reorderDragId: string | null = null;\nconst clearDropMarks = (host: ParentNode): void =>\n host.querySelectorAll(\".drop-before, .drop-after\").forEach((e) => e.classList.remove(\"drop-before\", \"drop-after\"));\n\n/**\n * Make `el` a drag source and a drop target for HTML5 drag-to-reorder among\n * its siblings. While carried it wears `.dragging`; a hovered target wears\n * `.drop-before` or `.drop-after` by the midpoint test on `axis` (\"y\" for\n * rows, \"x\" for a wrapping grid); a drop calls `onMove(draggedId, before,\n * targetId)` and the caller reorders its model and re-renders.\n *\n * Storyletter's `wireCardDrag`, lifted (ui-review-2026-09, finding 30);\n * Patterpad's `wireSceneDrag` is the same idea fixed on \"y\". The marks are\n * classes only: the app draws them (Patterpad's `box-shadow: 0 -2px 0 0\n * var(--accent)` pair is the family's look).\n */\nexport function wireReorder(el: HTMLElement, id: string, axis: \"x\" | \"y\", onMove: (id: string, before: boolean, targetId: string) => void): void {\n el.draggable = true;\n el.dataset[\"reorderId\"] = id;\n const host = (): ParentNode => el.parentElement ?? el;\n el.addEventListener(\"dragstart\", (e) => {\n reorderDragId = id; el.classList.add(\"dragging\");\n if (e.dataTransfer) { e.dataTransfer.effectAllowed = \"move\"; e.dataTransfer.setData(\"text/plain\", id); }\n });\n el.addEventListener(\"dragend\", () => { reorderDragId = null; el.classList.remove(\"dragging\"); clearDropMarks(host()); });\n el.addEventListener(\"dragover\", (e) => {\n if (!reorderDragId || reorderDragId === id) return;\n e.preventDefault();\n if (e.dataTransfer) e.dataTransfer.dropEffect = \"move\";\n const r = el.getBoundingClientRect();\n const before = axis === \"y\" ? e.clientY < r.top + r.height / 2 : e.clientX < r.left + r.width / 2;\n clearDropMarks(host());\n el.classList.add(before ? \"drop-before\" : \"drop-after\");\n });\n el.addEventListener(\"dragleave\", () => el.classList.remove(\"drop-before\", \"drop-after\"));\n el.addEventListener(\"drop\", (e) => {\n e.preventDefault();\n const before = el.classList.contains(\"drop-before\");\n clearDropMarks(host());\n if (reorderDragId && reorderDragId !== id) onMove(reorderDragId, before, id);\n });\n}\n\n// --- metadata and breadcrumbs ---------------------------------------------------\n// \"Icons are drawn, and so are separators\" (design-language.md section 4,\n// 2026-09-16): metadata is never joined with \"·\" and a breadcrumb is never a\n// string with \"›\" in it. Both apps had been building exactly those strings\n// (ui-review-2026-09, marker A); these two helpers make the same lines as DOM,\n// with the separator drawn by CSS (controls.css) or by the icon vocabulary.\n\n/**\n * A metadata line: `parts` as spans in a flex row, separated by a drawn\n * 3px disc (a `::before` on every part after the first, in controls.css),\n * so `metaLine([\"6 runs\", \"200 max steps\", \"seed 4\"])` is what\n * \"6 runs · 200 max steps · seed 4\" used to be, without the typed dot.\n * Empty, null and undefined parts are skipped, so a caller can pass an\n * optional segment without branching.\n */\nexport function metaLine(parts: (string | Node | null | undefined)[]): HTMLElement {\n const row = el(\"span\", \"shell-meta\");\n for (const part of parts) {\n if (part === null || part === undefined || part === \"\") continue;\n const seg = el(\"span\", \"shell-meta-part\");\n seg.append(part);\n row.append(seg);\n }\n return row;\n}\n\nexport interface Crumb {\n label: string;\n /** Makes the crumb a button. Ignored on the last crumb, which is where the\n * reader already is. */\n onClick?: (event: MouseEvent) => void;\n}\n\n/**\n * A breadcrumb trail: each crumb a span, or a button when it carries\n * `onClick` and is not the last, with `iconNode(\"forward\", 12)` drawn between\n * them. `breadcrumb([\"Box\", \"Deck\", \"Card\"])` is what \"Box › Deck › Card\"\n * used to be; the chevrons are the family's own drawing, not a symbol-font\n * character.\n */\nexport function breadcrumb(parts: (string | Crumb)[]): HTMLElement {\n const trail = el(\"nav\", \"shell-crumbs\");\n trail.setAttribute(\"aria-label\", \"Where this is\");\n parts.forEach((part, i) => {\n const crumb: Crumb = typeof part === \"string\" ? { label: part } : part;\n const last = i === parts.length - 1;\n if (i > 0) {\n const sep = iconNode(\"forward\", 12);\n sep.classList.add(\"shell-crumb-sep\");\n trail.append(sep);\n }\n if (crumb.onClick && !last) {\n const b = el(\"button\", { className: \"shell-crumb shell-crumb-link\", text: crumb.label, onClick: crumb.onClick });\n b.type = \"button\";\n trail.append(b);\n } else {\n const s = el(\"span\", \"shell-crumb\", crumb.label);\n if (last) s.setAttribute(\"aria-current\", \"location\");\n trail.append(s);\n }\n });\n return trail;\n}\n","// ---------------------------------------------------------------------------\n// The exit half of the panel motion vocabulary.\n//\n// Enters are pure CSS: they run on insertion. Exits cannot be, because the node\n// has to OUTLIVE the close for the animation to play and then be torn down. So\n// every closing surface routes its teardown through here instead of removing\n// itself directly.\n//\n// Patterpad's `closeWithExit`, lifted. It was already app-agnostic, and the\n// shell had grown a second copy of the same twelve lines inside `confirm.ts`,\n// which is the drift this package exists to stop: one motion vocabulary, drawn\n// one way. `tokens.css` already carried the keyframes and the durations; this\n// is the missing half that plays them.\n// ---------------------------------------------------------------------------\n\n/**\n * Play the close animation on `element`, then run `done` exactly once.\n *\n * `done` does the real teardown: remove the node, drop listeners, unmount\n * whatever was inside it. Under `prefers-reduced-motion` the duration reads 0\n * and `done` runs synchronously, so reduced motion means instant rather than\n * an invisible wait.\n */\nexport function closeWithExit(element: HTMLElement, done: () => void): void {\n element.classList.add(\"closing\");\n // getComputedStyle flushes pending style, so this reflects the `.closing`\n // rule we just added rather than the state before it.\n const seconds = parseFloat(getComputedStyle(element).animationDuration) || 0;\n if (seconds === 0) { done(); return; }\n\n let fired = false;\n const finish = (): void => {\n if (fired) return;\n fired = true;\n element.removeEventListener(\"animationend\", onEnd);\n clearTimeout(timer);\n done();\n };\n // Only OUR animation ending counts. Content inside the panel (rows, pills,\n // spinners) raises its own animationend, and an inner one firing first would\n // tear the panel down mid-fade.\n const onEnd = (e: AnimationEvent): void => { if (e.target === element) finish(); };\n element.addEventListener(\"animationend\", onEnd);\n // Fallback: animationend is missed if the element is hidden or the animation\n // is interrupted, and a panel that never tears down is worse than an abrupt one.\n const timer = setTimeout(finish, seconds * 1000 + 120);\n}\n","// ---------------------------------------------------------------------------\n// dialogFrame: the one modal skeleton every dialog in the family sits on.\n//\n// Title, an optional sub line, a body the caller fills, an actions row the\n// caller fills, on a native <dialog> so the focus trap and Escape come from the\n// platform. Four scrims and three panel drawings had grown across five shell\n// files and a dozen app dialogs (ui-review-2026-09, findings 9 and 30); this is\n// the frame they all sit on now, and the shell's own dialogs (confirm, identity,\n// about, notes) were moved onto it first, which is how it is proven.\n//\n// What the frame owns: the backdrop, the panel, the enter and exit motion\n// (closeWithExit), Escape, and giving focus back to whatever had it when the\n// dialog opened. What it does not own: the content classes, the buttons'\n// meaning, and when to close. The caller keeps those.\n//\n// Styles ship as dialog.css. The action buttons are expected to wear `.btn`\n// from controls.css; the frame does not draw buttons of its own.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\nimport { closeWithExit } from \"./exit.js\";\n\nexport interface DialogFrameOptions {\n /** The heading. Also the dialog's accessible name. */\n title: string;\n /** One plain sentence under the heading, if the title needs one. */\n sub?: string;\n /** Extra class(es) on the <dialog>, for the caller's own content rules and\n * width (`--dialog-width` on the class; see dialog.css). */\n className?: string;\n /** Runs once, after the dialog has left the document, however it closed. */\n onClose?: () => void;\n}\n\nexport interface DialogFrame {\n readonly dialog: HTMLDialogElement;\n /** Fill this with the content. */\n readonly body: HTMLElement;\n /** Fill this with the buttons, right-aligned; empty, it draws nothing. */\n readonly actions: HTMLElement;\n /** Show modally. Records what had focus so `close()` can give it back. */\n open(): void;\n /** Play the exit, tear down, restore focus. Idempotent. */\n close(): void;\n isOpen(): boolean;\n}\n\n/** Build a dialog on the family's frame. Appended to <body> at once (so an\n * `open()` on the next line has nothing to wait for); shown only on `open()`. */\nexport function dialogFrame(opts: DialogFrameOptions): DialogFrame {\n const dialog = el(\"dialog\", `shell-dialog ${opts.className ?? \"\"}`.trim());\n const body = el(\"div\", \"shell-dialog-body\");\n const actions = el(\"div\", \"shell-dialog-actions\");\n dialog.append(el(\"h2\", \"shell-dialog-title\", opts.title));\n if (opts.sub !== undefined) dialog.append(el(\"p\", \"shell-dialog-sub\", opts.sub));\n dialog.append(body, actions);\n dialog.setAttribute(\"aria-label\", opts.title);\n document.body.append(dialog);\n\n let state: \"closed\" | \"open\" | \"closing\" = \"closed\";\n let opener: Element | null = null;\n\n const teardown = (): void => {\n dialog.remove();\n // Back to the control that opened this. The platform does it for a plain\n // dialog.close(), but the exit motion ends in a remove(), and a removed\n // dialog restores nothing.\n if (opener instanceof HTMLElement && opener.isConnected) opener.focus();\n opener = null;\n state = \"closed\";\n opts.onClose?.();\n };\n\n const close = (): void => {\n if (state !== \"open\") return;\n state = \"closing\";\n closeWithExit(dialog, () => { if (dialog.open) dialog.close(); teardown(); });\n };\n\n // Escape arrives as the cancel event; route it through the exit motion.\n dialog.addEventListener(\"cancel\", (e) => { e.preventDefault(); close(); });\n // Closed by someone else (a form with method=\"dialog\", or a direct\n // dialog.close()): no motion to play, tear down at once.\n dialog.addEventListener(\"close\", () => { if (state === \"open\") teardown(); });\n\n return {\n dialog, body, actions,\n open() {\n if (state !== \"closed\") return;\n opener = document.activeElement;\n state = \"open\";\n dialog.showModal();\n },\n close,\n isOpen: () => state === \"open\",\n };\n}\n","// ---------------------------------------------------------------------------\n// The updater's VIEW: the themed prompt main summons over `updater:prompt`\n// (updater.ts) and reads the chosen button index back from, the same contract\n// as dialog.showMessageBox's `response`. The renderer half of the updater is\n// not optional: an unanswered prompt does not fall back to a native dialog,\n// the shell waits 300 seconds and resolves its own fallback index. Both apps\n// had written this view by hand (55 of 60 lines identical); this is the one\n// they share, on the family's dialog frame, the way save.ts has save-view.ts.\n//\n// Storyletter's copy, plus Patterpad's `links` row as an option (an About box\n// used to share this dialog there). Styles ship as updater.css.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\nimport { dialogFrame } from \"./dialog.js\";\nimport type { UpdaterDownloadProgress, UpdaterPromptOptions } from \"./updater.js\";\n\nexport interface UpdaterLink { label: string; url: string }\n\nexport interface UpdaterViewOptions extends UpdaterPromptOptions {\n /** Web links under the detail. Opened through `openExternal`, never by\n * navigating the window. */\n links?: UpdaterLink[];\n /** How a link opens (the app's allow-listed shell.openExternal bridge).\n * Required for `links` to do anything. */\n openExternal?: (url: string) => void;\n}\n\n// The progress row of the open `progress: true` dialog, if any. These are\n// modal, so one at a time, so one slot is enough. Closing clears it.\nlet liveProgress: { bar: HTMLElement; label: HTMLElement } | null = null;\n\nconst mb = (n: number): string => (n / (1024 * 1024)).toFixed(1);\n\n/** Route a download-progress tick into the open dialog's bar. No-op when none\n * is showing. Registered once at boot on the app's progress channel. */\nexport function feedUpdaterDownloadProgress(p: UpdaterDownloadProgress): void {\n if (!liveProgress) return;\n const pct = Math.max(0, Math.min(100, p.percent));\n liveProgress.bar.style.width = `${pct}%`;\n liveProgress.label.textContent =\n `${pct.toFixed(0)}%, ${mb(p.transferred)} of ${mb(p.total)} MB (${mb(p.bytesPerSecond)} MB/s)`;\n}\n\n/** Show the prompt as a modal themed dialog; resolve the index of the clicked\n * button (Esc resolves `cancelId`, which defaults to the last button). */\nexport function showUpdaterDialog(opts: UpdaterViewOptions): Promise<number> {\n return new Promise((resolve) => {\n const defaultId = opts.defaultId ?? 0;\n const cancelId = opts.cancelId ?? opts.buttons.length - 1;\n\n let done = false;\n const finish = (idx: number): void => {\n if (done) return;\n done = true;\n liveProgress = null; // stop feeding a dialog that is gone\n resolve(idx);\n frame.close();\n };\n const frame = dialogFrame({\n title: opts.message, className: \"updater-dialog\",\n // Esc is the cancel button, not a further answer: main is waiting on an index.\n onClose: () => finish(cancelId),\n });\n if (opts.detail) frame.body.append(el(\"p\", \"updater-detail\", opts.detail));\n\n if (opts.progress) {\n const track = el(\"div\", \"updater-progress-track\");\n const bar = el(\"div\", \"updater-progress-bar\");\n track.append(bar);\n const label = el(\"p\", \"updater-progress-label\", \"Starting download…\");\n frame.body.append(track, label);\n liveProgress = { bar, label };\n }\n\n if (opts.links?.length) {\n const row = el(\"p\", \"updater-links\");\n for (const link of opts.links) {\n const a = el(\"a\", undefined, link.label);\n a.href = link.url;\n a.addEventListener(\"click\", (e) => { e.preventDefault(); opts.openExternal?.(link.url); });\n row.append(a);\n }\n frame.body.append(row);\n }\n\n opts.buttons.forEach((label, i) => {\n // The default is the affirmative and wears the accent; every other\n // button, the way out included, is the plain one.\n const b = el(\"button\", i === defaultId ? \"btn primary\" : \"btn\", label);\n b.type = \"button\";\n b.addEventListener(\"click\", () => finish(i));\n frame.actions.append(b);\n if (i === defaultId) queueMicrotask(() => b.focus());\n });\n\n frame.open();\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/tooltip.ts","../src/dom.ts","../src/exit.ts","../src/dialog.ts","../src/updater-view.ts"],"names":["el","timer"],"mappings":"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","file":"updater-view.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// A single themed tooltip the family controls, replacing native `title`\n// rollovers: those are unstyled OS chrome and appear on the platform's slow\n// (~1s) delay, which is exactly the seam design-language section 4 (\"coherent\n// to the edges\") says not to leave showing.\n//\n// ONE floating bubble, document-DELEGATED: any element carrying a `data-tip`\n// attribute gets it, so a caller just sets `data-tip=\"...\"` anywhere and wires\n// nothing. The bubble anchors above the element (flipping below near the top\n// edge), uses our own snappy delay and the shared panel-enter motion.\n// `initTooltips()` is idempotent, so it is safe to call from more than one\n// place in an app that has several entry points.\n//\n// Lifted from Patterpad's patterpad-surface/web/tooltip.ts, which proved it,\n// with the one app-specific rule (suppressed in Writing View) generalised into\n// the `suppressed` option so the shell knows nothing about either app's modes.\n// ---------------------------------------------------------------------------\n\n// Bold markers: a producer wraps a span in these PRIVATE control chars (U+0002 STX / U+0003 ETX,\n// never present in user text) and the renderer turns it into a <strong>. Lets a data-tip bold part\n// of itself (a name, a \"VO:\" prefix) without any HTML, and each segment is still set via\n// textContent, so there is no injection risk from user content.\nconst B_OPEN = \"\\u0002\";\nconst B_CLOSE = \"\\u0003\";\n\n/** Wrap `s` so it renders bold inside a tooltip (see the marker note above). */\nexport function tipBold(s: string): string { return `${B_OPEN}${s}${B_CLOSE}`; }\n\n/** Render `text` into `host`, turning `\\u0002…\\u0003`-marked spans into <strong>; all set as text (safe). */\nfunction renderTip(host: HTMLElement, text: string): void {\n host.textContent = \"\";\n const re = /\\u0002([^\\u0003]*)\\u0003/g;\n let last = 0;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text)) !== null) {\n if (m.index > last) host.append(text.slice(last, m.index));\n const strong = document.createElement(\"strong\");\n strong.textContent = m[1] ?? \"\";\n host.append(strong);\n last = m.index + m[0].length;\n }\n if (last < text.length) host.append(text.slice(last));\n}\n\nexport interface TooltipOptions {\n /** Called before every show: return true to keep the bubble down. For a mode\n * that wants no chrome at all (Patterpad's Writing View). */\n suppressed?: () => boolean;\n}\n\nlet inited = false;\nlet checked = false; // the one-shot no-host warning below has been scheduled\nlet suppressed: (() => boolean) | undefined;\nconst SHOW_DELAY = 350; // ms - snappier than the OS title delay, slow enough not to flicker on pass-through\nconst EDGE = 6; // viewport inset so the bubble never touches the window edge\nconst GAP = 7; // px between the anchor and the bubble\n\nlet tip: HTMLDivElement | null = null;\nlet timer = 0;\nlet active: HTMLElement | null = null;\n/** The subject of a rect-anchored tip (see `tipAt`), when there is one. */\nlet activeKey: string | null = null;\n\nfunction ensureEl(): HTMLDivElement {\n if (tip) return tip;\n const el = document.createElement(\"div\");\n el.className = \"tooltip\";\n el.setAttribute(\"role\", \"tooltip\");\n el.hidden = true;\n document.body.appendChild(el);\n tip = el;\n return el;\n}\n\n/** A rectangle in VIEWPORT coordinates, for anchoring a tip to something that is\n * not a DOM element: a shape on a canvas, a run of text inside one. */\nexport interface TipRect { left: number; top: number; width: number; height: number }\n\nconst rectOf = (anchor: HTMLElement | TipRect): TipRect =>\n (anchor instanceof HTMLElement ? anchor.getBoundingClientRect() : anchor);\n\n/** Anchor the (already-visible, so measurable) bubble above the anchor, flipping below near the top. */\nfunction place(anchor: HTMLElement | TipRect): void {\n const t = ensureEl();\n const r = rectOf(anchor);\n const tr = t.getBoundingClientRect();\n let top = r.top - tr.height - GAP;\n if (top < EDGE) top = r.top + r.height + GAP; // not enough room above -> below\n let left = r.left + r.width / 2 - tr.width / 2;\n left = Math.max(EDGE, Math.min(left, window.innerWidth - tr.width - EDGE));\n t.style.left = `${Math.round(left)}px`;\n t.style.top = `${Math.round(top)}px`;\n}\n\nfunction show(anchor: HTMLElement): void {\n const text = anchor.dataset[\"tip\"];\n if (!text) return;\n paint(anchor, text);\n}\n\n/** Fill, place and reveal the bubble. The one path: both the delegated\n * `data-tip` route and `tipAt` end up here, so a canvas tip and a button tip\n * are the same bubble with the same look and the same motion. */\nfunction paint(anchor: HTMLElement | TipRect, text: string): void {\n if (suppressed?.()) return;\n const t = ensureEl();\n // A modal <dialog> (settings, a confirm) renders in the browser TOP LAYER, above every normal-flow\n // element whatever its z-index. If the anchor is inside one, move the bubble into that dialog so it\n // shares the top layer; otherwise keep it on <body>. Re-parenting per show also moves it back out\n // once the modal closes.\n const host: HTMLElement = (anchor instanceof HTMLElement ? anchor.closest<HTMLElement>(\"dialog[open]\") : null)\n ?? document.body;\n if (t.parentElement !== host) host.appendChild(t);\n active = anchor instanceof HTMLElement ? anchor : null;\n renderTip(t, text);\n t.hidden = false;\n t.classList.remove(\"show\"); void t.offsetWidth; // restart the enter animation\n place(anchor); // measure + position now it has content + is visible\n t.classList.add(\"show\");\n}\n\nfunction hide(): void {\n if (timer) { window.clearTimeout(timer); timer = 0; }\n active = null;\n activeKey = null;\n if (tip) { tip.hidden = true; tip.classList.remove(\"show\"); }\n}\n\nfunction schedule(anchor: HTMLElement): void {\n if (anchor === active) return;\n if (timer) window.clearTimeout(timer);\n timer = window.setTimeout(() => show(anchor), SHOW_DELAY);\n}\n\n/**\n * Show a tip for something that has no DOM node of its own: a card on a canvas,\n * a zone, a pin. The caller says WHERE (a viewport rectangle) and WHAT, and\n * identifies the subject with `key`.\n *\n * The key is what makes this usable from a canvas. Re-calling with the same key\n * re-places the bubble WITHOUT restarting the delay, so a tip can follow its\n * shape while the camera moves; calling with a different key starts the wait\n * again, so sweeping the pointer across a dense board does not strobe.\n *\n * The bubble is ordinary DOM, so it holds its size whatever the canvas is\n * zoomed to. That is the point of it: the label it stands in for is the one\n * thing on a canvas that must not shrink with everything else.\n */\nexport function tipAt(key: string, rect: TipRect, text: string): void {\n if (key === activeKey) {\n // Already up for this subject: move it, do not re-animate it.\n if (tip && !tip.hidden) { renderTip(tip, text); place(rect); }\n return;\n }\n // A different subject: down first, then wait again. Leaving the old bubble up\n // during the wait would put one card's name over another card, which is worse\n // than no name at all - and it matches the delegated route, where leaving an\n // element hides the tip before the next one is scheduled.\n hide();\n activeKey = key;\n timer = window.setTimeout(() => { timer = 0; paint(rect, text); }, SHOW_DELAY);\n}\n\n/** Take any tip down: the pointer has left, or a gesture has started. */\nexport function hideTip(): void { hide(); }\n\n/**\n * Mount the renderer because something is about to speak the grammar.\n *\n * The themed tooltip is not a feature a host turns on; it is the renderer for a grammar the shell's own\n * components already speak, and `data-tip` with nothing listening draws NOTHING - not the themed\n * bubble, not the platform one. That silence cost Patterpad two releases of a pin with no tooltip, and\n * cost it again when a `title` was converted to `data-tip` in a window that had never mounted this.\n *\n * So `el` calls this whenever it sets a tip. Mounting a delegated listener is the same class of act as\n * `confirmDialog` appending to `document.body`, which this package has always done.\n */\nexport function ensureTooltipHost(): void { initTooltips(); }\n\n/**\n * One deferred check that an app writing `data-tip` ITSELF has a host, warning if not.\n *\n * `ensureTooltipHost` covers anything built through `el`. It cannot cover a `data-tip` written straight\n * into markup or set by hand, which is the case that leaves a whole window of dead tips with no shell\n * component involved - so that one gets a warning rather than silence. Scheduled once, after the\n * current task, so it sees a rendered document rather than an empty one.\n */\nexport function checkTooltipHost(): void {\n if (checked) return;\n checked = true;\n setTimeout(() => {\n if (inited || typeof document === \"undefined\") return;\n if (document.querySelector(\"[data-tip]\")) {\n console.warn(\"app-shell: something set `data-tip` but no tooltip host is mounted, so those rollovers will not draw. Call initTooltips() once per window.\");\n }\n }, 0);\n}\n\nconst tipAncestor = (n: EventTarget | null): HTMLElement | null =>\n (n instanceof Element ? n.closest<HTMLElement>(\"[data-tip]\") : null);\n\n/**\n * Wire the one delegated controller. Mounting happens once; OPTIONS do not.\n *\n * An explicit call is always authoritative about its options, even after something else has already\n * mounted the listener. That ordering is not hypothetical, and Storyletter caught it before it shipped:\n * once components self-mount (see `ensureTooltipHost`), a host's own\n * `initTooltips({ suppressed })` could land second and be dropped by an early return, silently taking\n * a rule like \"no tooltips in Writing View\" with it. Nothing would fail and nothing would show.\n *\n * Components therefore call this with NO arguments: they mount the renderer without claiming the\n * options, and the host keeps the last word on behaviour.\n */\nexport function initTooltips(opts: TooltipOptions = {}): void {\n if (opts.suppressed !== undefined) suppressed = opts.suppressed; // an explicit call always wins\n if (inited) return;\n inited = true;\n\n document.addEventListener(\"pointerover\", (e) => {\n const el = tipAncestor(e.target);\n if (el) schedule(el);\n });\n document.addEventListener(\"pointerout\", (e) => {\n const el = tipAncestor(e.target);\n if (!el) return;\n const to = (e as PointerEvent).relatedTarget;\n if (to instanceof Node && el.contains(to)) return; // still inside the same anchor\n hide();\n });\n // Keyboard parity: reveal on focus, dismiss on blur.\n document.addEventListener(\"focusin\", (e) => { const el = tipAncestor(e.target); if (el) schedule(el); });\n document.addEventListener(\"focusout\", () => hide());\n // It's ANCHORED (not cursor-following), so anything that moves the layout or context dismisses it.\n document.addEventListener(\"pointerdown\", () => hide(), true);\n document.addEventListener(\"scroll\", () => { if (active) hide(); }, true);\n document.addEventListener(\"keydown\", (e) => { if (e.key === \"Escape\") hide(); });\n window.addEventListener(\"blur\", () => hide());\n}\n","// ---------------------------------------------------------------------------\n// `el`: the tiny tag-typed element factory both apps build their imperative,\n// idempotent views with.\n//\n// A SUPERSET of the two apps' prior signatures, so neither rewrites call sites:\n// - Patterpad: el(tag, cls?, text?) (positional)\n// - Storylet Studio: el(tag, { className, text, ... }, ...kids) (options bag)\n// A string second argument is the className; an object is the props bag; string\n// children become text nodes. So `el(\"div\", \"c\", \"t\")` and\n// `el(\"div\", { className: \"c\" }, \"t\")` produce identical DOM.\n// ---------------------------------------------------------------------------\n\nimport { ensureTooltipHost, checkTooltipHost } from \"./tooltip.js\";\nimport { iconNode, iconNameOfGlyph, isIconName, type IconName } from \"./icons.js\";\n\nexport type Child = Node | string | null | undefined;\n\nexport interface ElProps {\n className?: string;\n text?: string;\n /** A NATIVE rollover. Prefer `tip`: `title` is OS chrome on the platform's own\n * slow delay, which is the seam the themed tooltip exists to close. */\n title?: string;\n /** A themed rollover (`data-tip`, picked up by the delegated controller that\n * `initTooltips()` wires). Also the accessible name, since the elements that\n * want one are usually icon buttons with no text of their own. */\n tip?: string;\n onClick?: (event: MouseEvent) => void;\n}\n\nexport function el<K extends keyof HTMLElementTagNameMap>(\n tag: K,\n classNameOrProps?: string | ElProps,\n ...children: Child[]\n): HTMLElementTagNameMap[K] {\n const node = document.createElement(tag);\n const props: ElProps = typeof classNameOrProps === \"string\"\n ? { className: classNameOrProps }\n : classNameOrProps ?? {};\n if (props.className) node.className = props.className;\n if (props.text !== undefined && props.text !== null) node.textContent = props.text;\n if (props.title !== undefined) node.title = props.title;\n if (props.tip !== undefined) {\n node.dataset[\"tip\"] = props.tip;\n node.setAttribute(\"aria-label\", props.tip);\n // Setting a tip mounts the renderer. Without this, `data-tip` draws nothing at all in a window whose\n // host never called `initTooltips()`, which is a silent failure rather than a degraded one. No\n // options passed: the host stays authoritative about behaviour (see tooltip.ts).\n ensureTooltipHost();\n }\n if (props.onClick) node.addEventListener(\"click\", props.onClick as EventListener);\n for (const child of children) {\n if (child === null || child === undefined) continue;\n node.append(child);\n }\n checkTooltipHost(); // one-shot: warns if the APP wrote data-tip and nothing mounted a host\n return node;\n}\n\n/** A small square icon button (the move/delete controls list editors share).\n * Its rollover is the THEMED one (`data-tip`), so an app that has called\n * `initTooltips()` gets our bubble rather than the platform's.\n *\n * `name` is a word from the vocabulary (\"up\", \"close\"), drawn at 14px. A\n * typed glyph from the deprecated `icon` table (\"↑\", \"✕\") is accepted for\n * now and drawn as the word it stood for, so an app that has not moved its\n * call sites yet still gets the drawn set; any other string is set as text,\n * which is the old behaviour and on its way out. */\nexport function iconBtn(name: IconName | (string & {}), title: string, onClick: () => void, disabled = false, danger = false): HTMLButtonElement {\n const b = el(\"button\", `shell-icon${danger ? \" danger\" : \"\"}`);\n b.type = \"button\"; b.dataset[\"tip\"] = title; b.setAttribute(\"aria-label\", title);\n const word = isIconName(name) ? name : iconNameOfGlyph(name);\n if (word) b.append(iconNode(word)); else b.textContent = name;\n b.disabled = disabled;\n b.addEventListener(\"click\", onClick);\n return b;\n}\n\n/**\n * A captioned field: `<label class=\"shell-labelled\"><span class=\"shell-fieldcap\">…</span>control</label>`.\n *\n * The label is pointed at the field it captions, EXPLICITLY, and never at a button. A `<label>` with no\n * `for` forwards a click to its first labelable descendant, and buttons are labelable - so a caption\n * wrapped around a control containing buttons (`tagChips`, whose every chip carries a remove button)\n * turns every click on the row's dead space into a press of the FIRST button in it. That shipped:\n * patterkit/patter#44, where clicking a Game Data list value anywhere but its ✕ deleted the first value\n * in the list. Reported as \"clicking a value removes the wrong one\", which is not a wrong-index bug at\n * all - the click never reached the chip.\n *\n * When the control holds no labelable field (a chips editor with the add input removed, say), this is a\n * plain `<div>`: no caption behaviour is better than a caption that presses something.\n */\nexport function labelled(label: string, control: HTMLElement): HTMLElement {\n const target = control.matches(\"input, select, textarea\")\n ? control\n : control.querySelector<HTMLElement>(\"input:not([type=button]):not([type=submit]), select, textarea\");\n const w = el(target ? \"label\" : \"div\", \"shell-labelled\");\n w.append(el(\"span\", \"shell-fieldcap\", label), control);\n if (target) {\n if (!target.id) target.id = `shell-f-${Math.random().toString(36).slice(2, 9)}`;\n (w as HTMLLabelElement).htmlFor = target.id;\n }\n return w;\n}\n\n/** Swap item `i` with its neighbour `i + delta` IN PLACE (the up/down reorder list editors share);\n * a no-op when the target is out of range. Returns whether anything moved. */\nexport function moveItem<T>(arr: T[], i: number, delta: number): boolean {\n const j = i + delta;\n if (j < 0 || j >= arr.length) return false;\n [arr[i], arr[j]] = [arr[j]!, arr[i]!];\n return true;\n}\n\n/** The icon size inside a chip's remove / move buttons: the chip is set at\n * 0.76rem, so its controls draw smaller than a toolbar button's 14px. */\nconst CHIP_ICON = 10;\n\n/** A tag-style editor for a string-list field (an enum's allowed values / flags): removable chips + an\n * add input (Enter or \",\" commits; blank / duplicate ignored). Mutates `holder.values` in place (read\n * back on save). `onChange` fires after any add / remove so callers can refresh a dependent control. */\nexport function tagChips(holder: { values?: string[] }, onChange?: () => void): HTMLElement {\n const wrap = el(\"div\", \"shell-tags\");\n const input = el(\"input\", \"shell-tag-input\");\n // Sentence-cased like every other hint (the microcopy ruling, 2026-08-28):\n // lowercase fragments read as adrift, and a placeholder is a hint speaking.\n input.type = \"text\"; input.placeholder = \"Add value\"; input.spellcheck = false;\n const makeChip = (v: string): HTMLElement => {\n const chip = el(\"span\", \"shell-tag\", v);\n const x = el(\"button\", \"shell-tag-x\", iconNode(\"close\", CHIP_ICON));\n x.type = \"button\"; x.dataset[\"tip\"] = `Remove ${v}`; x.setAttribute(\"aria-label\", `Remove ${v}`);\n x.addEventListener(\"click\", () => { holder.values = (holder.values ?? []).filter((o) => o !== v); chip.remove(); onChange?.(); });\n chip.append(x);\n return chip;\n };\n const commit = (): void => {\n const v = input.value.trim();\n if (v && !(holder.values ?? []).includes(v)) { (holder.values ??= []).push(v); wrap.insertBefore(makeChip(v), input); onChange?.(); }\n input.value = \"\";\n };\n input.addEventListener(\"keydown\", (e) => { if (e.key === \"Enter\" || e.key === \",\") { e.preventDefault(); commit(); } });\n input.addEventListener(\"blur\", commit);\n for (const v of holder.values ?? []) wrap.append(makeChip(v));\n wrap.append(input);\n return wrap;\n}\n\n/**\n * An ORDERED chips editor for a string-list field whose order means something\n * (a quality's stages: `advance()` walks them, so 2 comes after 1): numbered\n * chips, each with move-earlier / move-later / remove, plus an add input\n * (Enter or \",\" commits; blank / duplicate ignored). Mutates `holder.stages`\n * in place; `onChange` fires after any add / move / remove.\n *\n * Patterpad's, lifted (ui-review-2026-09, finding 3); Storyletter had been\n * faking it with `tagChips` over a `values` holder, which loses the order\n * controls. Same class family as `tagChips` (`.shell-tags`), so it is styled by\n * settings.css already.\n */\nexport function stageChips(holder: { stages?: string[] }, onChange?: () => void): HTMLElement {\n const wrap = el(\"div\", \"shell-tags shell-stages\");\n const input = el(\"input\", \"shell-tag-input\");\n input.type = \"text\"; input.placeholder = \"Add stage\"; input.spellcheck = false;\n const control = (name: IconName, tip: string, disabled: boolean, onClick: () => void): HTMLButtonElement => {\n const b = el(\"button\", \"shell-tag-x shell-tag-move\", iconNode(name, CHIP_ICON));\n b.type = \"button\"; b.dataset[\"tip\"] = tip; b.setAttribute(\"aria-label\", tip);\n b.disabled = disabled;\n b.addEventListener(\"click\", onClick);\n return b;\n };\n const rebuild = (): void => {\n for (const c of Array.from(wrap.children)) if (c !== input) c.remove();\n const stages = holder.stages ?? [];\n stages.forEach((v, i) => {\n const chip = el(\"span\", \"shell-tag\", `${i + 1}. ${v}`);\n chip.dataset[\"stage\"] = v;\n chip.append(\n control(\"back\", `Move ${v} earlier`, i === 0,\n () => { if (moveItem(stages, i, -1)) { rebuild(); onChange?.(); } }),\n control(\"forward\", `Move ${v} later`, i === stages.length - 1,\n () => { if (moveItem(stages, i, 1)) { rebuild(); onChange?.(); } }),\n );\n const x = el(\"button\", \"shell-tag-x\", iconNode(\"close\", CHIP_ICON));\n x.type = \"button\"; x.dataset[\"tip\"] = `Remove ${v}`; x.setAttribute(\"aria-label\", `Remove ${v}`);\n x.addEventListener(\"click\", () => { holder.stages = (holder.stages ?? []).filter((o) => o !== v); rebuild(); onChange?.(); });\n chip.append(x);\n wrap.insertBefore(chip, input);\n });\n };\n const commit = (): void => {\n const v = input.value.trim();\n if (v && !(holder.stages ?? []).includes(v)) { (holder.stages ??= []).push(v); rebuild(); onChange?.(); }\n input.value = \"\";\n };\n input.addEventListener(\"keydown\", (e) => { if (e.key === \"Enter\" || e.key === \",\") { e.preventDefault(); commit(); } });\n input.addEventListener(\"blur\", commit);\n wrap.append(input);\n rebuild();\n return wrap;\n}\n\n// --- drag-to-reorder ---------------------------------------------------------\n// One drag at a time per window, shared across every wired list, so the\n// midpoint test knows what is being carried without a dataTransfer round trip.\nlet reorderDragId: string | null = null;\nconst clearDropMarks = (host: ParentNode): void =>\n host.querySelectorAll(\".drop-before, .drop-after\").forEach((e) => e.classList.remove(\"drop-before\", \"drop-after\"));\n\n/**\n * Make `el` a drag source and a drop target for HTML5 drag-to-reorder among\n * its siblings. While carried it wears `.dragging`; a hovered target wears\n * `.drop-before` or `.drop-after` by the midpoint test on `axis` (\"y\" for\n * rows, \"x\" for a wrapping grid); a drop calls `onMove(draggedId, before,\n * targetId)` and the caller reorders its model and re-renders.\n *\n * Storyletter's `wireCardDrag`, lifted (ui-review-2026-09, finding 30);\n * Patterpad's `wireSceneDrag` is the same idea fixed on \"y\". The marks are\n * classes only: the app draws them (Patterpad's `box-shadow: 0 -2px 0 0\n * var(--accent)` pair is the family's look).\n */\nexport function wireReorder(el: HTMLElement, id: string, axis: \"x\" | \"y\", onMove: (id: string, before: boolean, targetId: string) => void): void {\n el.draggable = true;\n el.dataset[\"reorderId\"] = id;\n const host = (): ParentNode => el.parentElement ?? el;\n el.addEventListener(\"dragstart\", (e) => {\n reorderDragId = id; el.classList.add(\"dragging\");\n if (e.dataTransfer) { e.dataTransfer.effectAllowed = \"move\"; e.dataTransfer.setData(\"text/plain\", id); }\n });\n el.addEventListener(\"dragend\", () => { reorderDragId = null; el.classList.remove(\"dragging\"); clearDropMarks(host()); });\n el.addEventListener(\"dragover\", (e) => {\n if (!reorderDragId || reorderDragId === id) return;\n e.preventDefault();\n if (e.dataTransfer) e.dataTransfer.dropEffect = \"move\";\n const r = el.getBoundingClientRect();\n const before = axis === \"y\" ? e.clientY < r.top + r.height / 2 : e.clientX < r.left + r.width / 2;\n clearDropMarks(host());\n el.classList.add(before ? \"drop-before\" : \"drop-after\");\n });\n el.addEventListener(\"dragleave\", () => el.classList.remove(\"drop-before\", \"drop-after\"));\n el.addEventListener(\"drop\", (e) => {\n e.preventDefault();\n const before = el.classList.contains(\"drop-before\");\n clearDropMarks(host());\n if (reorderDragId && reorderDragId !== id) onMove(reorderDragId, before, id);\n });\n}\n\n// --- metadata and breadcrumbs ---------------------------------------------------\n// \"Icons are drawn, and so are separators\" (design-language.md section 4,\n// 2026-09-16): metadata is never joined with \"·\" and a breadcrumb is never a\n// string with \"›\" in it. Both apps had been building exactly those strings\n// (ui-review-2026-09, marker A); these two helpers make the same lines as DOM,\n// with the separator drawn by CSS (controls.css) or by the icon vocabulary.\n\n/**\n * A metadata line: `parts` as spans in a flex row, separated by a drawn\n * 3px disc (a `::before` on every part after the first, in controls.css),\n * so `metaLine([\"6 runs\", \"200 max steps\", \"seed 4\"])` is what\n * \"6 runs · 200 max steps · seed 4\" used to be, without the typed dot.\n * Empty, null and undefined parts are skipped, so a caller can pass an\n * optional segment without branching.\n */\nexport function metaLine(parts: (string | Node | null | undefined)[]): HTMLElement {\n const row = el(\"span\", \"shell-meta\");\n for (const part of parts) {\n if (part === null || part === undefined || part === \"\") continue;\n const seg = el(\"span\", \"shell-meta-part\");\n seg.append(part);\n row.append(seg);\n }\n return row;\n}\n\nexport interface Crumb {\n label: string;\n /** Makes the crumb a button. Ignored on the last crumb, which is where the\n * reader already is. */\n onClick?: (event: MouseEvent) => void;\n}\n\n/**\n * A breadcrumb trail: each crumb a span, or a button when it carries\n * `onClick` and is not the last, with `iconNode(\"forward\", 12)` drawn between\n * them. `breadcrumb([\"Box\", \"Deck\", \"Card\"])` is what \"Box › Deck › Card\"\n * used to be; the chevrons are the family's own drawing, not a symbol-font\n * character.\n */\nexport function breadcrumb(parts: (string | Crumb)[]): HTMLElement {\n const trail = el(\"nav\", \"shell-crumbs\");\n trail.setAttribute(\"aria-label\", \"Where this is\");\n parts.forEach((part, i) => {\n const crumb: Crumb = typeof part === \"string\" ? { label: part } : part;\n const last = i === parts.length - 1;\n if (i > 0) {\n const sep = iconNode(\"forward\", 12);\n sep.classList.add(\"shell-crumb-sep\");\n trail.append(sep);\n }\n if (crumb.onClick && !last) {\n const b = el(\"button\", { className: \"shell-crumb shell-crumb-link\", text: crumb.label, onClick: crumb.onClick });\n b.type = \"button\";\n trail.append(b);\n } else {\n const s = el(\"span\", \"shell-crumb\", crumb.label);\n if (last) s.setAttribute(\"aria-current\", \"location\");\n trail.append(s);\n }\n });\n return trail;\n}\n","// ---------------------------------------------------------------------------\n// The exit half of the panel motion vocabulary.\n//\n// Enters are pure CSS: they run on insertion. Exits cannot be, because the node\n// has to OUTLIVE the close for the animation to play and then be torn down. So\n// every closing surface routes its teardown through here instead of removing\n// itself directly.\n//\n// Patterpad's `closeWithExit`, lifted. It was already app-agnostic, and the\n// shell had grown a second copy of the same twelve lines inside `confirm.ts`,\n// which is the drift this package exists to stop: one motion vocabulary, drawn\n// one way. `tokens.css` already carried the keyframes and the durations; this\n// is the missing half that plays them.\n// ---------------------------------------------------------------------------\n\n/**\n * Play the close animation on `element`, then run `done` exactly once.\n *\n * `done` does the real teardown: remove the node, drop listeners, unmount\n * whatever was inside it. Under `prefers-reduced-motion` the duration reads 0\n * and `done` runs synchronously, so reduced motion means instant rather than\n * an invisible wait.\n */\nexport function closeWithExit(element: HTMLElement, done: () => void): void {\n element.classList.add(\"closing\");\n // getComputedStyle flushes pending style, so this reflects the `.closing`\n // rule we just added rather than the state before it.\n const seconds = parseFloat(getComputedStyle(element).animationDuration) || 0;\n if (seconds === 0) { done(); return; }\n\n let fired = false;\n const finish = (): void => {\n if (fired) return;\n fired = true;\n element.removeEventListener(\"animationend\", onEnd);\n clearTimeout(timer);\n done();\n };\n // Only OUR animation ending counts. Content inside the panel (rows, pills,\n // spinners) raises its own animationend, and an inner one firing first would\n // tear the panel down mid-fade.\n const onEnd = (e: AnimationEvent): void => { if (e.target === element) finish(); };\n element.addEventListener(\"animationend\", onEnd);\n // Fallback: animationend is missed if the element is hidden or the animation\n // is interrupted, and a panel that never tears down is worse than an abrupt one.\n const timer = setTimeout(finish, seconds * 1000 + 120);\n}\n","// ---------------------------------------------------------------------------\n// dialogFrame: the one modal skeleton every dialog in the family sits on.\n//\n// Title, an optional sub line, a body the caller fills, an actions row the\n// caller fills, on a native <dialog> so the focus trap and Escape come from the\n// platform. Four scrims and three panel drawings had grown across five shell\n// files and a dozen app dialogs (ui-review-2026-09, findings 9 and 30); this is\n// the frame they all sit on now, and the shell's own dialogs (confirm, identity,\n// about, notes) were moved onto it first, which is how it is proven.\n//\n// What the frame owns: the backdrop, the panel, the enter and exit motion\n// (closeWithExit), Escape, and giving focus back to whatever had it when the\n// dialog opened. What it does not own: the content classes, the buttons'\n// meaning, and when to close. The caller keeps those.\n//\n// Styles ship as dialog.css. The action buttons are expected to wear `.btn`\n// from controls.css; the frame does not draw buttons of its own.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\nimport { closeWithExit } from \"./exit.js\";\n\nexport interface DialogFrameOptions {\n /** The heading. Also the dialog's accessible name. */\n title: string;\n /** One plain sentence under the heading, if the title needs one. */\n sub?: string;\n /** Extra class(es) on the <dialog>, for the caller's own content rules and\n * width (`--dialog-width` on the class; see dialog.css). */\n className?: string;\n /** Runs once, after the dialog has left the document, however it closed. */\n onClose?: () => void;\n}\n\nexport interface DialogFrame {\n readonly dialog: HTMLDialogElement;\n /** Fill this with the content. */\n readonly body: HTMLElement;\n /** Fill this with the buttons, right-aligned; empty, it draws nothing. */\n readonly actions: HTMLElement;\n /** Show modally. Records what had focus so `close()` can give it back. */\n open(): void;\n /** Play the exit, tear down, restore focus. Idempotent. */\n close(): void;\n isOpen(): boolean;\n}\n\n/** Build a dialog on the family's frame. Appended to <body> at once (so an\n * `open()` on the next line has nothing to wait for); shown only on `open()`. */\nexport function dialogFrame(opts: DialogFrameOptions): DialogFrame {\n const dialog = el(\"dialog\", `shell-dialog ${opts.className ?? \"\"}`.trim());\n const body = el(\"div\", \"shell-dialog-body\");\n const actions = el(\"div\", \"shell-dialog-actions\");\n dialog.append(el(\"h2\", \"shell-dialog-title\", opts.title));\n if (opts.sub !== undefined) dialog.append(el(\"p\", \"shell-dialog-sub\", opts.sub));\n dialog.append(body, actions);\n dialog.setAttribute(\"aria-label\", opts.title);\n document.body.append(dialog);\n\n let state: \"closed\" | \"open\" | \"closing\" = \"closed\";\n let opener: Element | null = null;\n\n const teardown = (): void => {\n dialog.remove();\n // Back to the control that opened this. The platform does it for a plain\n // dialog.close(), but the exit motion ends in a remove(), and a removed\n // dialog restores nothing.\n if (opener instanceof HTMLElement && opener.isConnected) opener.focus();\n opener = null;\n state = \"closed\";\n opts.onClose?.();\n };\n\n const close = (): void => {\n if (state !== \"open\") return;\n state = \"closing\";\n closeWithExit(dialog, () => { if (dialog.open) dialog.close(); teardown(); });\n };\n\n // Escape arrives as the cancel event; route it through the exit motion.\n dialog.addEventListener(\"cancel\", (e) => { e.preventDefault(); close(); });\n // Closed by someone else (a form with method=\"dialog\", or a direct\n // dialog.close()): no motion to play, tear down at once.\n dialog.addEventListener(\"close\", () => { if (state === \"open\") teardown(); });\n\n return {\n dialog, body, actions,\n open() {\n if (state !== \"closed\") return;\n opener = document.activeElement;\n state = \"open\";\n dialog.showModal();\n },\n close,\n isOpen: () => state === \"open\",\n };\n}\n","// ---------------------------------------------------------------------------\n// The updater's VIEW: the themed prompt main summons over `updater:prompt`\n// (updater.ts) and reads the chosen button index back from, the same contract\n// as dialog.showMessageBox's `response`. The renderer half of the updater is\n// not optional: an unanswered prompt does not fall back to a native dialog,\n// the shell waits 300 seconds and resolves its own fallback index. Both apps\n// had written this view by hand (55 of 60 lines identical); this is the one\n// they share, on the family's dialog frame, the way save.ts has save-view.ts.\n//\n// Storyletter's copy, plus Patterpad's `links` row as an option (an About box\n// used to share this dialog there). Styles ship as updater.css.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\nimport { dialogFrame } from \"./dialog.js\";\nimport type { UpdaterDownloadProgress, UpdaterPromptOptions } from \"./updater.js\";\n\nexport interface UpdaterLink { label: string; url: string }\n\nexport interface UpdaterViewOptions extends UpdaterPromptOptions {\n /** Web links under the detail. Opened through `openExternal`, never by\n * navigating the window. */\n links?: UpdaterLink[];\n /** How a link opens (the app's allow-listed shell.openExternal bridge).\n * Required for `links` to do anything. */\n openExternal?: (url: string) => void;\n}\n\n// The progress row of the open `progress: true` dialog, if any. These are\n// modal, so one at a time, so one slot is enough. Closing clears it.\nlet liveProgress: { bar: HTMLElement; label: HTMLElement } | null = null;\n\nconst mb = (n: number): string => (n / (1024 * 1024)).toFixed(1);\n\n/** Route a download-progress tick into the open dialog's bar. No-op when none\n * is showing. Registered once at boot on the app's progress channel. */\nexport function feedUpdaterDownloadProgress(p: UpdaterDownloadProgress): void {\n if (!liveProgress) return;\n const pct = Math.max(0, Math.min(100, p.percent));\n liveProgress.bar.style.width = `${pct}%`;\n liveProgress.label.textContent =\n `${pct.toFixed(0)}%, ${mb(p.transferred)} of ${mb(p.total)} MB (${mb(p.bytesPerSecond)} MB/s)`;\n}\n\n/** Show the prompt as a modal themed dialog; resolve the index of the clicked\n * button (Esc resolves `cancelId`, which defaults to the last button). */\nexport function showUpdaterDialog(opts: UpdaterViewOptions): Promise<number> {\n return new Promise((resolve) => {\n const defaultId = opts.defaultId ?? 0;\n const cancelId = opts.cancelId ?? opts.buttons.length - 1;\n\n let done = false;\n const finish = (idx: number): void => {\n if (done) return;\n done = true;\n liveProgress = null; // stop feeding a dialog that is gone\n resolve(idx);\n frame.close();\n };\n const frame = dialogFrame({\n title: opts.message, className: \"updater-dialog\",\n // Esc is the cancel button, not a further answer: main is waiting on an index.\n onClose: () => finish(cancelId),\n });\n if (opts.detail) frame.body.append(el(\"p\", \"updater-detail\", opts.detail));\n\n if (opts.progress) {\n const track = el(\"div\", \"updater-progress-track\");\n const bar = el(\"div\", \"updater-progress-bar\");\n track.append(bar);\n const label = el(\"p\", \"updater-progress-label\", \"Starting download…\");\n frame.body.append(track, label);\n liveProgress = { bar, label };\n }\n\n if (opts.links?.length) {\n const row = el(\"p\", \"updater-links\");\n for (const link of opts.links) {\n const a = el(\"a\", undefined, link.label);\n a.href = link.url;\n a.addEventListener(\"click\", (e) => { e.preventDefault(); opts.openExternal?.(link.url); });\n row.append(a);\n }\n frame.body.append(row);\n }\n\n opts.buttons.forEach((label, i) => {\n // The default is the affirmative and wears the accent; every other\n // button, the way out included, is the plain one.\n const b = el(\"button\", i === defaultId ? \"btn primary\" : \"btn\", label);\n b.type = \"button\";\n b.addEventListener(\"click\", () => finish(i));\n frame.actions.append(b);\n // Focus the default, unless nobody asked for this prompt: then the\n // cancel button, so a stray Enter or Space dismisses it, never says yes.\n if (i === (opts.unasked ? cancelId : defaultId)) queueMicrotask(() => b.focus());\n });\n\n frame.open();\n });\n}\n"]}
@@ -1,4 +1,4 @@
1
- export { feedUpdaterDownloadProgress, showUpdaterDialog } from './chunk-OPZPFOPS.js';
1
+ export { feedUpdaterDownloadProgress, showUpdaterDialog } from './chunk-WKPKBAXS.js';
2
2
  import './chunk-YRFJLJUH.js';
3
3
  import './chunk-UG2P777F.js';
4
4
  import './chunk-IU7EY6BI.js';
package/dist/updater.cjs CHANGED
@@ -9,6 +9,47 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
9
 
10
10
  var electronUpdater__default = /*#__PURE__*/_interopDefault(electronUpdater);
11
11
 
12
+ // src/updater.ts
13
+ var macPinned = /* @__PURE__ */ new Set();
14
+ var pinsHeld = 0;
15
+ var heldChildren = /* @__PURE__ */ new Map();
16
+ function frontForModal(main, platform = process.platform) {
17
+ if (main.isDestroyed()) return () => {
18
+ };
19
+ pinsHeld++;
20
+ if (platform === "darwin") {
21
+ for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(false);
22
+ } else {
23
+ for (const w of electron.BrowserWindow.getAllWindows()) {
24
+ if (w === main || w.isDestroyed()) continue;
25
+ const owner = w.getParentWindow();
26
+ if (owner !== main) continue;
27
+ heldChildren.set(w, main);
28
+ w.setParentWindow(null);
29
+ }
30
+ }
31
+ if (main.isMinimized()) main.restore();
32
+ main.show();
33
+ main.focus();
34
+ let released = false;
35
+ return () => {
36
+ if (released) return;
37
+ released = true;
38
+ if (--pinsHeld > 0) return;
39
+ if (platform === "darwin") {
40
+ const active = electron.BrowserWindow.getFocusedWindow() !== null;
41
+ if (active) {
42
+ for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(true, "floating");
43
+ }
44
+ } else {
45
+ for (const [w, owner] of heldChildren) {
46
+ if (!w.isDestroyed() && !owner.isDestroyed()) w.setParentWindow(owner);
47
+ }
48
+ heldChildren.clear();
49
+ }
50
+ };
51
+ }
52
+
12
53
  // src/updater.ts
13
54
  var UPDATER_CHANNELS = {
14
55
  /** main -> renderer: "are there unsaved edits?"; reply on `dirtyReply`. */
@@ -159,18 +200,37 @@ function themedPrompt(win, opts) {
159
200
  resolve(fallback);
160
201
  return;
161
202
  }
203
+ const release = frontWhileOpen(win);
204
+ const done = (idx) => {
205
+ release();
206
+ resolve(idx);
207
+ };
162
208
  const onReply = (_e, idx) => {
163
209
  clearTimeout(timeout);
164
- resolve(typeof idx === "number" ? idx : fallback);
210
+ done(typeof idx === "number" ? idx : fallback);
165
211
  };
166
212
  electron.ipcMain.once(UPDATER_CHANNELS.promptReply, onReply);
167
213
  const timeout = setTimeout(() => {
168
214
  electron.ipcMain.removeListener(UPDATER_CHANNELS.promptReply, onReply);
169
- resolve(fallback);
215
+ done(fallback);
170
216
  }, 3e5);
171
217
  win.webContents.send(UPDATER_CHANNELS.prompt, opts);
172
218
  });
173
219
  }
220
+ function frontWhileOpen(win) {
221
+ let release = null;
222
+ let finished = false;
223
+ const front = () => {
224
+ if (!finished && !win.isDestroyed()) release = frontForModal(win);
225
+ };
226
+ if (electron.BrowserWindow.getFocusedWindow()) front();
227
+ else electron.app.once("browser-window-focus", front);
228
+ return () => {
229
+ finished = true;
230
+ electron.app.removeListener("browser-window-focus", front);
231
+ release?.();
232
+ };
233
+ }
174
234
  async function quitAndInstallSafely() {
175
235
  const win = activeWindow();
176
236
  if (!await askRendererIsDirty(win)) {
@@ -210,7 +270,8 @@ autoUpdater.on("update-downloaded", (info) => {
210
270
  defaultId: 0,
211
271
  cancelId: 1,
212
272
  message: "Update ready to install",
213
- detail: `${appName} ${info.version} has been downloaded. Restart now to install it, or it installs the next time you quit.`
273
+ detail: `${appName} ${info.version} has been downloaded. Restart now to install it, or it installs the next time you quit.`,
274
+ unasked: true
214
275
  }).then((response) => {
215
276
  if (response === 0) void quitAndInstallSafely();
216
277
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/updater.ts"],"names":["BrowserWindow","electronUpdater","join","app","appendFileSync","ipcMain"],"mappings":"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","file":"updater.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// In-app auto-update over electron-updater: the family's, lifted from Patterpad\n// where it was hardened in the field.\n//\n// This is not a wrapper anybody should write twice. Every rule in it is a bug\n// that reached a user:\n//\n// - Downloads are driven HERE rather than by electron-updater's autoDownload,\n// because holding the CancellationToken is what lets a stall watchdog kill\n// and retry a download that hangs. A hung stream emits neither progress nor\n// `error`, so anything built on events alone sits \"downloading\" for ever\n// (Patterpad #33: a Windows user's download did exactly that, all day).\n// - Differential download is off. On Windows it stalled silently across an\n// Electron major bump, where almost no blocks match anyway.\n// - There is a PERSISTENT LOG in userData, because electron-updater is silent\n// by default and a stalled download otherwise leaves nothing to diagnose.\n// - Installing asks the renderer whether there is unsaved work, and offers\n// Save / Discard / Cancel. \"Restart Now\" must never silently drop an\n// author's afternoon.\n// - Every prompt is THEMED, in the renderer, never a stock OS dialog.\n//\n// Only runs in a packaged build; in development the manual check says so rather\n// than failing obscurely.\n//\n// THE SEAMS: the app supplies its NAME (it appears in every message), a way to\n// find the ACTIVE WINDOW, and a renderer that answers four channels - the\n// contract below. Everything else is the same in every app.\n// ---------------------------------------------------------------------------\n\nimport { app, BrowserWindow, ipcMain } from \"electron\";\nimport { appendFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { ProgressInfo, UpdateDownloadedEvent, UpdateInfo } from \"electron-updater\";\nimport electronUpdater from \"electron-updater\";\n\n/** Download progress, broadcast to every window so a live dialog can show it. */\nexport interface UpdaterDownloadProgress {\n percent: number;\n transferred: number;\n total: number;\n bytesPerSecond: number;\n /** Which version is coming, so a dialog can name it. */\n version: string;\n}\n\n/** A themed prompt, answered by the renderer with the chosen button index -\n * the same contract as `dialog.showMessageBox`'s `response`. */\nexport interface UpdaterPromptOptions {\n message: string;\n detail?: string;\n buttons: string[];\n defaultId?: number;\n cancelId?: number;\n /** Show live download progress in the dialog. */\n progress?: boolean;\n}\n\n/** The channels the app's renderer must answer. Named here so both sides agree. */\nexport const UPDATER_CHANNELS = {\n /** main -> renderer: \"are there unsaved edits?\"; reply on `dirtyReply`. */\n checkDirty: \"updater:check-dirty\",\n dirtyReply: \"updater:dirty-reply\",\n /** main -> renderer: \"save now\"; reply `{ ok }` on `saveDone`. */\n saveBeforeInstall: \"updater:save-before-install\",\n saveDone: \"updater:save-done\",\n /** main -> renderer: show this prompt; reply with the button index. */\n prompt: \"updater:prompt\",\n promptReply: \"updater:prompt-reply\",\n /** main -> renderer: live download progress. */\n progress: \"updater:download-progress\",\n} as const;\n\nexport interface UpdaterOptions {\n /** The product name, as it should read in every message (\"Patterpad\"). */\n appName: string;\n /** Which window the prompts belong to. */\n activeWindow: () => BrowserWindow | null;\n /** The log file inside userData. */\n logFile?: string;\n}\n\nlet appName = \"This app\";\nlet activeWindowFn: () => BrowserWindow | null = () =>\n BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0] ?? null;\nconst activeWindow = (): BrowserWindow | null => activeWindowFn();\n\nconst { autoUpdater, CancellationToken } = electronUpdater;\n\nlet updateDownloaded: UpdateDownloadedEvent | null = null;\n// Remember the last background error so the manual \"Check for Updates\" dialog can\n// surface it - otherwise a broken feed (network policy, expired cert, GitHub outage)\n// stays invisible and the user has no idea why updates never arrive.\nlet lastBackgroundError: string | null = null;\n\n// Persistent updater log (userData/updater.log). electron-updater is silent by default; without a log a\n// Windows download that stalls without emitting `error` (found in 0.1.5) leaves nothing to diagnose.\nlet logPath = join(app.getPath(\"userData\"), \"updater.log\");\nconst writeLog = (level: string, args: unknown[]): void => {\n try { appendFileSync(logPath, `${new Date().toISOString()} [${level}] ${args.map((a) => (a instanceof Error ? a.stack || a.message : String(a))).join(\" \")}\\n`); } catch { /* logging must never throw */ }\n};\nautoUpdater.logger = {\n info: (...a: unknown[]) => writeLog(\"info\", a),\n warn: (...a: unknown[]) => writeLog(\"warn\", a),\n error: (...a: unknown[]) => writeLog(\"error\", a),\n debug: (...a: unknown[]) => writeLog(\"debug\", a),\n};\n\n// Downloads are driven by the download manager below, NOT electron-updater's autoDownload: holding\n// the CancellationToken ourselves is what lets the stall watchdog kill and retry a download that\n// hangs without ever erroring (#33 - a Windows user's download sat \"in the background\" all day; a\n// hung stream emits neither progress nor `error`, so nothing built on events alone can recover it).\nautoUpdater.autoDownload = false;\nautoUpdater.autoInstallOnAppQuit = true;\n// Force a plain full download instead of the block-by-block differential. On Windows the differential\n// path stalled silently (0.1.5): across an Electron-major bump almost no blocks match, so it downloads\n// nearly everything anyway, and it's fragile for unsigned installers. Full download is one reliable\n// stream. No-op on macOS (Squirrel.Mac always fetches the whole zip).\nautoUpdater.disableDifferentialDownload = true;\n\nautoUpdater.on(\"error\", (err) => {\n console.error(\"AutoUpdater error:\", err?.message || err);\n lastBackgroundError = err?.message || String(err);\n});\n\n// ---------------------------------------------------------------------------\n// Download manager: start, watch, retry. Platform-agnostic - it rides\n// electron-updater's shared events, identical for NSIS (win), zip (mac), and\n// AppImage (linux) feeds.\n// ---------------------------------------------------------------------------\n\nconst STALL_MS = 3 * 60 * 1000; // no progress event for this long = the download is hung\nconst WATCH_EVERY_MS = 30 * 1000; // how often the watchdog looks at the clock\nconst RETRY_DELAY_MS = 15 * 1000; // pause before re-attempting a killed/failed download\nconst MAX_ATTEMPTS = 3; // per check cycle; the 6-hourly background check starts a fresh cycle\n\ntype DownloadState = {\n info: UpdateInfo;\n token: InstanceType<typeof CancellationToken>;\n attempts: number; // attempts consumed this cycle, including the running one\n lastProgressAt: number;\n cancelledByWatchdog: boolean;\n watchdog: NodeJS.Timeout;\n};\nlet download: DownloadState | null = null;\n\nfunction broadcastProgress(p: UpdaterDownloadProgress): void {\n for (const w of BrowserWindow.getAllWindows()) {\n if (!w.isDestroyed() && !w.webContents.isDestroyed()) w.webContents.send(UPDATER_CHANNELS.progress, p);\n }\n}\n\nfunction clearDownload(): void {\n if (download) clearInterval(download.watchdog);\n download = null;\n}\n\n/** Begin (or re-attempt) downloading `info`. One state machine for every platform. */\nfunction beginDownload(info: UpdateInfo, attempts: number): void {\n clearDownload();\n const token = new CancellationToken();\n const state: DownloadState = {\n info, token, attempts: attempts + 1, lastProgressAt: Date.now(), cancelledByWatchdog: false,\n watchdog: setInterval(() => {\n if (download !== state) return;\n const quiet = Date.now() - state.lastProgressAt;\n if (quiet < STALL_MS) return;\n // Hung: no progress and no error for STALL_MS. Kill it; the download promise's catch retries.\n writeLog(\"warn\", [`watchdog: no download progress for ${Math.round(quiet / 1000)}s - cancelling (attempt ${state.attempts}/${MAX_ATTEMPTS})`]);\n state.cancelledByWatchdog = true;\n state.token.cancel();\n }, WATCH_EVERY_MS),\n };\n download = state;\n writeLog(\"info\", [`download: starting ${info.version} (attempt ${state.attempts}/${MAX_ATTEMPTS})`]);\n\n autoUpdater.downloadUpdate(token).then(() => {\n // Success is reported via the update-downloaded event; it clears the state.\n }).catch((err: unknown) => {\n if (download !== state) return; // superseded by a newer attempt/cycle\n const why = state.cancelledByWatchdog ? \"stalled (killed by the watchdog)\"\n : (err instanceof Error ? err.message : String(err));\n clearDownload();\n if (state.attempts < MAX_ATTEMPTS) {\n writeLog(\"warn\", [`download: attempt ${state.attempts} failed - ${why}; retrying in ${RETRY_DELAY_MS / 1000}s`]);\n setTimeout(() => { if (!download && !updateDownloaded) beginDownload(info, state.attempts); }, RETRY_DELAY_MS);\n } else {\n // Out of attempts for this cycle. Record it where the manual check surfaces it; the next\n // background check (6-hourly, or Help > Check for Updates) starts a fresh cycle.\n lastBackgroundError = `Downloading ${info.version} failed ${MAX_ATTEMPTS} times, most recently with \"${why}\". It will be tried again on the next check.`;\n writeLog(\"error\", [`download: giving up on ${info.version} this cycle - ${why}`]);\n }\n });\n}\n\nautoUpdater.on(\"update-available\", (info: UpdateInfo) => {\n if (download || updateDownloaded) return; // already downloading it, or already have it\n beginDownload(info, 0);\n});\n\nautoUpdater.on(\"download-progress\", (p: ProgressInfo) => {\n if (download) download.lastProgressAt = Date.now();\n broadcastProgress({\n percent: p.percent, transferred: p.transferred, total: p.total,\n bytesPerSecond: p.bytesPerSecond, version: download?.info.version ?? \"\",\n });\n});\n\n/** Ask the renderer (one-shot, timeout-guarded) whether the open scene has unsaved edits.\n * Resolves false on any error/timeout - the renderer's dirty flag is the only truth here. */\nfunction askRendererIsDirty(win: BrowserWindow | null): Promise<boolean> {\n return new Promise((resolve) => {\n if (!win || win.isDestroyed() || win.webContents.isDestroyed()) { resolve(false); return; }\n const onReply = (_e: unknown, isDirty: boolean) => { clearTimeout(timeout); resolve(!!isDirty); };\n ipcMain.once(UPDATER_CHANNELS.dirtyReply, onReply);\n const timeout = setTimeout(() => {\n ipcMain.removeListener(UPDATER_CHANNELS.dirtyReply, onReply);\n console.warn(\"Updater dirty-check timed out; assuming clean.\");\n resolve(false);\n }, 2000);\n win.webContents.send(UPDATER_CHANNELS.checkDirty);\n });\n}\n\n/** Trigger a save in the renderer and wait for completion. Saves run the lock-aware VC write path,\n * so the timeout is generous; if it elapses we abort rather than install over a half-saved project. */\nfunction triggerRendererSave(win: BrowserWindow | null): Promise<{ ok: boolean }> {\n return new Promise((resolve) => {\n if (!win || win.isDestroyed() || win.webContents.isDestroyed()) { resolve({ ok: false }); return; }\n const onReply = (_e: unknown, result: { ok: boolean }) => { clearTimeout(timeout); resolve(result || { ok: false }); };\n ipcMain.once(UPDATER_CHANNELS.saveDone, onReply);\n const timeout = setTimeout(() => {\n ipcMain.removeListener(UPDATER_CHANNELS.saveDone, onReply);\n console.warn(\"Updater save timed out.\");\n resolve({ ok: false });\n }, 30000);\n win.webContents.send(UPDATER_CHANNELS.saveBeforeInstall);\n });\n}\n\n/** Show a THEMED prompt in the renderer (never a stock OS dialog; design-language \"coherent to the edges\")\n * and resolve the chosen button index - the same contract as dialog.showMessageBox's `response`. Falls\n * back to cancelId/defaultId if there's no live renderer to answer (so an install is never blocked on it). */\nfunction themedPrompt(win: BrowserWindow | null, opts: UpdaterPromptOptions): Promise<number> {\n return new Promise((resolve) => {\n const fallback = opts.cancelId ?? opts.defaultId ?? 0;\n if (!win || win.isDestroyed() || win.webContents.isDestroyed()) { resolve(fallback); return; }\n const onReply = (_e: unknown, idx: number) => { clearTimeout(timeout); resolve(typeof idx === \"number\" ? idx : fallback); };\n ipcMain.once(UPDATER_CHANNELS.promptReply, onReply);\n // Generous: these are user-facing prompts. If the renderer never answers (gone / hung), fall back safely.\n const timeout = setTimeout(() => { ipcMain.removeListener(UPDATER_CHANNELS.promptReply, onReply); resolve(fallback); }, 300000);\n win.webContents.send(UPDATER_CHANNELS.prompt, opts);\n });\n}\n\n/** quitAndInstall, guarded by the unsaved-edits check. autoInstallOnAppQuit routes through the normal\n * window-close save path already, so only this explicit \"Restart Now\" needs the guard. */\nasync function quitAndInstallSafely(): Promise<void> {\n const win = activeWindow();\n if (!(await askRendererIsDirty(win))) { autoUpdater.quitAndInstall(); return; }\n\n const response = await themedPrompt(win, {\n buttons: [\"Save and restart\", \"Discard and restart\", \"Cancel\"],\n defaultId: 0,\n cancelId: 2,\n message: \"You have unsaved changes.\",\n detail: `Save them before restarting to install ${appName} ${updateDownloaded?.version || \"\"}?`,\n });\n\n if (response === 2) return; // Cancel - abort the install\n if (response === 0) {\n const result = await triggerRendererSave(win);\n if (!result.ok) {\n await themedPrompt(win, {\n message: \"Save failed\",\n detail: \"Your changes could not be saved, so the update was not installed. Save manually, then try again.\",\n buttons: [\"OK\"],\n });\n return;\n }\n }\n // response === 1 (Discard) falls through.\n autoUpdater.quitAndInstall();\n}\n\nautoUpdater.on(\"update-downloaded\", (info: UpdateDownloadedEvent) => {\n clearDownload();\n updateDownloaded = info;\n lastBackgroundError = null; // it got here in the end; stale retry noise would only mislead\n writeLog(\"info\", [`download: ${info.version} downloaded and ready to install`]);\n const win = activeWindow();\n if (!win) return;\n void themedPrompt(win, {\n buttons: [\"Restart now\", \"Later\"],\n defaultId: 0,\n cancelId: 1,\n message: \"Update ready to install\",\n detail: `${appName} ${info.version} has been downloaded. Restart now to install it, or it installs the next time you quit.`,\n }).then((response) => { if (response === 0) void quitAndInstallSafely(); });\n});\n\n/** Name the app and say how to find its window. Call once, at startup. */\nexport function configureUpdater(opts: UpdaterOptions): void {\n appName = opts.appName;\n activeWindowFn = opts.activeWindow;\n if (opts.logFile !== undefined) logPath = join(app.getPath(\"userData\"), opts.logFile);\n}\n\n/** Fire-and-forget background check; periodic + on launch. No-op in dev. */\nexport function startBackgroundUpdateCheck(): void {\n if (!app.isPackaged) return;\n autoUpdater.checkForUpdates().catch((err) => {\n console.error(\"AutoUpdater background check failed:\", err?.message || err);\n });\n}\n\n/** The Help ▸ Check for Updates… handler: always gives the user feedback (downloading / up to date /\n * error), unlike the silent background check. */\nexport async function manualCheckForUpdates(win?: BrowserWindow | null): Promise<void> {\n const parent = win || activeWindow();\n\n if (!app.isPackaged) {\n await themedPrompt(parent, {\n message: \"Updates unavailable in a development build\",\n detail: `Auto-update only runs in a packaged build. This is version ${app.getVersion()}.`,\n buttons: [\"OK\"],\n });\n return;\n }\n\n if (updateDownloaded) {\n const response = await themedPrompt(parent, {\n buttons: [\"Restart now\", \"Later\"],\n defaultId: 0,\n cancelId: 1,\n message: \"Update ready to install\",\n detail: `${appName} ${updateDownloaded.version} has been downloaded. Restart now to install it.`,\n });\n if (response === 0) await quitAndInstallSafely();\n return;\n }\n\n if (download) {\n // A download is already in flight: show it, live (the dialog subscribes to progress events).\n await themedPrompt(parent, {\n message: \"Update available\",\n detail: `${appName} ${download.info.version} is downloading. You'll be prompted to restart when it's ready.`,\n buttons: [\"OK\"],\n progress: true,\n });\n return;\n }\n\n try {\n // A manual check starts a fresh retry cycle: if the last one gave up, this is the user asking again.\n await autoUpdater.checkForUpdates();\n // With autoDownload off, `update-available` (fired during the await) starts the managed download,\n // so by here `download` is set iff the feed had something newer. (Read through locals: TS narrows the\n // module-level `download` to null after the guard above and cannot see the event handler mutate it.)\n const started = download as DownloadState | null;\n const ready = updateDownloaded as UpdateDownloadedEvent | null;\n if (started) {\n await themedPrompt(parent, {\n message: \"Update available\",\n detail: `${appName} ${started.info.version} is downloading. You'll be prompted to restart when it's ready.`,\n buttons: [\"OK\"],\n progress: true,\n });\n lastBackgroundError = null;\n } else if (ready) {\n // The check completed a download between our earlier guard and now (tiny window, but free to handle).\n const response = await themedPrompt(parent, {\n buttons: [\"Restart now\", \"Later\"], defaultId: 0, cancelId: 1,\n message: \"Update ready to install\",\n detail: `${appName} ${ready.version} has been downloaded. Restart now to install it.`,\n });\n if (response === 0) await quitAndInstallSafely();\n } else if (lastBackgroundError) {\n await themedPrompt(parent, {\n message: \"You're on the latest version.\",\n detail: `${appName} ${app.getVersion()} is up to date. The last update check reported an error:\\n${lastBackgroundError}`,\n buttons: [\"OK\"],\n });\n lastBackgroundError = null;\n } else {\n await themedPrompt(parent, {\n message: \"You're on the latest version.\",\n detail: `${appName} ${app.getVersion()} is up to date.`,\n buttons: [\"OK\"],\n });\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const detail = lastBackgroundError && lastBackgroundError !== message\n ? `${message}\\n\\nA previous background check also failed with:\\n${lastBackgroundError}`\n : message;\n await themedPrompt(parent, { message: \"Update check failed\", detail, buttons: [\"OK\"] });\n lastBackgroundError = null;\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/tool-window.ts","../src/updater.ts"],"names":["BrowserWindow","electronUpdater","join","app","appendFileSync","ipcMain"],"mappings":"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","file":"updater.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// The tool-window kit, MAIN-PROCESS half: the detached helper-window\n// machinery both apps had grown four copies of (Patterpad's Play + Search +\n// Coverage, Storyletter's Board + Find). Remembered bounds with a\n// disconnected-monitor guard, debounced persistence, the Reset View rescue,\n// and the pinnable window factory they compose into.\n//\n// This module imports electron, so it ships as its own subpath\n// (@wildwinter/app-shell/tool-window) - never import it from renderer code.\n// The renderer half (pin button, .swin drag-bar grammar) lives in the root\n// export + tool-window.css.\n//\n// Two deliberate corrections over the Patterpad originals, for when it\n// migrates here: rescueToolWindow re-pins EVERY window it rescues (Patterpad\n// re-pinned only Play, so its store lied about Search + Coverage), and the\n// factory always wires closed-cleanup through an identity guard.\n// ---------------------------------------------------------------------------\n\nimport { BrowserWindow, app, screen } from \"electron\";\nimport { join } from \"node:path\";\nimport type { Satellite } from \"./session.js\";\n\nexport interface ToolWindowSize {\n width: number;\n height: number;\n}\n\nexport interface ToolWindowBounds {\n x?: number;\n y?: number;\n width: number;\n height: number;\n}\n\n/** The per-window store slice every app remembers: where it was, whether it\n * floats. (Patterpad's PlayWindowState, generalised.) */\nexport interface ToolWindowState {\n bounds?: ToolWindowBounds;\n pinned: boolean;\n}\n\n/** Use a remembered rect only if it still intersects a live display (a window\n * saved on a now-disconnected monitor must not open offscreen). Falls back\n * to the default size; a remembered size is kept even when the position is\n * dropped (Electron then centres it). */\nexport function savedWindowRect(\n saved: ToolWindowBounds | undefined,\n def: ToolWindowSize,\n min: ToolWindowSize,\n): ToolWindowBounds {\n if (!saved) return { ...def };\n const w = Math.max(min.width, saved.width), h = Math.max(min.height, saved.height);\n if (saved.x != null && saved.y != null) {\n const onScreen = screen.getAllDisplays().some((d) => {\n const a = d.workArea;\n return saved.x! + w > a.x + 40 && saved.x! < a.x + a.width - 40 && saved.y! + h > a.y + 20 && saved.y! < a.y + a.height - 20;\n });\n if (onScreen) return { x: saved.x, y: saved.y, width: w, height: h };\n }\n return { width: w, height: h };\n}\n\n/** Persist a helper window's bounds as the user moves / resizes / closes it\n * (debounced 400ms; close flushes synchronously). */\nexport function rememberBounds(w: BrowserWindow, write: (bounds: ToolWindowBounds) => void): void {\n const saveBounds = (): void => { if (!w.isDestroyed()) write(w.getBounds()); };\n let boundsTimer: ReturnType<typeof setTimeout> | undefined;\n const queueSave = (): void => { clearTimeout(boundsTimer); boundsTimer = setTimeout(saveBounds, 400); };\n w.on(\"resize\", queueSave);\n w.on(\"move\", queueSave);\n w.on(\"close\", () => { clearTimeout(boundsTimer); saveBounds(); });\n}\n\n/** Centre a rect on the primary display's work area. */\nexport function centeredOnPrimary(size: ToolWindowSize): { x: number; y: number } {\n const a = screen.getPrimaryDisplay().workArea;\n return { x: Math.round(a.x + (a.width - size.width) / 2), y: Math.round(a.y + (a.height - size.height) / 2) };\n}\n\n/** Reset View's per-window half: un-minimise, re-pin (always - the store\n * says pinned:true after a rescue, so the window must agree), default size,\n * centred on the primary display, shown. The host clears its remembered\n * bounds itself (that's a store concern). No focus steal: helpers show\n * behind the editor. */\nexport function rescueToolWindow(w: BrowserWindow | undefined | null, def: ToolWindowSize): void {\n if (!w || w.isDestroyed()) return;\n if (w.isMinimized()) w.restore();\n w.setBounds({ ...def, ...centeredOnPrimary(def) });\n w.show();\n // Raise within OUR stack. This used to setAlwaysOnTop(true), which on macOS\n // and Windows floats the window above every other APPLICATION - and stuck\n // that way, since nothing ever unset it.\n w.moveTop();\n}\n\n/** macOS pinned windows, for the activation toggle below. */\nconst macPinned = new Set<BrowserWindow>();\nlet macActivationWired = false;\nfunction wireMacActivation(): void {\n if (macActivationWired) return;\n macActivationWired = true;\n // Floating is a GLOBAL window level: left on while another app is front,\n // the pinned window would sit over that app too - the exact 2026-08-25\n // complaint. Scoping it to our app's active state keeps the pin meaning\n // \"above my editor\" and nothing more.\n // Not while a modal has the pins held (frontForModal): the editor is in\n // front for it, and coming back to the app must not cover it again.\n app.on(\"did-become-active\", () => { if (pinsHeld > 0) return; for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(true, \"floating\"); });\n app.on(\"did-resign-active\", () => { for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(false); });\n}\n\n/**\n * The pin: keep a tool window above THIS APP'S MAIN WINDOW, not above the\n * world. Two findings shaped the mechanism, one per platform family:\n *\n * - Bare `alwaysOnTop` (the first implementation) floats the window over\n * every other application on macOS and Windows, so a pinned Board meant\n * nothing else on the machine could come to the front (a Storyletter user\n * report, 2026-08-25).\n * - A CHILD window (the second implementation) is position-coupled to its\n * parent on macOS: dragging the editor dragged the Board with it (a\n * Storyletter user report, 2026-08-28). Windows and Linux owned windows do\n * not move with their owner, so the child mechanism stays right for them.\n *\n * So: on macOS the pin is ACTIVATION-SCOPED alwaysOnTop - floating while\n * this app is active, dropped the moment it resigns - which is above the\n * editor without following its drags and without covering anyone else. On\n * Windows and Linux the pin is a child window, which stays over its parent\n * and stacks normally against everything else. Each branch clears the other\n * mechanism first, which also heals a window an older build pinned the old\n * way. `platform` is a seam for the tests; leave it defaulted.\n */\nexport function pinToolWindow(\n w: BrowserWindow | undefined | null, parent: BrowserWindow | undefined | null, on: boolean,\n platform: NodeJS.Platform = process.platform,\n): void {\n if (!w || w.isDestroyed()) return;\n if (platform === \"darwin\") {\n w.setParentWindow(null);\n if (on) {\n if (!macPinned.has(w)) {\n macPinned.add(w);\n w.once(\"closed\", () => macPinned.delete(w));\n }\n wireMacActivation();\n // Raise only when the app is active now and no modal has the pins\n // held; otherwise the next did-become-active, or the release, raises it.\n w.setAlwaysOnTop(pinsHeld === 0 && BrowserWindow.getFocusedWindow() !== null, \"floating\");\n } else {\n macPinned.delete(w);\n w.setAlwaysOnTop(false);\n }\n return;\n }\n w.setAlwaysOnTop(false);\n const owner = on && parent && !parent.isDestroyed() ? parent : null;\n // While a modal has the pins held, remember the choice and apply it at the\n // release, so the editor stays in front until the modal closes.\n if (pinsHeld > 0) {\n if (owner) heldChildren.set(w, owner); else heldChildren.delete(w);\n w.setParentWindow(null);\n return;\n }\n w.setParentWindow(owner);\n}\n\n// ---------------------------------------------------------------------------\n// Fronting the main window for a modal. A pinned tool window rides above the\n// editor by design, so a modal the EDITOR shows (the updater's prompts) opened\n// underneath one: the editor blurred behind its scrim and the prompt was\n// hidden, with nothing on screen to say why (both apps, 2026-10-02, the Play\n// window over the editor). While such a modal is up the pins step aside and\n// the editor comes forward; when it closes, every pin goes back as it was.\n// ---------------------------------------------------------------------------\n\n/** How many modals are holding the pins down. The pins return at zero. */\nlet pinsHeld = 0;\n/** Windows and Linux: the tool windows unparented by the hold, and the window\n * each goes back to. */\nconst heldChildren = new Map<BrowserWindow, BrowserWindow>();\n\n/**\n * Bring `main` in front of every tool window, pinned ones included, for as\n * long as a modal it shows is open: un-minimise, show, focus, with the pins\n * set aside. Returns the release, which puts the pins back (idempotent; holds\n * nest, and the pins return when the last is released). Call it only while\n * the app is active: focusing a window of an inactive app is the focus steal\n * the platforms forbid. `platform` is a seam for the tests.\n */\nexport function frontForModal(main: BrowserWindow, platform: NodeJS.Platform = process.platform): () => void {\n if (main.isDestroyed()) return () => {};\n pinsHeld++;\n if (platform === \"darwin\") {\n for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(false);\n } else {\n for (const w of BrowserWindow.getAllWindows()) {\n if (w === main || w.isDestroyed()) continue;\n const owner = w.getParentWindow();\n if (owner !== main) continue;\n heldChildren.set(w, main);\n w.setParentWindow(null);\n }\n }\n if (main.isMinimized()) main.restore();\n main.show();\n main.focus();\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n if (--pinsHeld > 0) return;\n if (platform === \"darwin\") {\n // Only float again if the app is active; otherwise did-become-active does.\n const active = BrowserWindow.getFocusedWindow() !== null;\n if (active) for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(true, \"floating\");\n } else {\n for (const [w, owner] of heldChildren) {\n if (!w.isDestroyed() && !owner.isDestroyed()) w.setParentWindow(owner);\n }\n heldChildren.clear();\n }\n };\n}\n\nexport interface ToolWindowOptions {\n title: string;\n /** Renderer entry, relative to rendererDir (e.g. \"table.html\", \"search/index.html\"). */\n page: string;\n /** Absolute path of the built renderer dir (join(import.meta.dirname, \"../renderer\")). */\n rendererDir: string;\n /** Absolute path of the preload bridge. */\n preload: string;\n rect: ToolWindowBounds;\n min?: ToolWindowSize;\n /** false = frameless: the renderer draws its own slim drag bar (.swin-head). */\n frame?: boolean;\n /** The window to PIN ABOVE: the app's main window, resolved at open time (a\n * getter, because the main window can be recreated). With `pinned` true\n * the new tool window rides above it - on macOS via activation-scoped\n * alwaysOnTop (a child window there follows the editor's drags), on\n * Windows and Linux as a child window (bare alwaysOnTop there floats over\n * the whole machine). See pinToolWindow for both findings. */\n pinTo?: () => BrowserWindow | undefined;\n /** Start pinned (the host remembers); flip later with `pinToolWindow`. */\n pinned?: boolean;\n /** Wire debounced bounds persistence into the host's store slice. */\n remember?: (bounds: ToolWindowBounds) => void;\n /** Fired when THIS window closes (identity-guarded; a stale close never\n * clears a newer window). Null the host's handle here. */\n onClosed?: () => void;\n}\n\n/** The pinnable detached-window factory (hardened webPreferences, one\n * preload, electron-vite's dev-URL/file split). Focuses and returns the\n * existing window when there is one; otherwise creates, wires, and loads. */\nexport function openToolWindow(existing: BrowserWindow | undefined | null, opts: ToolWindowOptions): BrowserWindow {\n if (existing && !existing.isDestroyed()) { existing.focus(); return existing; }\n const w = new BrowserWindow({\n ...opts.rect,\n ...(opts.min !== undefined ? { minWidth: opts.min.width, minHeight: opts.min.height } : {}),\n show: false,\n title: opts.title,\n ...(opts.frame !== undefined ? { frame: opts.frame } : {}),\n webPreferences: {\n contextIsolation: true, nodeIntegration: false, sandbox: true,\n preload: opts.preload,\n },\n });\n if (opts.pinTo !== undefined) pinToolWindow(w, opts.pinTo(), opts.pinned ?? false);\n if (opts.remember) rememberBounds(w, opts.remember);\n w.once(\"ready-to-show\", () => w.show());\n if (opts.onClosed) w.on(\"closed\", opts.onClosed);\n if (process.env[\"ELECTRON_RENDERER_URL\"]) void w.loadURL(`${process.env[\"ELECTRON_RENDERER_URL\"]}/${opts.page}`);\n else void w.loadFile(join(opts.rendererDir, opts.page));\n return w;\n}\n\n// ---------------------------------------------------------------------------\n// The tool windows AS DATA: one row per window, and everything that walks the\n// windows walks the rows. Lifted from Storyletter's TOOL_WINDOWS table +\n// openToolWindowFor + rescueWindows (studio/src/main/index.ts), where it had\n// replaced four near-identical openX() functions whose differences were all\n// mistakes (only one forwarded its console; Reset View rescued two of four).\n// Patterpad's three hand copies (createPlayWindow / createSearchWindow /\n// createCoverageWindow and a four-branch rescueWindows) become three rows.\n//\n// Two things the table makes structurally impossible: forgetting to register\n// a window as a satellite for the project-changed nudge (every row is\n// registered when a session is given), and Reset View rescuing a subset (it\n// walks the rows).\n// ---------------------------------------------------------------------------\n\n\nexport interface ToolWindowSpec<N extends string = string> {\n /** The window's key: the store slice it remembers under, the dev-console\n * prefix, and what `open(name)` takes. */\n name: N;\n title: string;\n /** Renderer entry, relative to the env's rendererDir. */\n page: string;\n def: ToolWindowSize;\n min: ToolWindowSize;\n /** The remembered rect, read at open time (a store read-through). */\n bounds: () => ToolWindowBounds | undefined;\n /** Persist a moved / resized rect (debounced by the factory). */\n remember: (bounds: ToolWindowBounds) => void;\n /** Whether it starts pinned above the editor (both apps default true). */\n pinned: () => boolean;\n /** Frameless (the family's shape: the renderer draws `toolWindowHead`).\n * Default false = frameless; pass `frame: true` for an OS title bar. */\n frame?: boolean;\n /** The project-changed channel for THIS window when it differs from the\n * env's (Storyletter's Links window listens on \"links:focus\"). */\n channel?: string;\n /** Drop what main caches for the outgoing project on this window's behalf\n * (a coverage report, a search anchor). Runs whether the window is open\n * or not: see session.ts. */\n clear?: () => void;\n}\n\nexport interface ToolWindowEnv {\n /** Absolute path of the built renderer dir. */\n rendererDir: string;\n /** Absolute path of the preload bridge. */\n preload: string;\n /** The window to pin above: the app's main window, resolved at open time. */\n pinTo: () => BrowserWindow | undefined;\n /** Register every row as a satellite of the project session, so a new\n * project cannot open underneath a tool window that still shows the old\n * one. `channel` is the nudge each window listens on (a row may override). */\n session?: { addSatellite: (satellite: Satellite) => () => void; channel: string };\n /** Reset View's store half, run first: forget every remembered rect and\n * set pinned true (`resetWindows(store)` from app-store, or the app's own). */\n resetStore?: () => void;\n /** The channel a rescued window's renderer hears its re-pin on, so its pin\n * button agrees with the store (default \"state:pinned\"). */\n pinChannel?: string;\n /** Runs once per window created: forward its console in dev, say. */\n onOpened?: (w: BrowserWindow, name: string) => void;\n}\n\nexport interface ToolWindows<N extends string> {\n /** Open (or focus) one window by name. */\n open(name: N): BrowserWindow;\n /** The live window, if open. */\n get(name: N): BrowserWindow | undefined;\n /** Reset View's window half, over the table: store reset, then every open\n * window restored, default size, centred, re-pinned, and told so. */\n rescue(): void;\n /** Every window that is open right now. */\n all(): BrowserWindow[];\n readonly names: readonly N[];\n}\n\n/** Build the table. The windows are owned here: a host reads them through\n * `get(name)` rather than holding module variables of its own. */\nexport function defineToolWindows<N extends string>(specs: ToolWindowSpec<N>[], env: ToolWindowEnv): ToolWindows<N> {\n const live = new Map<N, BrowserWindow>();\n const byName = (name: N): ToolWindowSpec<N> => {\n const found = specs.find((s) => s.name === name);\n if (!found) throw new Error(`no tool window \"${name}\"`);\n return found;\n };\n const get = (name: N): BrowserWindow | undefined => {\n const w = live.get(name);\n return w && !w.isDestroyed() ? w : undefined;\n };\n if (env.session) {\n for (const spec of specs) {\n env.session.addSatellite({\n window: () => get(spec.name),\n channel: spec.channel ?? env.session.channel,\n ...(spec.clear ? { clear: spec.clear } : {}),\n });\n }\n }\n const open = (name: N): BrowserWindow => {\n const spec = byName(name);\n const existing = get(name);\n const opened = openToolWindow(existing, {\n title: spec.title, page: spec.page, frame: spec.frame ?? false,\n rendererDir: env.rendererDir, preload: env.preload,\n rect: savedWindowRect(spec.bounds(), spec.def, spec.min),\n min: spec.min,\n pinTo: env.pinTo,\n pinned: spec.pinned(),\n remember: spec.remember,\n });\n if (opened !== existing) {\n live.set(name, opened);\n // Identity-guarded: a stale close never clears a newer window.\n opened.on(\"closed\", () => { if (live.get(name) === opened) live.delete(name); });\n env.onOpened?.(opened, name);\n }\n return opened;\n };\n const rescue = (): void => {\n env.resetStore?.();\n for (const spec of specs) rescueToolWindow(get(spec.name), spec.def);\n // ACTUALLY pin them, then tell them. `rescueToolWindow` restores, resizes,\n // centres and raises but never pins; the store now says pinned, so the\n // window and its button have to agree before the claim is made.\n for (const spec of specs) {\n const w = get(spec.name);\n if (!w) continue;\n pinToolWindow(w, env.pinTo(), true);\n w.webContents.send(env.pinChannel ?? \"state:pinned\", true);\n }\n };\n return {\n open, get, rescue,\n all: () => specs.map((s) => get(s.name)).filter((w): w is BrowserWindow => w !== undefined),\n names: specs.map((s) => s.name),\n };\n}\n","// ---------------------------------------------------------------------------\n// In-app auto-update over electron-updater: the family's, lifted from Patterpad\n// where it was hardened in the field.\n//\n// This is not a wrapper anybody should write twice. Every rule in it is a bug\n// that reached a user:\n//\n// - Downloads are driven HERE rather than by electron-updater's autoDownload,\n// because holding the CancellationToken is what lets a stall watchdog kill\n// and retry a download that hangs. A hung stream emits neither progress nor\n// `error`, so anything built on events alone sits \"downloading\" for ever\n// (Patterpad #33: a Windows user's download did exactly that, all day).\n// - Differential download is off. On Windows it stalled silently across an\n// Electron major bump, where almost no blocks match anyway.\n// - There is a PERSISTENT LOG in userData, because electron-updater is silent\n// by default and a stalled download otherwise leaves nothing to diagnose.\n// - Installing asks the renderer whether there is unsaved work, and offers\n// Save / Discard / Cancel. \"Restart Now\" must never silently drop an\n// author's afternoon.\n// - Every prompt is THEMED, in the renderer, never a stock OS dialog.\n// - Every prompt brings its window to the FRONT, past pinned tool windows,\n// for as long as it is open. It used to open under the Play window, so\n// the editor blurred behind a prompt nobody could see. A prompt that\n// arrives while the app is in the background waits for the app to come\n// back before it takes the front, and one nobody asked for puts focus on\n// its safe button, so a keystroke meant for the window it covered cannot\n// answer it.\n//\n// Only runs in a packaged build; in development the manual check says so rather\n// than failing obscurely.\n//\n// THE SEAMS: the app supplies its NAME (it appears in every message), a way to\n// find the ACTIVE WINDOW, and a renderer that answers four channels - the\n// contract below. Everything else is the same in every app.\n// ---------------------------------------------------------------------------\n\nimport { app, BrowserWindow, ipcMain } from \"electron\";\nimport { appendFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { ProgressInfo, UpdateDownloadedEvent, UpdateInfo } from \"electron-updater\";\nimport electronUpdater from \"electron-updater\";\nimport { frontForModal } from \"./tool-window.js\";\n\n/** Download progress, broadcast to every window so a live dialog can show it. */\nexport interface UpdaterDownloadProgress {\n percent: number;\n transferred: number;\n total: number;\n bytesPerSecond: number;\n /** Which version is coming, so a dialog can name it. */\n version: string;\n}\n\n/** A themed prompt, answered by the renderer with the chosen button index -\n * the same contract as `dialog.showMessageBox`'s `response`. */\nexport interface UpdaterPromptOptions {\n message: string;\n detail?: string;\n buttons: string[];\n defaultId?: number;\n cancelId?: number;\n /** Show live download progress in the dialog. */\n progress?: boolean;\n /** Nobody asked for this prompt: it arrived on its own (an update finished\n * downloading). Focus starts on the cancel button, so a keystroke meant for\n * whatever the prompt covered dismisses it rather than answering yes. */\n unasked?: boolean;\n}\n\n/** The channels the app's renderer must answer. Named here so both sides agree. */\nexport const UPDATER_CHANNELS = {\n /** main -> renderer: \"are there unsaved edits?\"; reply on `dirtyReply`. */\n checkDirty: \"updater:check-dirty\",\n dirtyReply: \"updater:dirty-reply\",\n /** main -> renderer: \"save now\"; reply `{ ok }` on `saveDone`. */\n saveBeforeInstall: \"updater:save-before-install\",\n saveDone: \"updater:save-done\",\n /** main -> renderer: show this prompt; reply with the button index. */\n prompt: \"updater:prompt\",\n promptReply: \"updater:prompt-reply\",\n /** main -> renderer: live download progress. */\n progress: \"updater:download-progress\",\n} as const;\n\nexport interface UpdaterOptions {\n /** The product name, as it should read in every message (\"Patterpad\"). */\n appName: string;\n /** Which window the prompts belong to. */\n activeWindow: () => BrowserWindow | null;\n /** The log file inside userData. */\n logFile?: string;\n}\n\nlet appName = \"This app\";\nlet activeWindowFn: () => BrowserWindow | null = () =>\n BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0] ?? null;\nconst activeWindow = (): BrowserWindow | null => activeWindowFn();\n\nconst { autoUpdater, CancellationToken } = electronUpdater;\n\nlet updateDownloaded: UpdateDownloadedEvent | null = null;\n// Remember the last background error so the manual \"Check for Updates\" dialog can\n// surface it - otherwise a broken feed (network policy, expired cert, GitHub outage)\n// stays invisible and the user has no idea why updates never arrive.\nlet lastBackgroundError: string | null = null;\n\n// Persistent updater log (userData/updater.log). electron-updater is silent by default; without a log a\n// Windows download that stalls without emitting `error` (found in 0.1.5) leaves nothing to diagnose.\nlet logPath = join(app.getPath(\"userData\"), \"updater.log\");\nconst writeLog = (level: string, args: unknown[]): void => {\n try { appendFileSync(logPath, `${new Date().toISOString()} [${level}] ${args.map((a) => (a instanceof Error ? a.stack || a.message : String(a))).join(\" \")}\\n`); } catch { /* logging must never throw */ }\n};\nautoUpdater.logger = {\n info: (...a: unknown[]) => writeLog(\"info\", a),\n warn: (...a: unknown[]) => writeLog(\"warn\", a),\n error: (...a: unknown[]) => writeLog(\"error\", a),\n debug: (...a: unknown[]) => writeLog(\"debug\", a),\n};\n\n// Downloads are driven by the download manager below, NOT electron-updater's autoDownload: holding\n// the CancellationToken ourselves is what lets the stall watchdog kill and retry a download that\n// hangs without ever erroring (#33 - a Windows user's download sat \"in the background\" all day; a\n// hung stream emits neither progress nor `error`, so nothing built on events alone can recover it).\nautoUpdater.autoDownload = false;\nautoUpdater.autoInstallOnAppQuit = true;\n// Force a plain full download instead of the block-by-block differential. On Windows the differential\n// path stalled silently (0.1.5): across an Electron-major bump almost no blocks match, so it downloads\n// nearly everything anyway, and it's fragile for unsigned installers. Full download is one reliable\n// stream. No-op on macOS (Squirrel.Mac always fetches the whole zip).\nautoUpdater.disableDifferentialDownload = true;\n\nautoUpdater.on(\"error\", (err) => {\n console.error(\"AutoUpdater error:\", err?.message || err);\n lastBackgroundError = err?.message || String(err);\n});\n\n// ---------------------------------------------------------------------------\n// Download manager: start, watch, retry. Platform-agnostic - it rides\n// electron-updater's shared events, identical for NSIS (win), zip (mac), and\n// AppImage (linux) feeds.\n// ---------------------------------------------------------------------------\n\nconst STALL_MS = 3 * 60 * 1000; // no progress event for this long = the download is hung\nconst WATCH_EVERY_MS = 30 * 1000; // how often the watchdog looks at the clock\nconst RETRY_DELAY_MS = 15 * 1000; // pause before re-attempting a killed/failed download\nconst MAX_ATTEMPTS = 3; // per check cycle; the 6-hourly background check starts a fresh cycle\n\ntype DownloadState = {\n info: UpdateInfo;\n token: InstanceType<typeof CancellationToken>;\n attempts: number; // attempts consumed this cycle, including the running one\n lastProgressAt: number;\n cancelledByWatchdog: boolean;\n watchdog: NodeJS.Timeout;\n};\nlet download: DownloadState | null = null;\n\nfunction broadcastProgress(p: UpdaterDownloadProgress): void {\n for (const w of BrowserWindow.getAllWindows()) {\n if (!w.isDestroyed() && !w.webContents.isDestroyed()) w.webContents.send(UPDATER_CHANNELS.progress, p);\n }\n}\n\nfunction clearDownload(): void {\n if (download) clearInterval(download.watchdog);\n download = null;\n}\n\n/** Begin (or re-attempt) downloading `info`. One state machine for every platform. */\nfunction beginDownload(info: UpdateInfo, attempts: number): void {\n clearDownload();\n const token = new CancellationToken();\n const state: DownloadState = {\n info, token, attempts: attempts + 1, lastProgressAt: Date.now(), cancelledByWatchdog: false,\n watchdog: setInterval(() => {\n if (download !== state) return;\n const quiet = Date.now() - state.lastProgressAt;\n if (quiet < STALL_MS) return;\n // Hung: no progress and no error for STALL_MS. Kill it; the download promise's catch retries.\n writeLog(\"warn\", [`watchdog: no download progress for ${Math.round(quiet / 1000)}s - cancelling (attempt ${state.attempts}/${MAX_ATTEMPTS})`]);\n state.cancelledByWatchdog = true;\n state.token.cancel();\n }, WATCH_EVERY_MS),\n };\n download = state;\n writeLog(\"info\", [`download: starting ${info.version} (attempt ${state.attempts}/${MAX_ATTEMPTS})`]);\n\n autoUpdater.downloadUpdate(token).then(() => {\n // Success is reported via the update-downloaded event; it clears the state.\n }).catch((err: unknown) => {\n if (download !== state) return; // superseded by a newer attempt/cycle\n const why = state.cancelledByWatchdog ? \"stalled (killed by the watchdog)\"\n : (err instanceof Error ? err.message : String(err));\n clearDownload();\n if (state.attempts < MAX_ATTEMPTS) {\n writeLog(\"warn\", [`download: attempt ${state.attempts} failed - ${why}; retrying in ${RETRY_DELAY_MS / 1000}s`]);\n setTimeout(() => { if (!download && !updateDownloaded) beginDownload(info, state.attempts); }, RETRY_DELAY_MS);\n } else {\n // Out of attempts for this cycle. Record it where the manual check surfaces it; the next\n // background check (6-hourly, or Help > Check for Updates) starts a fresh cycle.\n lastBackgroundError = `Downloading ${info.version} failed ${MAX_ATTEMPTS} times, most recently with \"${why}\". It will be tried again on the next check.`;\n writeLog(\"error\", [`download: giving up on ${info.version} this cycle - ${why}`]);\n }\n });\n}\n\nautoUpdater.on(\"update-available\", (info: UpdateInfo) => {\n if (download || updateDownloaded) return; // already downloading it, or already have it\n beginDownload(info, 0);\n});\n\nautoUpdater.on(\"download-progress\", (p: ProgressInfo) => {\n if (download) download.lastProgressAt = Date.now();\n broadcastProgress({\n percent: p.percent, transferred: p.transferred, total: p.total,\n bytesPerSecond: p.bytesPerSecond, version: download?.info.version ?? \"\",\n });\n});\n\n/** Ask the renderer (one-shot, timeout-guarded) whether the open scene has unsaved edits.\n * Resolves false on any error/timeout - the renderer's dirty flag is the only truth here. */\nfunction askRendererIsDirty(win: BrowserWindow | null): Promise<boolean> {\n return new Promise((resolve) => {\n if (!win || win.isDestroyed() || win.webContents.isDestroyed()) { resolve(false); return; }\n const onReply = (_e: unknown, isDirty: boolean) => { clearTimeout(timeout); resolve(!!isDirty); };\n ipcMain.once(UPDATER_CHANNELS.dirtyReply, onReply);\n const timeout = setTimeout(() => {\n ipcMain.removeListener(UPDATER_CHANNELS.dirtyReply, onReply);\n console.warn(\"Updater dirty-check timed out; assuming clean.\");\n resolve(false);\n }, 2000);\n win.webContents.send(UPDATER_CHANNELS.checkDirty);\n });\n}\n\n/** Trigger a save in the renderer and wait for completion. Saves run the lock-aware VC write path,\n * so the timeout is generous; if it elapses we abort rather than install over a half-saved project. */\nfunction triggerRendererSave(win: BrowserWindow | null): Promise<{ ok: boolean }> {\n return new Promise((resolve) => {\n if (!win || win.isDestroyed() || win.webContents.isDestroyed()) { resolve({ ok: false }); return; }\n const onReply = (_e: unknown, result: { ok: boolean }) => { clearTimeout(timeout); resolve(result || { ok: false }); };\n ipcMain.once(UPDATER_CHANNELS.saveDone, onReply);\n const timeout = setTimeout(() => {\n ipcMain.removeListener(UPDATER_CHANNELS.saveDone, onReply);\n console.warn(\"Updater save timed out.\");\n resolve({ ok: false });\n }, 30000);\n win.webContents.send(UPDATER_CHANNELS.saveBeforeInstall);\n });\n}\n\n/** Show a THEMED prompt in the renderer (never a stock OS dialog; design-language \"coherent to the edges\")\n * and resolve the chosen button index - the same contract as dialog.showMessageBox's `response`. Falls\n * back to cancelId/defaultId if there's no live renderer to answer (so an install is never blocked on it). */\nfunction themedPrompt(win: BrowserWindow | null, opts: UpdaterPromptOptions): Promise<number> {\n return new Promise((resolve) => {\n const fallback = opts.cancelId ?? opts.defaultId ?? 0;\n if (!win || win.isDestroyed() || win.webContents.isDestroyed()) { resolve(fallback); return; }\n const release = frontWhileOpen(win);\n const done = (idx: number): void => { release(); resolve(idx); };\n const onReply = (_e: unknown, idx: number) => { clearTimeout(timeout); done(typeof idx === \"number\" ? idx : fallback); };\n ipcMain.once(UPDATER_CHANNELS.promptReply, onReply);\n // Generous: these are user-facing prompts. If the renderer never answers (gone / hung), fall back safely.\n const timeout = setTimeout(() => { ipcMain.removeListener(UPDATER_CHANNELS.promptReply, onReply); done(fallback); }, 300000);\n win.webContents.send(UPDATER_CHANNELS.prompt, opts);\n });\n}\n\n/** Keep `win` in front of the app's other windows, pinned tool windows included, while a prompt is open\n * on it, and return the release. If the app is in the background the fronting waits until one of its\n * windows takes focus again: a prompt is no reason to pull an app over whatever the user went to. */\nfunction frontWhileOpen(win: BrowserWindow): () => void {\n let release: (() => void) | null = null;\n let finished = false;\n const front = (): void => { if (!finished && !win.isDestroyed()) release = frontForModal(win); };\n if (BrowserWindow.getFocusedWindow()) front();\n else app.once(\"browser-window-focus\", front);\n return () => {\n finished = true;\n app.removeListener(\"browser-window-focus\", front);\n release?.();\n };\n}\n\n/** quitAndInstall, guarded by the unsaved-edits check. autoInstallOnAppQuit routes through the normal\n * window-close save path already, so only this explicit \"Restart Now\" needs the guard. */\nasync function quitAndInstallSafely(): Promise<void> {\n const win = activeWindow();\n if (!(await askRendererIsDirty(win))) { autoUpdater.quitAndInstall(); return; }\n\n const response = await themedPrompt(win, {\n buttons: [\"Save and restart\", \"Discard and restart\", \"Cancel\"],\n defaultId: 0,\n cancelId: 2,\n message: \"You have unsaved changes.\",\n detail: `Save them before restarting to install ${appName} ${updateDownloaded?.version || \"\"}?`,\n });\n\n if (response === 2) return; // Cancel - abort the install\n if (response === 0) {\n const result = await triggerRendererSave(win);\n if (!result.ok) {\n await themedPrompt(win, {\n message: \"Save failed\",\n detail: \"Your changes could not be saved, so the update was not installed. Save manually, then try again.\",\n buttons: [\"OK\"],\n });\n return;\n }\n }\n // response === 1 (Discard) falls through.\n autoUpdater.quitAndInstall();\n}\n\nautoUpdater.on(\"update-downloaded\", (info: UpdateDownloadedEvent) => {\n clearDownload();\n updateDownloaded = info;\n lastBackgroundError = null; // it got here in the end; stale retry noise would only mislead\n writeLog(\"info\", [`download: ${info.version} downloaded and ready to install`]);\n const win = activeWindow();\n if (!win) return;\n void themedPrompt(win, {\n buttons: [\"Restart now\", \"Later\"],\n defaultId: 0,\n cancelId: 1,\n message: \"Update ready to install\",\n detail: `${appName} ${info.version} has been downloaded. Restart now to install it, or it installs the next time you quit.`,\n unasked: true,\n }).then((response) => { if (response === 0) void quitAndInstallSafely(); });\n});\n\n/** Name the app and say how to find its window. Call once, at startup. */\nexport function configureUpdater(opts: UpdaterOptions): void {\n appName = opts.appName;\n activeWindowFn = opts.activeWindow;\n if (opts.logFile !== undefined) logPath = join(app.getPath(\"userData\"), opts.logFile);\n}\n\n/** Fire-and-forget background check; periodic + on launch. No-op in dev. */\nexport function startBackgroundUpdateCheck(): void {\n if (!app.isPackaged) return;\n autoUpdater.checkForUpdates().catch((err) => {\n console.error(\"AutoUpdater background check failed:\", err?.message || err);\n });\n}\n\n/** The Help ▸ Check for Updates… handler: always gives the user feedback (downloading / up to date /\n * error), unlike the silent background check. */\nexport async function manualCheckForUpdates(win?: BrowserWindow | null): Promise<void> {\n const parent = win || activeWindow();\n\n if (!app.isPackaged) {\n await themedPrompt(parent, {\n message: \"Updates unavailable in a development build\",\n detail: `Auto-update only runs in a packaged build. This is version ${app.getVersion()}.`,\n buttons: [\"OK\"],\n });\n return;\n }\n\n if (updateDownloaded) {\n const response = await themedPrompt(parent, {\n buttons: [\"Restart now\", \"Later\"],\n defaultId: 0,\n cancelId: 1,\n message: \"Update ready to install\",\n detail: `${appName} ${updateDownloaded.version} has been downloaded. Restart now to install it.`,\n });\n if (response === 0) await quitAndInstallSafely();\n return;\n }\n\n if (download) {\n // A download is already in flight: show it, live (the dialog subscribes to progress events).\n await themedPrompt(parent, {\n message: \"Update available\",\n detail: `${appName} ${download.info.version} is downloading. You'll be prompted to restart when it's ready.`,\n buttons: [\"OK\"],\n progress: true,\n });\n return;\n }\n\n try {\n // A manual check starts a fresh retry cycle: if the last one gave up, this is the user asking again.\n await autoUpdater.checkForUpdates();\n // With autoDownload off, `update-available` (fired during the await) starts the managed download,\n // so by here `download` is set iff the feed had something newer. (Read through locals: TS narrows the\n // module-level `download` to null after the guard above and cannot see the event handler mutate it.)\n const started = download as DownloadState | null;\n const ready = updateDownloaded as UpdateDownloadedEvent | null;\n if (started) {\n await themedPrompt(parent, {\n message: \"Update available\",\n detail: `${appName} ${started.info.version} is downloading. You'll be prompted to restart when it's ready.`,\n buttons: [\"OK\"],\n progress: true,\n });\n lastBackgroundError = null;\n } else if (ready) {\n // The check completed a download between our earlier guard and now (tiny window, but free to handle).\n const response = await themedPrompt(parent, {\n buttons: [\"Restart now\", \"Later\"], defaultId: 0, cancelId: 1,\n message: \"Update ready to install\",\n detail: `${appName} ${ready.version} has been downloaded. Restart now to install it.`,\n });\n if (response === 0) await quitAndInstallSafely();\n } else if (lastBackgroundError) {\n await themedPrompt(parent, {\n message: \"You're on the latest version.\",\n detail: `${appName} ${app.getVersion()} is up to date. The last update check reported an error:\\n${lastBackgroundError}`,\n buttons: [\"OK\"],\n });\n lastBackgroundError = null;\n } else {\n await themedPrompt(parent, {\n message: \"You're on the latest version.\",\n detail: `${appName} ${app.getVersion()} is up to date.`,\n buttons: [\"OK\"],\n });\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const detail = lastBackgroundError && lastBackgroundError !== message\n ? `${message}\\n\\nA previous background check also failed with:\\n${lastBackgroundError}`\n : message;\n await themedPrompt(parent, { message: \"Update check failed\", detail, buttons: [\"OK\"] });\n lastBackgroundError = null;\n }\n}\n"]}
@@ -19,6 +19,10 @@ interface UpdaterPromptOptions {
19
19
  cancelId?: number;
20
20
  /** Show live download progress in the dialog. */
21
21
  progress?: boolean;
22
+ /** Nobody asked for this prompt: it arrived on its own (an update finished
23
+ * downloading). Focus starts on the cancel button, so a keystroke meant for
24
+ * whatever the prompt covered dismisses it rather than answering yes. */
25
+ unasked?: boolean;
22
26
  }
23
27
  /** The channels the app's renderer must answer. Named here so both sides agree. */
24
28
  declare const UPDATER_CHANNELS: {
package/dist/updater.d.ts CHANGED
@@ -19,6 +19,10 @@ interface UpdaterPromptOptions {
19
19
  cancelId?: number;
20
20
  /** Show live download progress in the dialog. */
21
21
  progress?: boolean;
22
+ /** Nobody asked for this prompt: it arrived on its own (an update finished
23
+ * downloading). Focus starts on the cancel button, so a keystroke meant for
24
+ * whatever the prompt covered dismisses it rather than answering yes. */
25
+ unasked?: boolean;
22
26
  }
23
27
  /** The channels the app's renderer must answer. Named here so both sides agree. */
24
28
  declare const UPDATER_CHANNELS: {