@wildwinter/app-shell 0.37.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/LICENSES.md +62 -0
  2. package/README.md +21 -0
  3. package/dist/about.css +13 -34
  4. package/dist/about.css.map +1 -1
  5. package/dist/anchored.css +5 -5
  6. package/dist/anchored.css.map +1 -1
  7. package/dist/app-store.cjs +15 -0
  8. package/dist/app-store.cjs.map +1 -1
  9. package/dist/app-store.d.cts +25 -1
  10. package/dist/app-store.d.ts +25 -1
  11. package/dist/app-store.js +14 -1
  12. package/dist/app-store.js.map +1 -1
  13. package/dist/chunk-2ECJUTSX.js +97 -0
  14. package/dist/chunk-2ECJUTSX.js.map +1 -0
  15. package/dist/chunk-2NMVIM54.js +55 -0
  16. package/dist/chunk-2NMVIM54.js.map +1 -0
  17. package/dist/chunk-C6UVQYKI.js +70 -0
  18. package/dist/chunk-C6UVQYKI.js.map +1 -0
  19. package/dist/chunk-IU7EY6BI.js +26 -0
  20. package/dist/chunk-IU7EY6BI.js.map +1 -0
  21. package/dist/chunk-K2AOLQW4.js +82 -0
  22. package/dist/chunk-K2AOLQW4.js.map +1 -0
  23. package/dist/chunk-KGAMP5LW.js +87 -0
  24. package/dist/chunk-KGAMP5LW.js.map +1 -0
  25. package/dist/chunk-L5HOBEKL.js +534 -0
  26. package/dist/chunk-L5HOBEKL.js.map +1 -0
  27. package/dist/chunk-P4G4ERG6.js +60 -0
  28. package/dist/chunk-P4G4ERG6.js.map +1 -0
  29. package/dist/chunk-P6WLAEKZ.js +71 -0
  30. package/dist/chunk-P6WLAEKZ.js.map +1 -0
  31. package/dist/chunk-S4KWQLYN.js +66 -0
  32. package/dist/chunk-S4KWQLYN.js.map +1 -0
  33. package/dist/chunk-XZYZXY7I.js +554 -0
  34. package/dist/chunk-XZYZXY7I.js.map +1 -0
  35. package/dist/comments.css +5 -3
  36. package/dist/comments.css.map +1 -1
  37. package/dist/confirm.css +1 -54
  38. package/dist/confirm.css.map +1 -1
  39. package/dist/context-menu.cjs +250 -0
  40. package/dist/context-menu.cjs.map +1 -0
  41. package/dist/context-menu.css +64 -0
  42. package/dist/context-menu.css.map +1 -0
  43. package/dist/context-menu.d.cts +25 -0
  44. package/dist/context-menu.d.ts +25 -0
  45. package/dist/context-menu.js +5 -0
  46. package/dist/context-menu.js.map +1 -0
  47. package/dist/controls.css +155 -0
  48. package/dist/controls.css.map +1 -0
  49. package/dist/dialog.cjs +222 -0
  50. package/dist/dialog.cjs.map +1 -0
  51. package/dist/dialog.css +55 -0
  52. package/dist/dialog.css.map +1 -0
  53. package/dist/dialog.d.cts +28 -0
  54. package/dist/dialog.d.ts +28 -0
  55. package/dist/dialog.js +5 -0
  56. package/dist/dialog.js.map +1 -0
  57. package/dist/floating.cjs +83 -0
  58. package/dist/floating.cjs.map +1 -0
  59. package/dist/floating.d.cts +25 -0
  60. package/dist/floating.d.ts +25 -0
  61. package/dist/floating.js +4 -0
  62. package/dist/floating.js.map +1 -0
  63. package/dist/identity.css +15 -50
  64. package/dist/identity.css.map +1 -1
  65. package/dist/index.cjs +1322 -258
  66. package/dist/index.cjs.map +1 -1
  67. package/dist/index.d.cts +349 -131
  68. package/dist/index.d.ts +349 -131
  69. package/dist/index.js +259 -808
  70. package/dist/index.js.map +1 -1
  71. package/dist/job.css +23 -0
  72. package/dist/job.css.map +1 -1
  73. package/dist/link-status.cjs +430 -0
  74. package/dist/link-status.cjs.map +1 -0
  75. package/dist/link-status.css +76 -0
  76. package/dist/link-status.css.map +1 -0
  77. package/dist/link-status.d.cts +53 -0
  78. package/dist/link-status.d.ts +53 -0
  79. package/dist/link-status.js +5 -0
  80. package/dist/link-status.js.map +1 -0
  81. package/dist/menu.cjs +62 -1
  82. package/dist/menu.cjs.map +1 -1
  83. package/dist/menu.d.cts +88 -1
  84. package/dist/menu.d.ts +88 -1
  85. package/dist/menu.js +1 -1
  86. package/dist/notes-editor.css +3 -46
  87. package/dist/notes-editor.css.map +1 -1
  88. package/dist/pane-shell.css +16 -3
  89. package/dist/pane-shell.css.map +1 -1
  90. package/dist/property-list-D3VZCJzi.d.cts +162 -0
  91. package/dist/property-list-D3VZCJzi.d.ts +162 -0
  92. package/dist/property-list.cjs +784 -0
  93. package/dist/property-list.cjs.map +1 -0
  94. package/dist/property-list.d.cts +1 -0
  95. package/dist/property-list.d.ts +1 -0
  96. package/dist/property-list.js +4 -0
  97. package/dist/property-list.js.map +1 -0
  98. package/dist/settings.css +63 -8
  99. package/dist/settings.css.map +1 -1
  100. package/dist/stale.css +3 -0
  101. package/dist/stale.css.map +1 -1
  102. package/dist/stepper.css +35 -0
  103. package/dist/stepper.css.map +1 -1
  104. package/dist/tokens.css +19 -0
  105. package/dist/tokens.css.map +1 -1
  106. package/dist/tool-window.cjs +63 -0
  107. package/dist/tool-window.cjs.map +1 -1
  108. package/dist/tool-window.css +6 -3
  109. package/dist/tool-window.css.map +1 -1
  110. package/dist/tool-window.d.cts +67 -1
  111. package/dist/tool-window.d.ts +67 -1
  112. package/dist/tool-window.js +63 -1
  113. package/dist/tool-window.js.map +1 -1
  114. package/dist/updater-view.cjs +283 -0
  115. package/dist/updater-view.cjs.map +1 -0
  116. package/dist/updater-view.d.cts +23 -0
  117. package/dist/updater-view.d.ts +23 -0
  118. package/dist/updater-view.js +6 -0
  119. package/dist/updater-view.js.map +1 -0
  120. package/dist/updater.cjs +12 -16
  121. package/dist/updater.cjs.map +1 -1
  122. package/dist/updater.css +44 -0
  123. package/dist/updater.css.map +1 -0
  124. package/dist/updater.js +12 -16
  125. package/dist/updater.js.map +1 -1
  126. package/dist/util.cjs +78 -0
  127. package/dist/util.cjs.map +1 -0
  128. package/dist/util.d.cts +37 -0
  129. package/dist/util.d.ts +37 -0
  130. package/dist/util.js +3 -0
  131. package/dist/util.js.map +1 -0
  132. package/dist/vc.css +3 -2
  133. package/dist/vc.css.map +1 -1
  134. package/dist/welcome.cjs +215 -0
  135. package/dist/welcome.cjs.map +1 -0
  136. package/dist/welcome.css +112 -0
  137. package/dist/welcome.css.map +1 -0
  138. package/dist/welcome.d.cts +55 -0
  139. package/dist/welcome.d.ts +55 -0
  140. package/dist/welcome.js +4 -0
  141. package/dist/welcome.js.map +1 -0
  142. package/package.json +58 -2
  143. package/dist/chunk-QDPFU4WX.js +0 -43
  144. package/dist/chunk-QDPFU4WX.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/menu.ts"],"names":[],"mappings":"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","file":"chunk-2ECJUTSX.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Shell-owned menu naming: the labels + accelerators for the pane-frame menu\n// items, single-sourced so every app in the family names them identically\n// (and a rename here flows to all of them). Pure data - no electron, no DOM -\n// so a main-process menu builder can import it. Each app builds its own\n// MenuItem from these and wires the click to its shell controller.\n// ---------------------------------------------------------------------------\n\nimport type { RecentProject } from \"./app-store.js\";\n\nexport interface MenuLabel {\n label: string;\n /** Electron accelerator string, when the item has one. */\n accelerator?: string;\n}\n\n/**\n * The Edit menu items the family shares.\n *\n * Undo and Redo are here and the MECHANISM is not, which is the whole finding\n * about undo: Patterpad's is ProseMirror's document history, Storyletter's is a\n * replay of file bytes, and those have nothing in common but this. See\n * `design/shared-shell.md`.\n *\n * The rule both apps discovered separately, and the reason these are spelled\n * out rather than left as Electron's `role: \"undo\"`: the native role runs a DOM\n * text-field undo, which knows nothing about either app's model and will\n * silently undo the wrong thing.\n */\nexport const EDIT_MENU: {\n undo: MenuLabel;\n redo: MenuLabel;\n duplicate: MenuLabel;\n find: MenuLabel;\n /** Replace is Find's second tab, on the platform's key for it. */\n replace: { label: string; acceleratorMac: string; acceleratorOther: string };\n} = {\n undo: { label: \"Undo\", accelerator: \"CmdOrCtrl+Z\" },\n redo: { label: \"Redo\", accelerator: \"Shift+CmdOrCtrl+Z\" },\n duplicate: { label: \"Duplicate\", accelerator: \"CmdOrCtrl+D\" },\n find: { label: \"Find…\", accelerator: \"CmdOrCtrl+F\" },\n replace: { label: \"Replace…\", acceleratorMac: \"Cmd+Alt+F\", acceleratorOther: \"Ctrl+H\" },\n};\n\n/**\n * Back / Forward: navigation HISTORY, the other axis from Up a Level's\n * hierarchy. One string cannot serve both platforms - the mac pair is Xcode's\n * (Cmd+[ is already Up a Level in this family, so the browser pair is taken),\n * and the Alt pair is what every Windows browser and Explorer trained - so the\n * menu builder picks by platform.\n */\nexport const GO_MENU: {\n back: { label: string; acceleratorMac: string; acceleratorOther: string };\n forward: { label: string; acceleratorMac: string; acceleratorOther: string };\n} = {\n back: { label: \"Back\", acceleratorMac: \"Ctrl+Cmd+Left\", acceleratorOther: \"Alt+Left\" },\n forward: { label: \"Forward\", acceleratorMac: \"Ctrl+Cmd+Right\", acceleratorOther: \"Alt+Right\" },\n};\n\n/** The pane-frame menu items (a View menu, conventionally). */\nexport const PANE_MENU: {\n showNav: MenuLabel;\n showInspector: MenuLabel;\n resetView: MenuLabel;\n} = {\n showNav: { label: \"Show Navigator\", accelerator: \"CmdOrCtrl+1\" },\n showInspector: { label: \"Show Inspector\", accelerator: \"CmdOrCtrl+2\" },\n resetView: { label: \"Reset View\" },\n};\n\n/**\n * The Help menu items the family shares, and the one platform rule that goes\n * with them.\n *\n * Lifted from Patterpad, which is the only app that has ever had these: a\n * second app writing its own updater or About is pure waste (`design/\n * shared-shell.md`), and a suite is recognised by finding Check for Updates\n * where you left it. Storyletter will grow both; until it does, these labels\n * are the agreement rather than a description of two implementations.\n *\n * PLACEMENT, which differs by platform and is easy to get wrong:\n * - macOS keeps About in the APP menu (the one named after the product), and\n * Help carries documentation + Check for Updates only.\n * - Windows and Linux have no app menu, so About goes at the foot of Help,\n * after a separator. That is the conventional home there.\n */\nexport const HELP_MENU: {\n checkForUpdates: MenuLabel;\n} = {\n checkForUpdates: { label: \"Check for Updates\\u2026\" },\n};\n\n/** The app-menu items the family shares (macOS; the identity item also belongs in File elsewhere). */\nexport const APP_MENU: {\n userInfo: MenuLabel;\n} = {\n /** Name + optional email, used to sign edits and comments. One label across the suite. */\n userInfo: { label: \"User Information\\u2026\" },\n};\n\n/** What an app must tell the shell to build its named menu items. */\nexport interface MenuNaming {\n /** The product, as it appears in menus: \"Patterpad\", \"Storyletter\". */\n appName: string;\n /** The product's own documentation URL. Omit while an app has no docs yet and\n * the item is rendered disabled rather than opening a broken link. */\n docsUrl?: string;\n /** The suite's documentation home, shared by every app in the family. */\n suiteDocsUrl?: string;\n /** The suite, as it appears in \"<suite> Documentation Home\". Defaults to \"Patter\". */\n suiteName?: string;\n}\n\n/** A named menu item: the label, plus the URL it opens (absent = nothing wired yet). */\nexport interface NamedMenuItem extends MenuLabel {\n url?: string;\n /** False when the app has not supplied a URL, so the builder can render it\n * disabled instead of opening nothing. A placeholder, honestly labelled. */\n ready: boolean;\n}\n\n/**\n * The menu labels that carry a product or suite name, so no app spells its own\n * About or Documentation item differently from the rest of the family.\n *\n * An app with no documentation site yet still gets the ITEM, marked\n * `ready: false`, so the menu has its family shape from day one and wiring the\n * URL later is a one-line change rather than a menu redesign.\n */\nexport function namedMenuItems(naming: MenuNaming): {\n about: NamedMenuItem;\n docs: NamedMenuItem;\n suiteDocs: NamedMenuItem;\n} {\n const suite = naming.suiteName ?? \"Patter\";\n return {\n about: { label: `About ${naming.appName}`, ready: true },\n docs: {\n label: `${naming.appName} Documentation`,\n ...(naming.docsUrl ? { url: naming.docsUrl } : {}),\n ready: naming.docsUrl !== undefined,\n },\n suiteDocs: {\n label: `${suite} Documentation Home`,\n ...(naming.suiteDocsUrl ? { url: naming.suiteDocsUrl } : {}),\n ready: naming.suiteDocsUrl !== undefined,\n },\n };\n}\n\n// --- the rest of the spine (ui-review-2026-09, finding 12) -----------------------\n// Each app used roughly half of what is above and hand-typed the other half,\n// and the copies had already drifted (Review Feedback's accelerator spelled\n// two ways; \"No Recent Projects\" vs \"No recent projects\"). Labels are Title\n// Case, as the platform's menus are; an item that opens something ends in the\n// command ellipsis.\n\n/** The File menu items the family shares. */\nexport const FILE_MENU: {\n newProject: MenuLabel;\n openProject: MenuLabel;\n openRecent: MenuLabel;\n /** The empty Open Recent submenu's one disabled item. */\n noRecents: MenuLabel;\n clearRecents: MenuLabel;\n closeProject: MenuLabel;\n save: MenuLabel;\n saveAs: MenuLabel;\n projectSettings: MenuLabel;\n} = {\n newProject: { label: \"New Project…\", accelerator: \"CmdOrCtrl+N\" },\n openProject: { label: \"Open Project…\", accelerator: \"CmdOrCtrl+O\" },\n openRecent: { label: \"Open Recent\" },\n noRecents: { label: \"No Recent Projects\" },\n clearRecents: { label: \"Clear Recents\" },\n closeProject: { label: \"Close Project\" },\n save: { label: \"Save\", accelerator: \"CmdOrCtrl+S\" },\n saveAs: { label: \"Save As…\", accelerator: \"Shift+CmdOrCtrl+S\" },\n projectSettings: { label: \"Project Settings…\", accelerator: \"CmdOrCtrl+,\" },\n};\n\n/**\n * The Play menu's shared item. The play SURFACE itself (Patterpad's \"Play\n * Scene\" on Cmd+P, Storyletter's \"The Board\" on Cmd+T) is app-named and\n * app-keyed on purpose: the two are not the same act, so the family reserves\n * neither key.\n */\nexport const PLAY_MENU: {\n liveLink: MenuLabel;\n} = {\n liveLink: { label: \"Live Link\" },\n};\n\n/** The Review menu: examining what you have built rather than building or\n * playing it. Review Feedback's accelerator is spelled ONCE, here. */\nexport const REVIEW_MENU: {\n reviewFeedback: MenuLabel;\n nextFeedback: MenuLabel;\n previousFeedback: MenuLabel;\n coverageTest: MenuLabel;\n findPropertyUsage: MenuLabel;\n showResolvedComments: MenuLabel;\n} = {\n reviewFeedback: { label: \"Review Feedback\", accelerator: \"CmdOrCtrl+Shift+R\" },\n nextFeedback: { label: \"Next Feedback\", accelerator: \"F8\" },\n previousFeedback: { label: \"Previous Feedback\", accelerator: \"Shift+F8\" },\n coverageTest: { label: \"Coverage Test…\", accelerator: \"Shift+CmdOrCtrl+C\" },\n findPropertyUsage: { label: \"Find Property Usage…\" },\n showResolvedComments: { label: \"Show Resolved Comments\" },\n};\n\n/** The Publish menu: everything that turns the project into something you\n * hand to others (\"Publish\", never \"Export\"). */\nexport const PUBLISH_MENU: {\n playableHtml: MenuLabel;\n bundle: MenuLabel;\n autoRebuild: MenuLabel;\n} = {\n playableHtml: { label: \"Publish Playable HTML…\" },\n bundle: { label: \"Publish Bundle\", accelerator: \"Shift+CmdOrCtrl+B\" },\n autoRebuild: { label: \"Auto Rebuild\" },\n};\n\n/** The View menu items beyond the pane toggles (PANE_MENU) and history\n * (GO_MENU). Cmd+[ is family-reserved for Up a Level, which is why Back\n * takes Xcode's pair on the mac. */\nexport const VIEW_MENU: {\n projectOverview: MenuLabel;\n upALevel: MenuLabel;\n colourTheme: MenuLabel;\n} = {\n projectOverview: { label: \"Project Overview\" },\n upALevel: { label: \"Up a Level\", accelerator: \"CmdOrCtrl+[\" },\n colourTheme: { label: \"Colour Theme\" },\n};\n\n/** A path with the home directory as `~`, so a recents entry stays readable\n * at menu width. `home` is `app.getPath(\"home\")`; this file stays\n * electron-free. */\nexport function tildePath(path: string, home: string): string {\n if (path === home) return \"~\";\n const sep = home.includes(\"\\\\\") ? \"\\\\\" : \"/\";\n return path.startsWith(home + sep) ? `~${path.slice(home.length)}` : path;\n}\n\n/** As much of Electron's MenuItemConstructorOptions as a recents entry needs;\n * structurally assignable to it. */\nexport interface RecentsMenuItem {\n label?: string;\n sublabel?: string;\n toolTip?: string;\n enabled?: boolean;\n type?: \"separator\";\n click?: () => void;\n}\n\nexport interface RecentsSubmenuOptions {\n onOpen: (path: string) => void;\n /** Offered after a separator when there is anything to clear. */\n onClear?: () => void;\n /** `app.getPath(\"home\")`, for the `~` abbreviation. */\n home: string;\n /** Defaults to process.platform; a seam for the tests. */\n platform?: string;\n}\n\n/**\n * The Open Recent submenu: the fifteen lines both apps had.\n *\n * Shows WHERE each recent lives. macOS renders `sublabel` as a dimmed second\n * line and `toolTip` on hover; native menus elsewhere render neither, so\n * there the path is folded into the label itself. Empty, it is the one\n * disabled \"No Recent Projects\" item.\n */\nexport function recentsSubmenu(recents: RecentProject[], opts: RecentsSubmenuOptions): RecentsMenuItem[] {\n const isMac = (opts.platform ?? process.platform) === \"darwin\";\n if (recents.length === 0) return [{ label: FILE_MENU.noRecents.label, enabled: false }];\n const items: RecentsMenuItem[] = recents.map((r) => {\n const shown = tildePath(r.path, opts.home);\n const name = r.name ?? r.path.split(/[\\\\/]/).filter(Boolean).pop() ?? r.path;\n return {\n label: isMac ? name : `${name} (${shown})`,\n sublabel: shown,\n toolTip: r.path,\n click: () => opts.onOpen(r.path),\n };\n });\n if (opts.onClear) items.push({ type: \"separator\" }, { label: FILE_MENU.clearRecents.label, click: opts.onClear });\n return items;\n}\n"]}
@@ -0,0 +1,55 @@
1
+ import { el } from './chunk-XZYZXY7I.js';
2
+ import { closeWithExit } from './chunk-IU7EY6BI.js';
3
+
4
+ // src/dialog.ts
5
+ function dialogFrame(opts) {
6
+ const dialog = el("dialog", `shell-dialog ${opts.className ?? ""}`.trim());
7
+ const body = el("div", "shell-dialog-body");
8
+ const actions = el("div", "shell-dialog-actions");
9
+ dialog.append(el("h2", "shell-dialog-title", opts.title));
10
+ if (opts.sub !== void 0) dialog.append(el("p", "shell-dialog-sub", opts.sub));
11
+ dialog.append(body, actions);
12
+ dialog.setAttribute("aria-label", opts.title);
13
+ document.body.append(dialog);
14
+ let state = "closed";
15
+ let opener = null;
16
+ const teardown = () => {
17
+ dialog.remove();
18
+ if (opener instanceof HTMLElement && opener.isConnected) opener.focus();
19
+ opener = null;
20
+ state = "closed";
21
+ opts.onClose?.();
22
+ };
23
+ const close = () => {
24
+ if (state !== "open") return;
25
+ state = "closing";
26
+ closeWithExit(dialog, () => {
27
+ if (dialog.open) dialog.close();
28
+ teardown();
29
+ });
30
+ };
31
+ dialog.addEventListener("cancel", (e) => {
32
+ e.preventDefault();
33
+ close();
34
+ });
35
+ dialog.addEventListener("close", () => {
36
+ if (state === "open") teardown();
37
+ });
38
+ return {
39
+ dialog,
40
+ body,
41
+ actions,
42
+ open() {
43
+ if (state !== "closed") return;
44
+ opener = document.activeElement;
45
+ state = "open";
46
+ dialog.showModal();
47
+ },
48
+ close,
49
+ isOpen: () => state === "open"
50
+ };
51
+ }
52
+
53
+ export { dialogFrame };
54
+ //# sourceMappingURL=chunk-2NMVIM54.js.map
55
+ //# sourceMappingURL=chunk-2NMVIM54.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dialog.ts"],"names":[],"mappings":";;;;AAiDO,SAAS,YAAY,IAAA,EAAuC;AACjE,EAAA,MAAM,MAAA,GAAS,GAAG,QAAA,EAAU,CAAA,aAAA,EAAgB,KAAK,SAAA,IAAa,EAAE,CAAA,CAAA,CAAG,IAAA,EAAM,CAAA;AACzE,EAAA,MAAM,IAAA,GAAO,EAAA,CAAG,KAAA,EAAO,mBAAmB,CAAA;AAC1C,EAAA,MAAM,OAAA,GAAU,EAAA,CAAG,KAAA,EAAO,sBAAsB,CAAA;AAChD,EAAA,MAAA,CAAO,OAAO,EAAA,CAAG,IAAA,EAAM,oBAAA,EAAsB,IAAA,CAAK,KAAK,CAAC,CAAA;AACxD,EAAA,IAAI,IAAA,CAAK,GAAA,KAAQ,MAAA,EAAW,MAAA,CAAO,MAAA,CAAO,GAAG,GAAA,EAAK,kBAAA,EAAoB,IAAA,CAAK,GAAG,CAAC,CAAA;AAC/E,EAAA,MAAA,CAAO,MAAA,CAAO,MAAM,OAAO,CAAA;AAC3B,EAAA,MAAA,CAAO,YAAA,CAAa,YAAA,EAAc,IAAA,CAAK,KAAK,CAAA;AAC5C,EAAA,QAAA,CAAS,IAAA,CAAK,OAAO,MAAM,CAAA;AAE3B,EAAA,IAAI,KAAA,GAAuC,QAAA;AAC3C,EAAA,IAAI,MAAA,GAAyB,IAAA;AAE7B,EAAA,MAAM,WAAW,MAAY;AAC3B,IAAA,MAAA,CAAO,MAAA,EAAO;AAId,IAAA,IAAI,MAAA,YAAkB,WAAA,IAAe,MAAA,CAAO,WAAA,SAAoB,KAAA,EAAM;AACtE,IAAA,MAAA,GAAS,IAAA;AACT,IAAA,KAAA,GAAQ,QAAA;AACR,IAAA,IAAA,CAAK,OAAA,IAAU;AAAA,EACjB,CAAA;AAEA,EAAA,MAAM,QAAQ,MAAY;AACxB,IAAA,IAAI,UAAU,MAAA,EAAQ;AACtB,IAAA,KAAA,GAAQ,SAAA;AACR,IAAA,aAAA,CAAc,QAAQ,MAAM;AAAE,MAAA,IAAI,MAAA,CAAO,IAAA,EAAM,MAAA,CAAO,KAAA,EAAM;AAAG,MAAA,QAAA,EAAS;AAAA,IAAG,CAAC,CAAA;AAAA,EAC9E,CAAA;AAGA,EAAA,MAAA,CAAO,gBAAA,CAAiB,QAAA,EAAU,CAAC,CAAA,KAAM;AAAE,IAAA,CAAA,CAAE,cAAA,EAAe;AAAG,IAAA,KAAA,EAAM;AAAA,EAAG,CAAC,CAAA;AAGzE,EAAA,MAAA,CAAO,gBAAA,CAAiB,SAAS,MAAM;AAAE,IAAA,IAAI,KAAA,KAAU,QAAQ,QAAA,EAAS;AAAA,EAAG,CAAC,CAAA;AAE5E,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IAAQ,IAAA;AAAA,IAAM,OAAA;AAAA,IACd,IAAA,GAAO;AACL,MAAA,IAAI,UAAU,QAAA,EAAU;AACxB,MAAA,MAAA,GAAS,QAAA,CAAS,aAAA;AAClB,MAAA,KAAA,GAAQ,MAAA;AACR,MAAA,MAAA,CAAO,SAAA,EAAU;AAAA,IACnB,CAAA;AAAA,IACA,KAAA;AAAA,IACA,MAAA,EAAQ,MAAM,KAAA,KAAU;AAAA,GAC1B;AACF","file":"chunk-2NMVIM54.js","sourcesContent":["// ---------------------------------------------------------------------------\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"]}
@@ -0,0 +1,70 @@
1
+ import { el } from './chunk-XZYZXY7I.js';
2
+
3
+ // src/welcome.ts
4
+ function mountWelcome(host, opts) {
5
+ const caption = (text) => el("h2", "welcome-caption", text);
6
+ const group = (label, ...body) => el("section", "welcome-group", caption(label), ...body);
7
+ const actions = el("div", "welcome-actions");
8
+ for (const a of opts.actions) {
9
+ const b = el("button", { className: `btn${a.primary ? " primary" : ""}`, text: a.label, onClick: a.onClick });
10
+ b.type = "button";
11
+ actions.append(b);
12
+ }
13
+ const groups = (opts.groups ?? []).map((g) => group(
14
+ g.caption,
15
+ g.note !== void 0 ? el("p", "welcome-note", g.note) : null,
16
+ el("div", "welcome-list", ...g.items.map((item) => {
17
+ const b = el(
18
+ "button",
19
+ { className: "welcome-item", onClick: item.onOpen },
20
+ el("span", "welcome-item-name", item.name),
21
+ item.hint !== void 0 ? el("span", "welcome-item-hint", item.hint) : null
22
+ );
23
+ b.type = "button";
24
+ return b;
25
+ }))
26
+ ));
27
+ const recentsHost = el("div", "welcome-recents-slot");
28
+ const drawRecents = (recents) => {
29
+ const shown = recents.slice(0, opts.maxRecents ?? 5);
30
+ recentsHost.replaceChildren();
31
+ if (shown.length === 0) return;
32
+ recentsHost.append(group(
33
+ opts.recentsCaption ?? "Recent",
34
+ el("div", "welcome-recents", ...shown.map((r) => {
35
+ const b = el(
36
+ "button",
37
+ { className: "welcome-recent", onClick: r.onOpen },
38
+ el("span", "welcome-recent-name", r.name),
39
+ el("span", "welcome-recent-path", r.path)
40
+ );
41
+ b.type = "button";
42
+ return b;
43
+ }))
44
+ ));
45
+ };
46
+ drawRecents(opts.recents);
47
+ const error = el("p", "welcome-error");
48
+ const setError = (text) => {
49
+ error.textContent = text ?? "";
50
+ error.hidden = text === void 0 || text === "";
51
+ };
52
+ setError(opts.error);
53
+ const root = el("div", "welcome", el(
54
+ "div",
55
+ "welcome-card",
56
+ el("h1", "welcome-title", opts.title),
57
+ el("p", "welcome-sub", opts.sub),
58
+ actions,
59
+ opts.tourLine ? el("div", "welcome-line", opts.tourLine) : null,
60
+ ...groups,
61
+ recentsHost,
62
+ error
63
+ ));
64
+ host.replaceChildren(root);
65
+ return { el: root, setRecents: drawRecents, setError };
66
+ }
67
+
68
+ export { mountWelcome };
69
+ //# sourceMappingURL=chunk-C6UVQYKI.js.map
70
+ //# sourceMappingURL=chunk-C6UVQYKI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/welcome.ts"],"names":[],"mappings":"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","file":"chunk-C6UVQYKI.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The welcome screen: what an app shows with no project open. Title, one line\n// under it, the actions (Open, New), an optional teaching line, optional\n// captioned groups (Storyletter's shipped examples), and the recents.\n//\n// Both apps drew this with the same twelve CSS rules under two class prefixes\n// (`.recent-name` / `.wel-recent-name`), four of them byte-identical. The CSS\n// here is Patterpad's; the captioned groups are Storyletter's, as an optional\n// slot, drawn as a plain list of rows (name + hint) rather than cards.\n//\n// The whole screen is a drag region (the window has no title bar to grab\n// here) and every control opts out. Styles ship as welcome.css.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\n\nexport interface WelcomeAction {\n label: string;\n onClick: () => void;\n /** The one accent button (Open a project…). */\n primary?: boolean;\n}\n\nexport interface WelcomeRecent {\n /** What the project calls itself, with the folder stem as the fallback. */\n name: string;\n /** Beside the name, because two projects may share a name and the folder\n * is how you tell them apart. */\n path: string;\n onOpen: () => void;\n}\n\nexport interface WelcomeGroupItem {\n name: string;\n /** What choosing it gets you, said before the click. */\n hint?: string;\n onOpen: () => void;\n}\n\nexport interface WelcomeGroup {\n /** Sentence-case label text (\"Learn from a finished project\"); never an overline. */\n caption: string;\n /** The consequence of a click, said before the click (\"Each opens as your\n * own copy, in a folder you choose.\"). */\n note?: string;\n items: WelcomeGroupItem[];\n}\n\nexport interface WelcomeOptions {\n title: string;\n sub: string;\n actions: WelcomeAction[];\n recents: WelcomeRecent[];\n groups?: WelcomeGroup[];\n /** The teaching door: a line under the actions, quieter than a button\n * (\"New to Patter? Take the interactive tour\"). The app builds and styles\n * it; a text action in it is a link, not bare muted text. */\n tourLine?: HTMLElement;\n /** The recents caption. Default \"Recent\". */\n recentsCaption?: string;\n /** How many recents to show. Default 5. */\n maxRecents?: number;\n /** A sentence about why the last open failed, under everything. */\n error?: string;\n}\n\nexport interface Welcome {\n el: HTMLElement;\n /** Redraw the recents (the list changes as projects open and close). */\n setRecents(recents: WelcomeRecent[]): void;\n setError(error: string | undefined): void;\n}\n\n/** Mount the welcome into `host` (replacing its children). */\nexport function mountWelcome(host: HTMLElement, opts: WelcomeOptions): Welcome {\n const caption = (text: string): HTMLElement => el(\"h2\", \"welcome-caption\", text);\n const group = (label: string, ...body: (Node | null)[]): HTMLElement =>\n el(\"section\", \"welcome-group\", caption(label), ...body);\n\n const actions = el(\"div\", \"welcome-actions\");\n for (const a of opts.actions) {\n const b = el(\"button\", { className: `btn${a.primary ? \" primary\" : \"\"}`, text: a.label, onClick: a.onClick });\n b.type = \"button\";\n actions.append(b);\n }\n\n const groups = (opts.groups ?? []).map((g) => group(g.caption,\n g.note !== undefined ? el(\"p\", \"welcome-note\", g.note) : null,\n el(\"div\", \"welcome-list\", ...g.items.map((item) => {\n const b = el(\"button\", { className: \"welcome-item\", onClick: item.onOpen },\n el(\"span\", \"welcome-item-name\", item.name),\n item.hint !== undefined ? el(\"span\", \"welcome-item-hint\", item.hint) : null);\n b.type = \"button\";\n return b;\n })),\n ));\n\n const recentsHost = el(\"div\", \"welcome-recents-slot\");\n const drawRecents = (recents: WelcomeRecent[]): void => {\n const shown = recents.slice(0, opts.maxRecents ?? 5);\n recentsHost.replaceChildren();\n if (shown.length === 0) return;\n recentsHost.append(group(opts.recentsCaption ?? \"Recent\",\n el(\"div\", \"welcome-recents\", ...shown.map((r) => {\n const b = el(\"button\", { className: \"welcome-recent\", onClick: r.onOpen },\n el(\"span\", \"welcome-recent-name\", r.name),\n el(\"span\", \"welcome-recent-path\", r.path));\n b.type = \"button\";\n return b;\n })),\n ));\n };\n drawRecents(opts.recents);\n\n const error = el(\"p\", \"welcome-error\");\n const setError = (text: string | undefined): void => {\n error.textContent = text ?? \"\";\n error.hidden = text === undefined || text === \"\";\n };\n setError(opts.error);\n\n const root = el(\"div\", \"welcome\", el(\"div\", \"welcome-card\",\n el(\"h1\", \"welcome-title\", opts.title),\n el(\"p\", \"welcome-sub\", opts.sub),\n actions,\n opts.tourLine ? el(\"div\", \"welcome-line\", opts.tourLine) : null,\n ...groups,\n recentsHost,\n error,\n ));\n host.replaceChildren(root);\n return { el: root, setRecents: drawRecents, setError };\n}\n"]}
@@ -0,0 +1,26 @@
1
+ // src/exit.ts
2
+ function closeWithExit(element, done) {
3
+ element.classList.add("closing");
4
+ const seconds = parseFloat(getComputedStyle(element).animationDuration) || 0;
5
+ if (seconds === 0) {
6
+ done();
7
+ return;
8
+ }
9
+ let fired = false;
10
+ const finish = () => {
11
+ if (fired) return;
12
+ fired = true;
13
+ element.removeEventListener("animationend", onEnd);
14
+ clearTimeout(timer);
15
+ done();
16
+ };
17
+ const onEnd = (e) => {
18
+ if (e.target === element) finish();
19
+ };
20
+ element.addEventListener("animationend", onEnd);
21
+ const timer = setTimeout(finish, seconds * 1e3 + 120);
22
+ }
23
+
24
+ export { closeWithExit };
25
+ //# sourceMappingURL=chunk-IU7EY6BI.js.map
26
+ //# sourceMappingURL=chunk-IU7EY6BI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/exit.ts"],"names":[],"mappings":";AAuBO,SAAS,aAAA,CAAc,SAAsB,IAAA,EAAwB;AAC1E,EAAA,OAAA,CAAQ,SAAA,CAAU,IAAI,SAAS,CAAA;AAG/B,EAAA,MAAM,UAAU,UAAA,CAAW,gBAAA,CAAiB,OAAO,CAAA,CAAE,iBAAiB,CAAA,IAAK,CAAA;AAC3E,EAAA,IAAI,YAAY,CAAA,EAAG;AAAE,IAAA,IAAA,EAAK;AAAG,IAAA;AAAA,EAAQ;AAErC,EAAA,IAAI,KAAA,GAAQ,KAAA;AACZ,EAAA,MAAM,SAAS,MAAY;AACzB,IAAA,IAAI,KAAA,EAAO;AACX,IAAA,KAAA,GAAQ,IAAA;AACR,IAAA,OAAA,CAAQ,mBAAA,CAAoB,gBAAgB,KAAK,CAAA;AACjD,IAAA,YAAA,CAAa,KAAK,CAAA;AAClB,IAAA,IAAA,EAAK;AAAA,EACP,CAAA;AAIA,EAAA,MAAM,KAAA,GAAQ,CAAC,CAAA,KAA4B;AAAE,IAAA,IAAI,CAAA,CAAE,MAAA,KAAW,OAAA,EAAS,MAAA,EAAO;AAAA,EAAG,CAAA;AACjF,EAAA,OAAA,CAAQ,gBAAA,CAAiB,gBAAgB,KAAK,CAAA;AAG9C,EAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,MAAA,EAAQ,OAAA,GAAU,MAAO,GAAG,CAAA;AACvD","file":"chunk-IU7EY6BI.js","sourcesContent":["// ---------------------------------------------------------------------------\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"]}
@@ -0,0 +1,82 @@
1
+ import { el } from './chunk-XZYZXY7I.js';
2
+ import { closeWithExit } from './chunk-IU7EY6BI.js';
3
+
4
+ // src/context-menu.ts
5
+ var EDGE = 6;
6
+ var current = null;
7
+ function floating(node, onDismiss) {
8
+ let gone = false;
9
+ const dismiss = () => {
10
+ if (gone) return;
11
+ gone = true;
12
+ if (current === dismiss) current = null;
13
+ window.removeEventListener("pointerdown", onAway, true);
14
+ window.removeEventListener("keydown", onKey, true);
15
+ closeWithExit(node, () => node.remove());
16
+ onDismiss?.();
17
+ };
18
+ const onAway = (e) => {
19
+ if (!node.contains(e.target)) dismiss();
20
+ };
21
+ const onKey = (e) => {
22
+ if (e.key === "Escape") {
23
+ e.preventDefault();
24
+ dismiss();
25
+ }
26
+ };
27
+ window.addEventListener("pointerdown", onAway, true);
28
+ window.addEventListener("keydown", onKey, true);
29
+ current?.();
30
+ current = dismiss;
31
+ return { dismiss };
32
+ }
33
+ function placeBelow(node, anchor) {
34
+ node.style.left = `${anchor.left}px`;
35
+ node.style.top = `${anchor.bottom + 4}px`;
36
+ const r = node.getBoundingClientRect();
37
+ if (r.right > window.innerWidth) node.style.left = `${Math.max(EDGE, window.innerWidth - r.width - EDGE)}px`;
38
+ if (r.bottom > window.innerHeight) node.style.top = `${Math.max(EDGE, anchor.top - r.height - 4)}px`;
39
+ }
40
+ function openPopover(anchor, build, onClose, opts = {}) {
41
+ const pop = el("div", "popover");
42
+ let closed = false;
43
+ const finish = () => {
44
+ if (closed) return;
45
+ closed = true;
46
+ onClose?.();
47
+ };
48
+ const { dismiss } = floating(pop, finish);
49
+ pop.append(build(dismiss));
50
+ (opts.host ?? document.body).append(pop);
51
+ placeBelow(pop, anchor.getBoundingClientRect());
52
+ }
53
+ function openContextMenu(x, y, items) {
54
+ const menu = el("div", "ctxmenu");
55
+ menu.setAttribute("role", "menu");
56
+ const { dismiss } = floating(menu);
57
+ for (const item of items) {
58
+ const button = el("button", {
59
+ className: `ctxmenu-item${item.danger ? " danger" : ""}`,
60
+ text: item.label,
61
+ onClick: () => {
62
+ if (item.disabled === true) return;
63
+ dismiss();
64
+ item.onClick();
65
+ }
66
+ });
67
+ button.type = "button";
68
+ button.setAttribute("role", "menuitem");
69
+ if (item.disabled === true) button.disabled = true;
70
+ menu.append(button);
71
+ }
72
+ menu.style.left = `${x}px`;
73
+ menu.style.top = `${y}px`;
74
+ document.body.append(menu);
75
+ const r = menu.getBoundingClientRect();
76
+ if (r.right > window.innerWidth) menu.style.left = `${Math.max(EDGE, window.innerWidth - r.width - EDGE)}px`;
77
+ if (r.bottom > window.innerHeight) menu.style.top = `${Math.max(EDGE, window.innerHeight - r.height - EDGE)}px`;
78
+ }
79
+
80
+ export { openContextMenu, openPopover };
81
+ //# sourceMappingURL=chunk-K2AOLQW4.js.map
82
+ //# sourceMappingURL=chunk-K2AOLQW4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/context-menu.ts"],"names":[],"mappings":"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","file":"chunk-K2AOLQW4.js","sourcesContent":["// ---------------------------------------------------------------------------\n// A right-click context menu and a small anchored popover: a floating list of\n// actions at the cursor (or a panel under a control), dismissed on outside\n// click or Escape, clamped to the viewport, closed with the family's exit\n// motion.\n//\n// Storyletter's, lifted (ui-review-2026-09, finding 11): its own header had\n// named itself an extraction candidate, and it had the two things Patterpad's\n// inline `sceneContextMenu` lacked, a `disabled` item and the clamp. A DOM\n// popover, not a native menu, so it is themed like everything else.\n//\n// Styles ship as context-menu.css; import it wherever the module is used,\n// including tool windows, which is the bug that made Storyletter give the\n// menu its own stylesheet in the first place.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\nimport { closeWithExit } from \"./exit.js\";\n\nexport interface ContextItem {\n label: string;\n /** Drawn in --danger: a destructive action, which names its object. */\n danger?: boolean;\n /** Shown greyed and unclickable. For an action that belongs on this menu\n * but cannot apply right now (Move up on the first row), so the menu keeps\n * the same shape and the same item stays in the same place every time. */\n disabled?: boolean;\n onClick: () => void;\n}\n\nconst EDGE = 6;\n\n/** The one open menu or popover. Only ever one, so a fresh open replaces it\n * and a re-render that removed the anchor cannot leave one hanging in the air. */\nlet current: (() => void) | null = null;\n\n/** Outside-click / Escape wiring shared by the menu and the popover. The\n * returned `dismiss` plays the exit and removes the node; `onDismiss` fires\n * for every way out so a caller can flush on all of them. */\nfunction floating(node: HTMLElement, onDismiss?: () => void): { dismiss: () => void } {\n let gone = false;\n const dismiss = (): void => {\n if (gone) return;\n gone = true;\n if (current === dismiss) current = null;\n window.removeEventListener(\"pointerdown\", onAway, true);\n window.removeEventListener(\"keydown\", onKey, true);\n // A node mid-fade must not take a second click.\n closeWithExit(node, () => node.remove());\n onDismiss?.();\n };\n const onAway = (e: PointerEvent): void => { if (!node.contains(e.target as Node)) dismiss(); };\n const onKey = (e: KeyboardEvent): void => { if (e.key === \"Escape\") { e.preventDefault(); dismiss(); } };\n window.addEventListener(\"pointerdown\", onAway, true);\n window.addEventListener(\"keydown\", onKey, true);\n current?.();\n current = dismiss;\n return { dismiss };\n}\n\n/** Clamp a floating node into the viewport, preferring below `anchor` and\n * flipping above it when there is no room. */\nfunction placeBelow(node: HTMLElement, anchor: DOMRect): void {\n node.style.left = `${anchor.left}px`;\n node.style.top = `${anchor.bottom + 4}px`;\n const r = node.getBoundingClientRect();\n if (r.right > window.innerWidth) node.style.left = `${Math.max(EDGE, window.innerWidth - r.width - EDGE)}px`;\n if (r.bottom > window.innerHeight) node.style.top = `${Math.max(EDGE, anchor.top - r.height - 4)}px`;\n}\n\nexport interface PopoverOptions {\n /** Where the popover is appended. Default <body>. A popover opened from\n * inside a modal <dialog> passes the dialog: a body-level node sits behind\n * the scrim, inert, while one inside the dialog paints in its top layer\n * (Patterpad's colour swatch popover in Project settings). */\n host?: HTMLElement;\n}\n\n/** Open a small panel anchored to an element; `build` gets a close callback.\n * `onClose` runs however it closes (Escape, outside click, or `close`), the\n * place to flush an edit the panel was collecting. Only one is ever open. */\nexport function openPopover(\n anchor: HTMLElement, build: (close: () => void) => HTMLElement, onClose?: () => void, opts: PopoverOptions = {},\n): void {\n const pop = el(\"div\", \"popover\");\n let closed = false;\n const finish = (): void => { if (closed) return; closed = true; onClose?.(); };\n const { dismiss } = floating(pop, finish);\n pop.append(build(dismiss));\n (opts.host ?? document.body).append(pop);\n placeBelow(pop, anchor.getBoundingClientRect());\n}\n\n/** Open a context menu at (x, y). Only one is ever open. */\nexport function openContextMenu(x: number, y: number, items: ContextItem[]): void {\n const menu = el(\"div\", \"ctxmenu\");\n menu.setAttribute(\"role\", \"menu\");\n const { dismiss } = floating(menu);\n\n for (const item of items) {\n const button = el(\"button\", {\n className: `ctxmenu-item${item.danger ? \" danger\" : \"\"}`, text: item.label,\n onClick: () => { if (item.disabled === true) return; dismiss(); item.onClick(); },\n });\n button.type = \"button\";\n button.setAttribute(\"role\", \"menuitem\");\n if (item.disabled === true) button.disabled = true;\n menu.append(button);\n }\n\n menu.style.left = `${x}px`;\n menu.style.top = `${y}px`;\n document.body.append(menu);\n // Keep it on-screen.\n const r = menu.getBoundingClientRect();\n if (r.right > window.innerWidth) menu.style.left = `${Math.max(EDGE, window.innerWidth - r.width - EDGE)}px`;\n if (r.bottom > window.innerHeight) menu.style.top = `${Math.max(EDGE, window.innerHeight - r.height - EDGE)}px`;\n}\n"]}
@@ -0,0 +1,87 @@
1
+ import { copyWithFeedback } from './chunk-P6WLAEKZ.js';
2
+ import { el, iconNode } from './chunk-XZYZXY7I.js';
3
+
4
+ // src/link-status.ts
5
+ function linkStatusClass(s) {
6
+ if (s.state === "off" || s.state === "error") return "off";
7
+ if (s.state === "listening") return "listening";
8
+ return s.build === "stale" ? "stale" : "live";
9
+ }
10
+ function linkStatusTip(s) {
11
+ switch (s.state) {
12
+ case "off":
13
+ return "Live link is off. Click to start listening.";
14
+ case "error":
15
+ return `Live link failed (${s.message}). Click to retry.`;
16
+ case "listening":
17
+ return "Live link is listening for a game. Click to stop.";
18
+ case "connected": {
19
+ const who = s.project !== void 0 ? ` to ${s.project}` : "";
20
+ const build = s.build === "stale" ? " on a different build, so save or rebuild to re-sync" : s.build === "match" ? " and in sync" : "";
21
+ const note = s.note ? ` ${s.note}` : "";
22
+ return `Live link is connected${who}${build}.${note} Click to stop.`;
23
+ }
24
+ }
25
+ }
26
+ function linkAddress(s) {
27
+ if (s.state !== "listening" && s.state !== "connected") return void 0;
28
+ return s.address ?? `ws://127.0.0.1:${s.port}`;
29
+ }
30
+ function mountLinkStatus(host, opts) {
31
+ const wrap = el("div", "linkstatus");
32
+ wrap.hidden = true;
33
+ const url = el("button", "linkstatus-url");
34
+ url.type = "button";
35
+ url.hidden = true;
36
+ const toggle = el("button", "linkstatus-toggle off");
37
+ toggle.type = "button";
38
+ toggle.append(iconNode("connect"));
39
+ if (opts.label !== void 0) wrap.append(el("span", "linkstatus-word", opts.label));
40
+ wrap.append(...opts.lead ?? [], url, toggle);
41
+ host.append(wrap);
42
+ let status = { state: "off" };
43
+ const render = () => {
44
+ toggle.className = `linkstatus-toggle ${linkStatusClass(status)}`;
45
+ const tip = linkStatusTip(status);
46
+ toggle.dataset["tip"] = tip;
47
+ toggle.setAttribute("aria-label", tip);
48
+ const addr = linkAddress(status);
49
+ if (addr !== void 0) {
50
+ url.textContent = addr;
51
+ url.dataset["tip"] = "Click to copy the live link address";
52
+ url.hidden = false;
53
+ } else url.hidden = true;
54
+ };
55
+ const apply = (s) => {
56
+ status = s;
57
+ render();
58
+ };
59
+ render();
60
+ toggle.addEventListener("click", () => {
61
+ const out = opts.onToggle(status);
62
+ void Promise.resolve(out).then((next) => {
63
+ if (next) apply(next);
64
+ });
65
+ });
66
+ url.addEventListener("click", () => {
67
+ const addr = url.textContent ?? "";
68
+ if (!addr) return;
69
+ void copyWithFeedback(url, addr);
70
+ opts.onCopy?.(addr);
71
+ });
72
+ return {
73
+ el: wrap,
74
+ apply,
75
+ status: () => status,
76
+ setVisible(on) {
77
+ wrap.hidden = !on;
78
+ },
79
+ toggle() {
80
+ toggle.click();
81
+ }
82
+ };
83
+ }
84
+
85
+ export { linkAddress, linkStatusClass, linkStatusTip, mountLinkStatus };
86
+ //# sourceMappingURL=chunk-KGAMP5LW.js.map
87
+ //# sourceMappingURL=chunk-KGAMP5LW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/link-status.ts"],"names":[],"mappings":"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","file":"chunk-KGAMP5LW.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The live-link status chip: a compact control in the bottom-right corner for\n// the loopback server a running game connects to. All the state collapses to\n// one coloured \"connect\" icon (grey off, amber listening, green connected and\n// in sync, red connected on a different build); click it to toggle the link\n// (off -> listening -> stop), and while the link is up the copiable ws://\n// address sits beside it. Patterpad's debug-panel.ts and Storyletter's\n// live-link.ts were this file twice, character for character down to the\n// 1000ms `.copied` revert. Styles ship as link-status.css.\n//\n// What the game sends is the app's business; this is the control and the\n// status only.\n// ---------------------------------------------------------------------------\n\nimport { el } from \"./dom.js\";\nimport { iconNode } from \"./icons.js\";\nimport { copyWithFeedback } from \"./util.js\";\n\nexport type LinkStatus =\n | { state: \"off\" }\n | { state: \"error\"; message: string }\n | { state: \"listening\"; port: number; address?: string }\n | {\n state: \"connected\"; port: number; address?: string;\n /** What connected, if it said. */\n project?: string;\n /** Whether the game's build matches the editor's. */\n build?: \"match\" | \"stale\" | \"unknown\";\n /** One extra sentence for the tip (Storyletter names the live boxes). */\n note?: string;\n };\n\n/** The chip's colour class for a state: the four. */\nexport type LinkStatusClass = \"off\" | \"listening\" | \"live\" | \"stale\";\n\nexport interface LinkStatusOptions {\n /** A word beside the chip (\"Live link\"), so a bottom-corner plug is not a\n * mystery. Omitted draws none. */\n label?: string;\n /** Start when off or failed, stop otherwise. A returned status is applied;\n * a status pushed later through `apply` works as well. */\n onToggle: (current: LinkStatus) => LinkStatus | void | Promise<LinkStatus | void>;\n /** Runs after the address is copied. The copy itself is the chip's. */\n onCopy?: (address: string) => void;\n /** Controls before the address (Patterpad's flow picker). */\n lead?: HTMLElement[];\n}\n\nexport interface LinkStatusChip {\n el: HTMLElement;\n apply(status: LinkStatus): void;\n status(): LinkStatus;\n /** Show / hide the control (shown when a project is open). */\n setVisible(on: boolean): void;\n /** Toggle the link (a menu item routes here). */\n toggle(): void;\n}\n\nexport function linkStatusClass(s: LinkStatus): LinkStatusClass {\n if (s.state === \"off\" || s.state === \"error\") return \"off\";\n if (s.state === \"listening\") return \"listening\";\n return s.build === \"stale\" ? \"stale\" : \"live\";\n}\n\n/** The tooltip, spelling the state out. One wording for the family. */\nexport function linkStatusTip(s: LinkStatus): string {\n switch (s.state) {\n case \"off\": return \"Live link is off. Click to start listening.\";\n case \"error\": return `Live link failed (${s.message}). Click to retry.`;\n case \"listening\": return \"Live link is listening for a game. Click to stop.\";\n case \"connected\": {\n const who = s.project !== undefined ? ` to ${s.project}` : \"\";\n const build = s.build === \"stale\" ? \" on a different build, so save or rebuild to re-sync\"\n : s.build === \"match\" ? \" and in sync\" : \"\";\n const note = s.note ? ` ${s.note}` : \"\";\n return `Live link is connected${who}${build}.${note} Click to stop.`;\n }\n }\n}\n\n/** The address a game connects to. */\nexport function linkAddress(s: LinkStatus): string | undefined {\n if (s.state !== \"listening\" && s.state !== \"connected\") return undefined;\n return s.address ?? `ws://127.0.0.1:${s.port}`;\n}\n\n/** Mount the chip into `host` (document.body in both apps: it is fixed to the\n * window's corner). Hidden until `setVisible(true)`. */\nexport function mountLinkStatus(host: HTMLElement, opts: LinkStatusOptions): LinkStatusChip {\n const wrap = el(\"div\", \"linkstatus\"); wrap.hidden = true;\n const url = el(\"button\", \"linkstatus-url\"); url.type = \"button\"; url.hidden = true;\n const toggle = el(\"button\", \"linkstatus-toggle off\"); toggle.type = \"button\"; toggle.append(iconNode(\"connect\"));\n if (opts.label !== undefined) wrap.append(el(\"span\", \"linkstatus-word\", opts.label));\n wrap.append(...(opts.lead ?? []), url, toggle);\n host.append(wrap);\n\n let status: LinkStatus = { state: \"off\" };\n\n const render = (): void => {\n toggle.className = `linkstatus-toggle ${linkStatusClass(status)}`;\n const tip = linkStatusTip(status);\n toggle.dataset[\"tip\"] = tip;\n toggle.setAttribute(\"aria-label\", tip);\n // The copiable address means something once the server is up.\n const addr = linkAddress(status);\n if (addr !== undefined) {\n url.textContent = addr;\n url.dataset[\"tip\"] = \"Click to copy the live link address\";\n url.hidden = false;\n } else url.hidden = true;\n };\n const apply = (s: LinkStatus): void => { status = s; render(); };\n render();\n\n toggle.addEventListener(\"click\", () => {\n const out = opts.onToggle(status);\n void Promise.resolve(out).then((next) => { if (next) apply(next); });\n });\n url.addEventListener(\"click\", () => {\n const addr = url.textContent ?? \"\";\n if (!addr) return;\n void copyWithFeedback(url, addr);\n opts.onCopy?.(addr);\n });\n\n return {\n el: wrap, apply,\n status: () => status,\n setVisible(on: boolean): void { wrap.hidden = !on; },\n toggle(): void { toggle.click(); },\n };\n}\n"]}