@wildwinter/app-shell 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES.md +62 -0
- package/README.md +8 -0
- package/dist/about.css +17 -38
- package/dist/about.css.map +1 -1
- package/dist/anchored.css +9 -9
- package/dist/anchored.css.map +1 -1
- package/dist/app-store.cjs +15 -0
- package/dist/app-store.cjs.map +1 -1
- package/dist/app-store.d.cts +25 -1
- package/dist/app-store.d.ts +25 -1
- package/dist/app-store.js +14 -1
- package/dist/app-store.js.map +1 -1
- package/dist/chunk-2ECJUTSX.js +97 -0
- package/dist/chunk-2ECJUTSX.js.map +1 -0
- package/dist/chunk-DS4XXAZS.js +56 -0
- package/dist/chunk-DS4XXAZS.js.map +1 -0
- package/dist/chunk-EHR2KBZC.js +82 -0
- package/dist/chunk-EHR2KBZC.js.map +1 -0
- package/dist/chunk-IU7EY6BI.js +26 -0
- package/dist/chunk-IU7EY6BI.js.map +1 -0
- package/dist/chunk-OPZPFOPS.js +66 -0
- package/dist/chunk-OPZPFOPS.js.map +1 -0
- package/dist/chunk-P4G4ERG6.js +60 -0
- package/dist/chunk-P4G4ERG6.js.map +1 -0
- package/dist/chunk-P6WLAEKZ.js +71 -0
- package/dist/chunk-P6WLAEKZ.js.map +1 -0
- package/dist/chunk-R3NXPHRZ.js +534 -0
- package/dist/chunk-R3NXPHRZ.js.map +1 -0
- package/dist/chunk-SDOKZTAR.js +87 -0
- package/dist/chunk-SDOKZTAR.js.map +1 -0
- package/dist/chunk-SEWJLUOD.js +70 -0
- package/dist/chunk-SEWJLUOD.js.map +1 -0
- package/dist/chunk-UG2P777F.js +587 -0
- package/dist/chunk-UG2P777F.js.map +1 -0
- package/dist/chunk-YRFJLJUH.js +55 -0
- package/dist/chunk-YRFJLJUH.js.map +1 -0
- package/dist/chunk-YWMSQFXX.js +146 -0
- package/dist/chunk-YWMSQFXX.js.map +1 -0
- package/dist/comments.css +16 -14
- package/dist/comments.css.map +1 -1
- package/dist/confirm.css +2 -55
- package/dist/confirm.css.map +1 -1
- package/dist/context-menu.cjs +250 -0
- package/dist/context-menu.cjs.map +1 -0
- package/dist/context-menu.css +64 -0
- package/dist/context-menu.css.map +1 -0
- package/dist/context-menu.d.cts +25 -0
- package/dist/context-menu.d.ts +25 -0
- package/dist/context-menu.js +5 -0
- package/dist/context-menu.js.map +1 -0
- package/dist/controls.css +214 -0
- package/dist/controls.css.map +1 -0
- package/dist/dialog.cjs +222 -0
- package/dist/dialog.cjs.map +1 -0
- package/dist/dialog.css +56 -0
- package/dist/dialog.css.map +1 -0
- package/dist/dialog.d.cts +28 -0
- package/dist/dialog.d.ts +28 -0
- package/dist/dialog.js +5 -0
- package/dist/dialog.js.map +1 -0
- package/dist/floating.cjs +83 -0
- package/dist/floating.cjs.map +1 -0
- package/dist/floating.d.cts +25 -0
- package/dist/floating.d.ts +25 -0
- package/dist/floating.js +4 -0
- package/dist/floating.js.map +1 -0
- package/dist/identity.css +15 -50
- package/dist/identity.css.map +1 -1
- package/dist/index.cjs +1553 -260
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +385 -152
- package/dist/index.d.ts +385 -152
- package/dist/index.js +264 -823
- package/dist/index.js.map +1 -1
- package/dist/job.css +24 -1
- package/dist/job.css.map +1 -1
- package/dist/keys.cjs +155 -0
- package/dist/keys.cjs.map +1 -0
- package/dist/keys.css +41 -0
- package/dist/keys.css.map +1 -0
- package/dist/keys.d.cts +57 -0
- package/dist/keys.d.ts +57 -0
- package/dist/keys.js +3 -0
- package/dist/keys.js.map +1 -0
- package/dist/link-status.cjs +430 -0
- package/dist/link-status.cjs.map +1 -0
- package/dist/link-status.css +76 -0
- package/dist/link-status.css.map +1 -0
- package/dist/link-status.d.cts +53 -0
- package/dist/link-status.d.ts +53 -0
- package/dist/link-status.js +5 -0
- package/dist/link-status.js.map +1 -0
- package/dist/menu.cjs +62 -1
- package/dist/menu.cjs.map +1 -1
- package/dist/menu.d.cts +88 -1
- package/dist/menu.d.ts +88 -1
- package/dist/menu.js +1 -1
- package/dist/notes-editor.css +9 -52
- package/dist/notes-editor.css.map +1 -1
- package/dist/pane-shell.css +23 -5
- package/dist/pane-shell.css.map +1 -1
- package/dist/problems.cjs +61 -0
- package/dist/problems.cjs.map +1 -0
- package/dist/problems.d.cts +57 -0
- package/dist/problems.d.ts +57 -0
- package/dist/problems.js +3 -0
- package/dist/problems.js.map +1 -0
- package/dist/property-list-D3VZCJzi.d.cts +162 -0
- package/dist/property-list-D3VZCJzi.d.ts +162 -0
- package/dist/property-list.cjs +784 -0
- package/dist/property-list.cjs.map +1 -0
- package/dist/property-list.d.cts +1 -0
- package/dist/property-list.d.ts +1 -0
- package/dist/property-list.js +4 -0
- package/dist/property-list.js.map +1 -0
- package/dist/save.css +1 -1
- package/dist/save.css.map +1 -1
- package/dist/settings.css +62 -18
- package/dist/settings.css.map +1 -1
- package/dist/stale.css +4 -1
- package/dist/stale.css.map +1 -1
- package/dist/stepper.css +47 -3
- package/dist/stepper.css.map +1 -1
- package/dist/toast.css +5 -5
- package/dist/toast.css.map +1 -1
- package/dist/tokens.css +20 -0
- package/dist/tokens.css.map +1 -1
- package/dist/tool-window.cjs +63 -0
- package/dist/tool-window.cjs.map +1 -1
- package/dist/tool-window.css +8 -4
- package/dist/tool-window.css.map +1 -1
- package/dist/tool-window.d.cts +67 -1
- package/dist/tool-window.d.ts +67 -1
- package/dist/tool-window.js +63 -1
- package/dist/tool-window.js.map +1 -1
- package/dist/tooltip.css +3 -3
- package/dist/tooltip.css.map +1 -1
- package/dist/updater-view.cjs +283 -0
- package/dist/updater-view.cjs.map +1 -0
- package/dist/updater-view.d.cts +23 -0
- package/dist/updater-view.d.ts +23 -0
- package/dist/updater-view.js +6 -0
- package/dist/updater-view.js.map +1 -0
- package/dist/updater.cjs +12 -16
- package/dist/updater.cjs.map +1 -1
- package/dist/updater.css +44 -0
- package/dist/updater.css.map +1 -0
- package/dist/updater.js +12 -16
- package/dist/updater.js.map +1 -1
- package/dist/util.cjs +78 -0
- package/dist/util.cjs.map +1 -0
- package/dist/util.d.cts +37 -0
- package/dist/util.d.ts +37 -0
- package/dist/util.js +3 -0
- package/dist/util.js.map +1 -0
- package/dist/vc.css +4 -3
- package/dist/vc.css.map +1 -1
- package/dist/welcome.cjs +215 -0
- package/dist/welcome.cjs.map +1 -0
- package/dist/welcome.css +112 -0
- package/dist/welcome.css.map +1 -0
- package/dist/welcome.d.cts +55 -0
- package/dist/welcome.d.ts +55 -0
- package/dist/welcome.js +4 -0
- package/dist/welcome.js.map +1 -0
- package/package.json +72 -2
- package/dist/chunk-QDPFU4WX.js +0 -43
- package/dist/chunk-QDPFU4WX.js.map +0 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// src/menu.ts
|
|
2
|
+
var EDIT_MENU = {
|
|
3
|
+
undo: { label: "Undo", accelerator: "CmdOrCtrl+Z" },
|
|
4
|
+
redo: { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z" },
|
|
5
|
+
duplicate: { label: "Duplicate", accelerator: "CmdOrCtrl+D" },
|
|
6
|
+
find: { label: "Find\u2026", accelerator: "CmdOrCtrl+F" },
|
|
7
|
+
replace: { label: "Replace\u2026", acceleratorMac: "Cmd+Alt+F", acceleratorOther: "Ctrl+H" }
|
|
8
|
+
};
|
|
9
|
+
var GO_MENU = {
|
|
10
|
+
back: { label: "Back", acceleratorMac: "Ctrl+Cmd+Left", acceleratorOther: "Alt+Left" },
|
|
11
|
+
forward: { label: "Forward", acceleratorMac: "Ctrl+Cmd+Right", acceleratorOther: "Alt+Right" }
|
|
12
|
+
};
|
|
13
|
+
var PANE_MENU = {
|
|
14
|
+
showNav: { label: "Show Navigator", accelerator: "CmdOrCtrl+1" },
|
|
15
|
+
showInspector: { label: "Show Inspector", accelerator: "CmdOrCtrl+2" },
|
|
16
|
+
resetView: { label: "Reset View" }
|
|
17
|
+
};
|
|
18
|
+
var HELP_MENU = {
|
|
19
|
+
checkForUpdates: { label: "Check for Updates\u2026" }
|
|
20
|
+
};
|
|
21
|
+
var APP_MENU = {
|
|
22
|
+
/** Name + optional email, used to sign edits and comments. One label across the suite. */
|
|
23
|
+
userInfo: { label: "User Information\u2026" }
|
|
24
|
+
};
|
|
25
|
+
function namedMenuItems(naming) {
|
|
26
|
+
const suite = naming.suiteName ?? "Patter";
|
|
27
|
+
return {
|
|
28
|
+
about: { label: `About ${naming.appName}`, ready: true },
|
|
29
|
+
docs: {
|
|
30
|
+
label: `${naming.appName} Documentation`,
|
|
31
|
+
...naming.docsUrl ? { url: naming.docsUrl } : {},
|
|
32
|
+
ready: naming.docsUrl !== void 0
|
|
33
|
+
},
|
|
34
|
+
suiteDocs: {
|
|
35
|
+
label: `${suite} Documentation Home`,
|
|
36
|
+
...naming.suiteDocsUrl ? { url: naming.suiteDocsUrl } : {},
|
|
37
|
+
ready: naming.suiteDocsUrl !== void 0
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
var FILE_MENU = {
|
|
42
|
+
newProject: { label: "New Project\u2026", accelerator: "CmdOrCtrl+N" },
|
|
43
|
+
openProject: { label: "Open Project\u2026", accelerator: "CmdOrCtrl+O" },
|
|
44
|
+
openRecent: { label: "Open Recent" },
|
|
45
|
+
noRecents: { label: "No Recent Projects" },
|
|
46
|
+
clearRecents: { label: "Clear Recents" },
|
|
47
|
+
closeProject: { label: "Close Project" },
|
|
48
|
+
save: { label: "Save", accelerator: "CmdOrCtrl+S" },
|
|
49
|
+
saveAs: { label: "Save As\u2026", accelerator: "Shift+CmdOrCtrl+S" },
|
|
50
|
+
projectSettings: { label: "Project Settings\u2026", accelerator: "CmdOrCtrl+," }
|
|
51
|
+
};
|
|
52
|
+
var PLAY_MENU = {
|
|
53
|
+
liveLink: { label: "Live Link" }
|
|
54
|
+
};
|
|
55
|
+
var REVIEW_MENU = {
|
|
56
|
+
reviewFeedback: { label: "Review Feedback", accelerator: "CmdOrCtrl+Shift+R" },
|
|
57
|
+
nextFeedback: { label: "Next Feedback", accelerator: "F8" },
|
|
58
|
+
previousFeedback: { label: "Previous Feedback", accelerator: "Shift+F8" },
|
|
59
|
+
coverageTest: { label: "Coverage Test\u2026", accelerator: "Shift+CmdOrCtrl+C" },
|
|
60
|
+
findPropertyUsage: { label: "Find Property Usage\u2026" },
|
|
61
|
+
showResolvedComments: { label: "Show Resolved Comments" }
|
|
62
|
+
};
|
|
63
|
+
var PUBLISH_MENU = {
|
|
64
|
+
playableHtml: { label: "Publish Playable HTML\u2026" },
|
|
65
|
+
bundle: { label: "Publish Bundle", accelerator: "Shift+CmdOrCtrl+B" },
|
|
66
|
+
autoRebuild: { label: "Auto Rebuild" }
|
|
67
|
+
};
|
|
68
|
+
var VIEW_MENU = {
|
|
69
|
+
projectOverview: { label: "Project Overview" },
|
|
70
|
+
upALevel: { label: "Up a Level", accelerator: "CmdOrCtrl+[" },
|
|
71
|
+
colourTheme: { label: "Colour Theme" }
|
|
72
|
+
};
|
|
73
|
+
function tildePath(path, home) {
|
|
74
|
+
if (path === home) return "~";
|
|
75
|
+
const sep = home.includes("\\") ? "\\" : "/";
|
|
76
|
+
return path.startsWith(home + sep) ? `~${path.slice(home.length)}` : path;
|
|
77
|
+
}
|
|
78
|
+
function recentsSubmenu(recents, opts) {
|
|
79
|
+
const isMac = (opts.platform ?? process.platform) === "darwin";
|
|
80
|
+
if (recents.length === 0) return [{ label: FILE_MENU.noRecents.label, enabled: false }];
|
|
81
|
+
const items = recents.map((r) => {
|
|
82
|
+
const shown = tildePath(r.path, opts.home);
|
|
83
|
+
const name = r.name ?? r.path.split(/[\\/]/).filter(Boolean).pop() ?? r.path;
|
|
84
|
+
return {
|
|
85
|
+
label: isMac ? name : `${name} (${shown})`,
|
|
86
|
+
sublabel: shown,
|
|
87
|
+
toolTip: r.path,
|
|
88
|
+
click: () => opts.onOpen(r.path)
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
if (opts.onClear) items.push({ type: "separator" }, { label: FILE_MENU.clearRecents.label, click: opts.onClear });
|
|
92
|
+
return items;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export { APP_MENU, EDIT_MENU, FILE_MENU, GO_MENU, HELP_MENU, PANE_MENU, PLAY_MENU, PUBLISH_MENU, REVIEW_MENU, VIEW_MENU, namedMenuItems, recentsSubmenu, tildePath };
|
|
96
|
+
//# sourceMappingURL=chunk-2ECJUTSX.js.map
|
|
97
|
+
//# sourceMappingURL=chunk-2ECJUTSX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/menu.ts"],"names":[],"mappings":";AA6BO,IAAM,SAAA,GAOT;AAAA,EACF,IAAA,EAAM,EAAE,KAAA,EAAO,MAAA,EAAQ,aAAa,aAAA,EAAc;AAAA,EAClD,IAAA,EAAM,EAAE,KAAA,EAAO,MAAA,EAAQ,aAAa,mBAAA,EAAoB;AAAA,EACxD,SAAA,EAAW,EAAE,KAAA,EAAO,WAAA,EAAa,aAAa,aAAA,EAAc;AAAA,EAC5D,IAAA,EAAM,EAAE,KAAA,EAAO,YAAA,EAAS,aAAa,aAAA,EAAc;AAAA,EACnD,SAAS,EAAE,KAAA,EAAO,iBAAY,cAAA,EAAgB,WAAA,EAAa,kBAAkB,QAAA;AAC/E;AASO,IAAM,OAAA,GAGT;AAAA,EACF,MAAM,EAAE,KAAA,EAAO,QAAQ,cAAA,EAAgB,eAAA,EAAiB,kBAAkB,UAAA,EAAW;AAAA,EACrF,SAAS,EAAE,KAAA,EAAO,WAAW,cAAA,EAAgB,gBAAA,EAAkB,kBAAkB,WAAA;AACnF;AAGO,IAAM,SAAA,GAIT;AAAA,EACF,OAAA,EAAS,EAAE,KAAA,EAAO,gBAAA,EAAkB,aAAa,aAAA,EAAc;AAAA,EAC/D,aAAA,EAAe,EAAE,KAAA,EAAO,gBAAA,EAAkB,aAAa,aAAA,EAAc;AAAA,EACrE,SAAA,EAAW,EAAE,KAAA,EAAO,YAAA;AACtB;AAkBO,IAAM,SAAA,GAET;AAAA,EACF,eAAA,EAAiB,EAAE,KAAA,EAAO,yBAAA;AAC5B;AAGO,IAAM,QAAA,GAET;AAAA;AAAA,EAEF,QAAA,EAAU,EAAE,KAAA,EAAO,wBAAA;AACrB;AA+BO,SAAS,eAAe,MAAA,EAI7B;AACA,EAAA,MAAM,KAAA,GAAQ,OAAO,SAAA,IAAa,QAAA;AAClC,EAAA,OAAO;AAAA,IACL,KAAA,EAAO,EAAE,KAAA,EAAO,CAAA,MAAA,EAAS,OAAO,OAAO,CAAA,CAAA,EAAI,OAAO,IAAA,EAAK;AAAA,IACvD,IAAA,EAAM;AAAA,MACJ,KAAA,EAAO,CAAA,EAAG,MAAA,CAAO,OAAO,CAAA,cAAA,CAAA;AAAA,MACxB,GAAI,OAAO,OAAA,GAAU,EAAE,KAAK,MAAA,CAAO,OAAA,KAAY,EAAC;AAAA,MAChD,KAAA,EAAO,OAAO,OAAA,KAAY;AAAA,KAC5B;AAAA,IACA,SAAA,EAAW;AAAA,MACT,KAAA,EAAO,GAAG,KAAK,CAAA,mBAAA,CAAA;AAAA,MACf,GAAI,OAAO,YAAA,GAAe,EAAE,KAAK,MAAA,CAAO,YAAA,KAAiB,EAAC;AAAA,MAC1D,KAAA,EAAO,OAAO,YAAA,KAAiB;AAAA;AACjC,GACF;AACF;AAUO,IAAM,SAAA,GAWT;AAAA,EACF,UAAA,EAAY,EAAE,KAAA,EAAO,mBAAA,EAAgB,aAAa,aAAA,EAAc;AAAA,EAChE,WAAA,EAAa,EAAE,KAAA,EAAO,oBAAA,EAAiB,aAAa,aAAA,EAAc;AAAA,EAClE,UAAA,EAAY,EAAE,KAAA,EAAO,aAAA,EAAc;AAAA,EACnC,SAAA,EAAW,EAAE,KAAA,EAAO,oBAAA,EAAqB;AAAA,EACzC,YAAA,EAAc,EAAE,KAAA,EAAO,eAAA,EAAgB;AAAA,EACvC,YAAA,EAAc,EAAE,KAAA,EAAO,eAAA,EAAgB;AAAA,EACvC,IAAA,EAAM,EAAE,KAAA,EAAO,MAAA,EAAQ,aAAa,aAAA,EAAc;AAAA,EAClD,MAAA,EAAQ,EAAE,KAAA,EAAO,eAAA,EAAY,aAAa,mBAAA,EAAoB;AAAA,EAC9D,eAAA,EAAiB,EAAE,KAAA,EAAO,wBAAA,EAAqB,aAAa,aAAA;AAC9D;AAQO,IAAM,SAAA,GAET;AAAA,EACF,QAAA,EAAU,EAAE,KAAA,EAAO,WAAA;AACrB;AAIO,IAAM,WAAA,GAOT;AAAA,EACF,cAAA,EAAgB,EAAE,KAAA,EAAO,iBAAA,EAAmB,aAAa,mBAAA,EAAoB;AAAA,EAC7E,YAAA,EAAc,EAAE,KAAA,EAAO,eAAA,EAAiB,aAAa,IAAA,EAAK;AAAA,EAC1D,gBAAA,EAAkB,EAAE,KAAA,EAAO,mBAAA,EAAqB,aAAa,UAAA,EAAW;AAAA,EACxE,YAAA,EAAc,EAAE,KAAA,EAAO,qBAAA,EAAkB,aAAa,mBAAA,EAAoB;AAAA,EAC1E,iBAAA,EAAmB,EAAE,KAAA,EAAO,2BAAA,EAAuB;AAAA,EACnD,oBAAA,EAAsB,EAAE,KAAA,EAAO,wBAAA;AACjC;AAIO,IAAM,YAAA,GAIT;AAAA,EACF,YAAA,EAAc,EAAE,KAAA,EAAO,6BAAA,EAAyB;AAAA,EAChD,MAAA,EAAQ,EAAE,KAAA,EAAO,gBAAA,EAAkB,aAAa,mBAAA,EAAoB;AAAA,EACpE,WAAA,EAAa,EAAE,KAAA,EAAO,cAAA;AACxB;AAKO,IAAM,SAAA,GAIT;AAAA,EACF,eAAA,EAAiB,EAAE,KAAA,EAAO,kBAAA,EAAmB;AAAA,EAC7C,QAAA,EAAU,EAAE,KAAA,EAAO,YAAA,EAAc,aAAa,aAAA,EAAc;AAAA,EAC5D,WAAA,EAAa,EAAE,KAAA,EAAO,cAAA;AACxB;AAKO,SAAS,SAAA,CAAU,MAAc,IAAA,EAAsB;AAC5D,EAAA,IAAI,IAAA,KAAS,MAAM,OAAO,GAAA;AAC1B,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,QAAA,CAAS,IAAI,IAAI,IAAA,GAAO,GAAA;AACzC,EAAA,OAAO,IAAA,CAAK,UAAA,CAAW,IAAA,GAAO,GAAG,CAAA,GAAI,CAAA,CAAA,EAAI,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,MAAM,CAAC,CAAA,CAAA,GAAK,IAAA;AACvE;AA+BO,SAAS,cAAA,CAAe,SAA0B,IAAA,EAAgD;AACvG,EAAA,MAAM,KAAA,GAAA,CAAS,IAAA,CAAK,QAAA,IAAY,OAAA,CAAQ,QAAA,MAAc,QAAA;AACtD,EAAA,IAAI,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG,OAAO,CAAC,EAAE,KAAA,EAAO,SAAA,CAAU,SAAA,CAAU,KAAA,EAAO,OAAA,EAAS,KAAA,EAAO,CAAA;AACtF,EAAA,MAAM,KAAA,GAA2B,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,KAAM;AAClD,IAAA,MAAM,KAAA,GAAQ,SAAA,CAAU,CAAA,CAAE,IAAA,EAAM,KAAK,IAAI,CAAA;AACzC,IAAA,MAAM,IAAA,GAAO,CAAA,CAAE,IAAA,IAAQ,CAAA,CAAE,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA,CAAE,MAAA,CAAO,OAAO,CAAA,CAAE,GAAA,MAAS,CAAA,CAAE,IAAA;AACxE,IAAA,OAAO;AAAA,MACL,OAAO,KAAA,GAAQ,IAAA,GAAO,CAAA,EAAG,IAAI,MAAM,KAAK,CAAA,CAAA,CAAA;AAAA,MACxC,QAAA,EAAU,KAAA;AAAA,MACV,SAAS,CAAA,CAAE,IAAA;AAAA,MACX,KAAA,EAAO,MAAM,IAAA,CAAK,MAAA,CAAO,EAAE,IAAI;AAAA,KACjC;AAAA,EACF,CAAC,CAAA;AACD,EAAA,IAAI,KAAK,OAAA,EAAS,KAAA,CAAM,IAAA,CAAK,EAAE,MAAM,WAAA,EAAY,EAAG,EAAE,KAAA,EAAO,UAAU,YAAA,CAAa,KAAA,EAAO,KAAA,EAAO,IAAA,CAAK,SAAS,CAAA;AAChH,EAAA,OAAO,KAAA;AACT","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,56 @@
|
|
|
1
|
+
// src/problems.ts
|
|
2
|
+
function problemName(p, dangling = false) {
|
|
3
|
+
if (p.title) return `\u201C${p.title}\u201D`;
|
|
4
|
+
if (dangling && p.where) return `(id ${p.where})`;
|
|
5
|
+
return "This";
|
|
6
|
+
}
|
|
7
|
+
function describeProblem(problem, table = {}) {
|
|
8
|
+
const entry = problem.code ? table[problem.code] : void 0;
|
|
9
|
+
if (entry) {
|
|
10
|
+
const copy = entry(problem);
|
|
11
|
+
const text = copy.dangling && !problem.title && problem.where && !copy.text.includes(`(id ${problem.where})`) ? `${copy.text} (id ${problem.where})` : copy.text;
|
|
12
|
+
return copy.next ? { text, next: copy.next } : { text };
|
|
13
|
+
}
|
|
14
|
+
const message = problem.message.replace(/\s*\[[^\]]*\]\s*$/, "").trim();
|
|
15
|
+
return { text: problem.title ? `${problem.title}: ${message}` : message };
|
|
16
|
+
}
|
|
17
|
+
function problemLine(problem, table) {
|
|
18
|
+
const { text, next } = describeProblem(problem, table);
|
|
19
|
+
return next ? `${text} ${next}` : text;
|
|
20
|
+
}
|
|
21
|
+
var defaultProblemCopy = {
|
|
22
|
+
"duplicate-gameid": (p) => ({
|
|
23
|
+
text: `The Game ID on ${problemName(p)} is already used elsewhere.`,
|
|
24
|
+
next: "Each one must be unique; change one of them."
|
|
25
|
+
}),
|
|
26
|
+
"invalid-gameid": (p) => ({
|
|
27
|
+
text: `The Game ID on ${problemName(p)} isn't valid.`,
|
|
28
|
+
next: "Use lowercase letters, digits and hyphens."
|
|
29
|
+
}),
|
|
30
|
+
"dangling-reference": (p) => ({
|
|
31
|
+
text: `${problemName(p, true)} points at something that no longer exists.`,
|
|
32
|
+
next: "Choose where it goes.",
|
|
33
|
+
dangling: true
|
|
34
|
+
}),
|
|
35
|
+
"unknown-property": (p) => ({
|
|
36
|
+
text: `${problemName(p)} uses a property that isn't set up yet.`,
|
|
37
|
+
next: "Declare it in the project settings, or fix the name."
|
|
38
|
+
}),
|
|
39
|
+
"missing-name": (p) => ({ text: `${problemName(p)} needs a name.` }),
|
|
40
|
+
"empty": (p) => ({
|
|
41
|
+
text: `${problemName(p)} is empty.`,
|
|
42
|
+
next: "Add something inside it, or remove it."
|
|
43
|
+
}),
|
|
44
|
+
"stale-build": (p) => ({
|
|
45
|
+
text: `Your playable build${p.path ? ` (${p.path})` : ""} is out of date.`,
|
|
46
|
+
next: "It refreshes the next time you publish."
|
|
47
|
+
}),
|
|
48
|
+
"merge-conflict": (p) => ({
|
|
49
|
+
text: `${p.path ?? "This file"} still has an unresolved merge conflict in it.`,
|
|
50
|
+
next: "Resolve it in your version control tool, then reopen the project."
|
|
51
|
+
})
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export { defaultProblemCopy, describeProblem, problemLine, problemName };
|
|
55
|
+
//# sourceMappingURL=chunk-DS4XXAZS.js.map
|
|
56
|
+
//# sourceMappingURL=chunk-DS4XXAZS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/problems.ts"],"names":[],"mappings":"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","file":"chunk-DS4XXAZS.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Problem copy: a validator's diagnostic rewritten for the person at the\n// keyboard, through a table the app fills.\n//\n// Patterpad's `humanizeProblem` (its renderer.ts) proved the shape: a dozen\n// validator categories each mapped to a plain sentence with a next step, naming\n// things by their title, and a fallback that softens the technical tells.\n// Storyletter showed the raw compiler string and labelled it `path [where]`\n// (ui-review-2026-09, finding 17), the bracketed id that \"Diagnostics name what\n// the author can see\" (design-language.md) rules out for anything but a\n// DANGLING reference. This module is the translator; the tables are the\n// apps', because the codes are theirs. `defaultProblemCopy` covers the handful\n// of codes both validators raise in some form, as a floor an app spreads into\n// its own table.\n// ---------------------------------------------------------------------------\n\n/** What a translator needs to know about a problem. `code` is the app's own\n * category (Patterpad's `detail`, or a code Storyletter derives from its\n * compiler message); `title` is the author-facing name of the thing the\n * problem is about, when the app can resolve one; `where` is the raw\n * reference (an id) for when it cannot. */\nexport interface ProblemLike {\n code?: string;\n message: string;\n path?: string;\n where?: string;\n title?: string;\n}\n\nexport interface ProblemCopy {\n /** The sentence: what is wrong, in the author's words. */\n text: string;\n /** What to do about it, when there is one thing to do. */\n next?: string;\n /** The reference this problem is about no longer has a name to give, so the\n * raw id may be shown, written `(id ...)` so it cannot be mistaken for a\n * name. Set by the table entry; the translator never guesses it. */\n dangling?: boolean;\n}\n\nexport type ProblemCopyEntry = (problem: ProblemLike) => ProblemCopy;\nexport type ProblemCopyTable = Record<string, ProblemCopyEntry>;\n\n/** The name to call the thing a problem is about: its title in curly quotes,\n * \"(id ...)\" only when the reference dangles and there is nothing else,\n * and \"This\" otherwise, because the bar's \"go to\" shows the reader what\n * \"this\" is and an id is not something they can look for. */\nexport function problemName(p: ProblemLike, dangling = false): string {\n if (p.title) return `“${p.title}”`;\n if (dangling && p.where) return `(id ${p.where})`;\n return \"This\";\n}\n\n/**\n * Translate `problem` through `table`. A matching entry decides the copy; with\n * no entry the raw message is kept and prefixed by the title when there is one\n * (\"Burner Rig: duplicate id\"). Nothing here ever writes `[where]`; an id\n * appears only as `(id ...)`, only when the entry says the reference dangles,\n * and only when there is no title to say instead.\n */\nexport function describeProblem(problem: ProblemLike, table: ProblemCopyTable = {}): ProblemCopy {\n const entry = problem.code ? table[problem.code] : undefined;\n if (entry) {\n const copy = entry(problem);\n const text = copy.dangling && !problem.title && problem.where && !copy.text.includes(`(id ${problem.where})`)\n ? `${copy.text} (id ${problem.where})`\n : copy.text;\n return copy.next ? { text, next: copy.next } : { text };\n }\n const message = problem.message.replace(/\\s*\\[[^\\]]*\\]\\s*$/, \"\").trim();\n return { text: problem.title ? `${problem.title}: ${message}` : message };\n}\n\n/** `describeProblem` as one line for a bar that has room for one: the sentence,\n * then the next step after a space. */\nexport function problemLine(problem: ProblemLike, table?: ProblemCopyTable): string {\n const { text, next } = describeProblem(problem, table);\n return next ? `${text} ${next}` : text;\n}\n\n/**\n * The codes both validators raise in some form, in the family's voice. An app\n * spreads this into its own table and adds its own codes over it; a code an\n * app does not raise costs nothing.\n *\n * duplicate-gameid two things share one Game ID\n * invalid-gameid a Game ID that breaks the address rule\n * dangling-reference a jump / tag / property points at something gone\n * unknown-property a condition or effect names an undeclared property\n * missing-name a thing that needs a name has none\n * empty a container with nothing in it\n * stale-build the playable build is older than the source\n * merge-conflict a file still carries conflict markers\n */\nexport const defaultProblemCopy: ProblemCopyTable = {\n \"duplicate-gameid\": (p) => ({\n text: `The Game ID on ${problemName(p)} is already used elsewhere.`,\n next: \"Each one must be unique; change one of them.\",\n }),\n \"invalid-gameid\": (p) => ({\n text: `The Game ID on ${problemName(p)} isn't valid.`,\n next: \"Use lowercase letters, digits and hyphens.\",\n }),\n \"dangling-reference\": (p) => ({\n text: `${problemName(p, true)} points at something that no longer exists.`,\n next: \"Choose where it goes.\",\n dangling: true,\n }),\n \"unknown-property\": (p) => ({\n text: `${problemName(p)} uses a property that isn't set up yet.`,\n next: \"Declare it in the project settings, or fix the name.\",\n }),\n \"missing-name\": (p) => ({ text: `${problemName(p)} needs a name.` }),\n \"empty\": (p) => ({\n text: `${problemName(p)} is empty.`,\n next: \"Add something inside it, or remove it.\",\n }),\n \"stale-build\": (p) => ({\n text: `Your playable build${p.path ? ` (${p.path})` : \"\"} is out of date.`,\n next: \"It refreshes the next time you publish.\",\n }),\n \"merge-conflict\": (p) => ({\n text: `${p.path ?? \"This file\"} still has an unresolved merge conflict in it.`,\n next: \"Resolve it in your version control tool, then reopen the project.\",\n }),\n};\n"]}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { el } from './chunk-UG2P777F.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-EHR2KBZC.js.map
|
|
82
|
+
//# sourceMappingURL=chunk-EHR2KBZC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context-menu.ts"],"names":[],"mappings":"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","file":"chunk-EHR2KBZC.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,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,66 @@
|
|
|
1
|
+
import { dialogFrame } from './chunk-YRFJLJUH.js';
|
|
2
|
+
import { el } from './chunk-UG2P777F.js';
|
|
3
|
+
|
|
4
|
+
// src/updater-view.ts
|
|
5
|
+
var liveProgress = null;
|
|
6
|
+
var mb = (n) => (n / (1024 * 1024)).toFixed(1);
|
|
7
|
+
function feedUpdaterDownloadProgress(p) {
|
|
8
|
+
if (!liveProgress) return;
|
|
9
|
+
const pct = Math.max(0, Math.min(100, p.percent));
|
|
10
|
+
liveProgress.bar.style.width = `${pct}%`;
|
|
11
|
+
liveProgress.label.textContent = `${pct.toFixed(0)}%, ${mb(p.transferred)} of ${mb(p.total)} MB (${mb(p.bytesPerSecond)} MB/s)`;
|
|
12
|
+
}
|
|
13
|
+
function showUpdaterDialog(opts) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const defaultId = opts.defaultId ?? 0;
|
|
16
|
+
const cancelId = opts.cancelId ?? opts.buttons.length - 1;
|
|
17
|
+
let done = false;
|
|
18
|
+
const finish = (idx) => {
|
|
19
|
+
if (done) return;
|
|
20
|
+
done = true;
|
|
21
|
+
liveProgress = null;
|
|
22
|
+
resolve(idx);
|
|
23
|
+
frame.close();
|
|
24
|
+
};
|
|
25
|
+
const frame = dialogFrame({
|
|
26
|
+
title: opts.message,
|
|
27
|
+
className: "updater-dialog",
|
|
28
|
+
// Esc is the cancel button, not a further answer: main is waiting on an index.
|
|
29
|
+
onClose: () => finish(cancelId)
|
|
30
|
+
});
|
|
31
|
+
if (opts.detail) frame.body.append(el("p", "updater-detail", opts.detail));
|
|
32
|
+
if (opts.progress) {
|
|
33
|
+
const track = el("div", "updater-progress-track");
|
|
34
|
+
const bar = el("div", "updater-progress-bar");
|
|
35
|
+
track.append(bar);
|
|
36
|
+
const label = el("p", "updater-progress-label", "Starting download\u2026");
|
|
37
|
+
frame.body.append(track, label);
|
|
38
|
+
liveProgress = { bar, label };
|
|
39
|
+
}
|
|
40
|
+
if (opts.links?.length) {
|
|
41
|
+
const row = el("p", "updater-links");
|
|
42
|
+
for (const link of opts.links) {
|
|
43
|
+
const a = el("a", void 0, link.label);
|
|
44
|
+
a.href = link.url;
|
|
45
|
+
a.addEventListener("click", (e) => {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
opts.openExternal?.(link.url);
|
|
48
|
+
});
|
|
49
|
+
row.append(a);
|
|
50
|
+
}
|
|
51
|
+
frame.body.append(row);
|
|
52
|
+
}
|
|
53
|
+
opts.buttons.forEach((label, i) => {
|
|
54
|
+
const b = el("button", i === defaultId ? "btn primary" : "btn", label);
|
|
55
|
+
b.type = "button";
|
|
56
|
+
b.addEventListener("click", () => finish(i));
|
|
57
|
+
frame.actions.append(b);
|
|
58
|
+
if (i === defaultId) queueMicrotask(() => b.focus());
|
|
59
|
+
});
|
|
60
|
+
frame.open();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { feedUpdaterDownloadProgress, showUpdaterDialog };
|
|
65
|
+
//# sourceMappingURL=chunk-OPZPFOPS.js.map
|
|
66
|
+
//# sourceMappingURL=chunk-OPZPFOPS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/updater-view.ts"],"names":[],"mappings":"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","file":"chunk-OPZPFOPS.js","sourcesContent":["// ---------------------------------------------------------------------------\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"]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { closeWithExit } from './chunk-IU7EY6BI.js';
|
|
2
|
+
|
|
3
|
+
// src/floating.ts
|
|
4
|
+
function followOnScroll(reposition) {
|
|
5
|
+
const onMove = () => reposition();
|
|
6
|
+
window.addEventListener("scroll", onMove, true);
|
|
7
|
+
window.addEventListener("resize", onMove);
|
|
8
|
+
return () => {
|
|
9
|
+
window.removeEventListener("scroll", onMove, true);
|
|
10
|
+
window.removeEventListener("resize", onMove);
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function createFloating(className, opts = {}) {
|
|
14
|
+
const el = document.createElement("div");
|
|
15
|
+
el.className = className;
|
|
16
|
+
el.style.display = "none";
|
|
17
|
+
(opts.host ?? document.body).appendChild(el);
|
|
18
|
+
let open = false;
|
|
19
|
+
let detach = null;
|
|
20
|
+
let outside = null;
|
|
21
|
+
let closeToken = 0;
|
|
22
|
+
const close = () => {
|
|
23
|
+
if (!open) return;
|
|
24
|
+
open = false;
|
|
25
|
+
detach?.();
|
|
26
|
+
detach = null;
|
|
27
|
+
if (outside) {
|
|
28
|
+
document.removeEventListener("mousedown", outside, true);
|
|
29
|
+
outside = null;
|
|
30
|
+
}
|
|
31
|
+
const myToken = ++closeToken;
|
|
32
|
+
closeWithExit(el, () => {
|
|
33
|
+
if (myToken !== closeToken) return;
|
|
34
|
+
el.style.display = "none";
|
|
35
|
+
el.replaceChildren();
|
|
36
|
+
el.classList.remove("closing");
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
const show = (reposition) => {
|
|
40
|
+
closeToken++;
|
|
41
|
+
el.classList.remove("closing");
|
|
42
|
+
open = true;
|
|
43
|
+
el.style.display = "block";
|
|
44
|
+
reposition();
|
|
45
|
+
detach ??= followOnScroll(reposition);
|
|
46
|
+
};
|
|
47
|
+
const dismissOnOutside = (onOutside, isInside) => {
|
|
48
|
+
if (outside) return;
|
|
49
|
+
outside = (e) => {
|
|
50
|
+
const t = e.target;
|
|
51
|
+
if (!el.contains(t) && !(isInside?.(t) ?? false)) onOutside();
|
|
52
|
+
};
|
|
53
|
+
document.addEventListener("mousedown", outside, true);
|
|
54
|
+
};
|
|
55
|
+
return { el, show, dismissOnOutside, close, isOpen: () => open };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { createFloating };
|
|
59
|
+
//# sourceMappingURL=chunk-P4G4ERG6.js.map
|
|
60
|
+
//# sourceMappingURL=chunk-P4G4ERG6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/floating.ts"],"names":[],"mappings":";;;AAqCA,SAAS,eAAe,UAAA,EAAoC;AAC1D,EAAA,MAAM,MAAA,GAAS,MAAY,UAAA,EAAW;AACtC,EAAA,MAAA,CAAO,gBAAA,CAAiB,QAAA,EAAU,MAAA,EAAQ,IAAI,CAAA;AAC9C,EAAA,MAAA,CAAO,gBAAA,CAAiB,UAAU,MAAM,CAAA;AACxC,EAAA,OAAO,MAAM;AAAE,IAAA,MAAA,CAAO,mBAAA,CAAoB,QAAA,EAAU,MAAA,EAAQ,IAAI,CAAA;AAAG,IAAA,MAAA,CAAO,mBAAA,CAAoB,UAAU,MAAM,CAAA;AAAA,EAAG,CAAA;AACnH;AASO,SAAS,cAAA,CAAe,SAAA,EAAmB,IAAA,GAAwB,EAAC,EAAa;AACtF,EAAA,MAAM,EAAA,GAAK,QAAA,CAAS,aAAA,CAAc,KAAK,CAAA;AAAG,EAAA,EAAA,CAAG,SAAA,GAAY,SAAA;AAAW,EAAA,EAAA,CAAG,MAAM,OAAA,GAAU,MAAA;AACvF,EAAA,CAAC,IAAA,CAAK,IAAA,IAAQ,QAAA,CAAS,IAAA,EAAM,YAAY,EAAE,CAAA;AAC3C,EAAA,IAAI,IAAA,GAAO,KAAA;AACX,EAAA,IAAI,MAAA,GAA8B,IAAA;AAClC,EAAA,IAAI,OAAA,GAA4C,IAAA;AAChD,EAAA,IAAI,UAAA,GAAa,CAAA;AAEjB,EAAA,MAAM,QAAQ,MAAY;AACxB,IAAA,IAAI,CAAC,IAAA,EAAM;AACX,IAAA,IAAA,GAAO,KAAA;AACP,IAAA,MAAA,IAAS;AAAG,IAAA,MAAA,GAAS,IAAA;AACrB,IAAA,IAAI,OAAA,EAAS;AAAE,MAAA,QAAA,CAAS,mBAAA,CAAoB,WAAA,EAAa,OAAA,EAAS,IAAI,CAAA;AAAG,MAAA,OAAA,GAAU,IAAA;AAAA,IAAM;AACzF,IAAA,MAAM,UAAU,EAAE,UAAA;AAGlB,IAAA,aAAA,CAAc,IAAI,MAAM;AACtB,MAAA,IAAI,YAAY,UAAA,EAAY;AAC5B,MAAA,EAAA,CAAG,MAAM,OAAA,GAAU,MAAA;AAAQ,MAAA,EAAA,CAAG,eAAA,EAAgB;AAAG,MAAA,EAAA,CAAG,SAAA,CAAU,OAAO,SAAS,CAAA;AAAA,IAChF,CAAC,CAAA;AAAA,EACH,CAAA;AAEA,EAAA,MAAM,IAAA,GAAO,CAAC,UAAA,KAAiC;AAC7C,IAAA,UAAA,EAAA;AAAc,IAAA,EAAA,CAAG,SAAA,CAAU,OAAO,SAAS,CAAA;AAC3C,IAAA,IAAA,GAAO,IAAA;AAAM,IAAA,EAAA,CAAG,MAAM,OAAA,GAAU,OAAA;AAAS,IAAA,UAAA,EAAW;AACpD,IAAA,MAAA,KAAW,eAAe,UAAU,CAAA;AAAA,EACtC,CAAA;AAEA,EAAA,MAAM,gBAAA,GAAmB,CAAC,SAAA,EAAuB,QAAA,KAA+C;AAC9F,IAAA,IAAI,OAAA,EAAS;AACb,IAAA,OAAA,GAAU,CAAC,CAAA,KAAwB;AACjC,MAAA,MAAM,IAAI,CAAA,CAAE,MAAA;AACZ,MAAA,IAAI,CAAC,EAAA,CAAG,QAAA,CAAS,CAAC,CAAA,IAAK,EAAE,QAAA,GAAW,CAAC,CAAA,IAAK,KAAA,CAAA,EAAQ,SAAA,EAAU;AAAA,IAC9D,CAAA;AACA,IAAA,QAAA,CAAS,gBAAA,CAAiB,WAAA,EAAa,OAAA,EAAS,IAAI,CAAA;AAAA,EACtD,CAAA;AAEA,EAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,kBAAkB,KAAA,EAAO,MAAA,EAAQ,MAAM,IAAA,EAAK;AACjE","file":"chunk-P4G4ERG6.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The floating-layer lifecycle every popup and menu shares: a body-appended\n// element, display toggling, a scroll / resize follower that keeps it glued to\n// its anchor, a click-away dismiss, and the family's exit motion on close.\n//\n// Patterpad's surface had it (four popups: the cue picker, the slash menu, the\n// action menu, the jump picker) and Storyletter's context menu had the same\n// idea in eleven lines without the follower (ui-review-2026-09, finding 5).\n// This is the surface's, lifted whole. Positioning stays the caller's, because\n// it differs per popup: pass a `reposition` closure to `show()` and the\n// follower re-runs it on scroll and resize until `close()`.\n//\n// The element is REUSED across opens (a popup that re-renders on every\n// keystroke must not tear itself down each time), which is why a stale exit\n// teardown has a token to lose against a reopen.\n// ---------------------------------------------------------------------------\n\nimport { closeWithExit } from \"./exit.js\";\n\nexport interface Floating {\n /** The floating element. Build content into it; it is appended to <body>\n * (or the `host` given), hidden until `show()`. */\n readonly el: HTMLElement;\n /** Show `el` and keep `reposition` glued to the anchor on scroll / resize.\n * Safe to call repeatedly (on every re-render): the follower attaches once. */\n show(reposition: () => void): void;\n /** While open, call `onOutside` on a pointer-down outside `el`, and outside\n * anything `isInside` accepts (a second flyout, the anchor button).\n * Idempotent: arms once. */\n dismissOnOutside(onOutside: () => void, isInside?: (target: Node) => boolean): void;\n /** Play the exit, then hide `el`, clear its children, and drop the follower\n * and any outside listener. */\n close(): void;\n isOpen(): boolean;\n}\n\n/** Re-run `reposition` on any scroll or resize until the returned disposer runs. */\nfunction followOnScroll(reposition: () => void): () => void {\n const onMove = (): void => reposition();\n window.addEventListener(\"scroll\", onMove, true);\n window.addEventListener(\"resize\", onMove);\n return () => { window.removeEventListener(\"scroll\", onMove, true); window.removeEventListener(\"resize\", onMove); };\n}\n\nexport interface FloatingOptions {\n /** Where the element is appended. Default <body>. Under a modal <dialog> a\n * body-level node sits behind the scrim, inert, so a popup opened from\n * inside one passes the dialog here and paints in its top layer. */\n host?: HTMLElement;\n}\n\nexport function createFloating(className: string, opts: FloatingOptions = {}): Floating {\n const el = document.createElement(\"div\"); el.className = className; el.style.display = \"none\";\n (opts.host ?? document.body).appendChild(el);\n let open = false;\n let detach: (() => void) | null = null;\n let outside: ((e: MouseEvent) => void) | null = null;\n let closeToken = 0; // bumped on every show / close so a stale exit teardown cannot clobber a reopen\n\n const close = (): void => {\n if (!open) return;\n open = false;\n detach?.(); detach = null; // stop following + dismissing immediately; it is on its way out\n if (outside) { document.removeEventListener(\"mousedown\", outside, true); outside = null; }\n const myToken = ++closeToken;\n // Play the exit, THEN hide + clear. A reopen during the fade must win:\n // show() bumps closeToken, and this guard abandons the stale teardown.\n closeWithExit(el, () => {\n if (myToken !== closeToken) return;\n el.style.display = \"none\"; el.replaceChildren(); el.classList.remove(\"closing\");\n });\n };\n\n const show = (reposition: () => void): void => {\n closeToken++; el.classList.remove(\"closing\"); // cancel any in-flight exit, undo its class\n open = true; el.style.display = \"block\"; reposition();\n detach ??= followOnScroll(reposition);\n };\n\n const dismissOnOutside = (onOutside: () => void, isInside?: (target: Node) => boolean): void => {\n if (outside) return; // already armed\n outside = (e: MouseEvent): void => {\n const t = e.target as Node;\n if (!el.contains(t) && !(isInside?.(t) ?? false)) onOutside();\n };\n document.addEventListener(\"mousedown\", outside, true);\n };\n\n return { el, show, dismissOnOutside, close, isOpen: () => open };\n}\n"]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// src/util.ts
|
|
2
|
+
function plural(n, noun, pluralForm = `${noun}s`) {
|
|
3
|
+
return `${n} ${n === 1 ? noun : pluralForm}`;
|
|
4
|
+
}
|
|
5
|
+
function debounce(fn, ms) {
|
|
6
|
+
let timer;
|
|
7
|
+
let pending;
|
|
8
|
+
const run = () => {
|
|
9
|
+
timer = void 0;
|
|
10
|
+
if (pending === void 0) return;
|
|
11
|
+
const args = pending;
|
|
12
|
+
pending = void 0;
|
|
13
|
+
fn(...args);
|
|
14
|
+
};
|
|
15
|
+
const debounced = ((...args) => {
|
|
16
|
+
pending = args;
|
|
17
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
18
|
+
timer = setTimeout(run, ms);
|
|
19
|
+
});
|
|
20
|
+
debounced.cancel = () => {
|
|
21
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
22
|
+
timer = void 0;
|
|
23
|
+
pending = void 0;
|
|
24
|
+
};
|
|
25
|
+
debounced.flush = () => {
|
|
26
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
27
|
+
run();
|
|
28
|
+
};
|
|
29
|
+
return debounced;
|
|
30
|
+
}
|
|
31
|
+
function isEditableTarget(t) {
|
|
32
|
+
if (!(t instanceof Element)) return false;
|
|
33
|
+
if (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t instanceof HTMLSelectElement) return true;
|
|
34
|
+
if (t instanceof HTMLElement && t.isContentEditable === true) return true;
|
|
35
|
+
const attr = t.getAttribute("contenteditable");
|
|
36
|
+
return attr !== null && attr !== "false";
|
|
37
|
+
}
|
|
38
|
+
function formatCount(n) {
|
|
39
|
+
return n.toLocaleString();
|
|
40
|
+
}
|
|
41
|
+
var copyTimers = /* @__PURE__ */ new WeakMap();
|
|
42
|
+
function copyWithFeedback(el, text, ms = 1e3) {
|
|
43
|
+
const write = typeof navigator !== "undefined" && navigator.clipboard ? navigator.clipboard.writeText(text).catch(() => {
|
|
44
|
+
}) : Promise.resolve();
|
|
45
|
+
el.classList.add("copied");
|
|
46
|
+
const prior = copyTimers.get(el);
|
|
47
|
+
if (prior !== void 0) clearTimeout(prior);
|
|
48
|
+
copyTimers.set(el, setTimeout(() => {
|
|
49
|
+
el.classList.remove("copied");
|
|
50
|
+
copyTimers.delete(el);
|
|
51
|
+
}, ms));
|
|
52
|
+
return write;
|
|
53
|
+
}
|
|
54
|
+
function relativeTime(date, now = Date.now()) {
|
|
55
|
+
const then = date instanceof Date ? date.getTime() : new Date(date).getTime();
|
|
56
|
+
if (!Number.isFinite(then)) return "";
|
|
57
|
+
const mins = Math.floor((now - then) / 6e4);
|
|
58
|
+
if (mins < 1) return "just now";
|
|
59
|
+
if (mins < 60) return `${plural(mins, "minute")} ago`;
|
|
60
|
+
const hours = Math.floor(mins / 60);
|
|
61
|
+
if (hours === 1) return "an hour ago";
|
|
62
|
+
if (hours < 24) return `${hours} hours ago`;
|
|
63
|
+
const days = Math.floor(hours / 24);
|
|
64
|
+
if (days === 1) return "yesterday";
|
|
65
|
+
if (days < 7) return `${days} days ago`;
|
|
66
|
+
return new Date(then).toLocaleDateString(void 0, { day: "numeric", month: "short", year: "numeric" });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { copyWithFeedback, debounce, formatCount, isEditableTarget, plural, relativeTime };
|
|
70
|
+
//# sourceMappingURL=chunk-P6WLAEKZ.js.map
|
|
71
|
+
//# sourceMappingURL=chunk-P6WLAEKZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/util.ts"],"names":[],"mappings":";AAaO,SAAS,OAAO,CAAA,EAAW,IAAA,EAAc,UAAA,GAAa,CAAA,EAAG,IAAI,CAAA,CAAA,CAAA,EAAa;AAC/E,EAAA,OAAO,GAAG,CAAC,CAAA,CAAA,EAAI,CAAA,KAAM,CAAA,GAAI,OAAO,UAAU,CAAA,CAAA;AAC5C;AAWO,SAAS,QAAA,CAA8B,IAA0B,EAAA,EAA0B;AAChG,EAAA,IAAI,KAAA;AACJ,EAAA,IAAI,OAAA;AACJ,EAAA,MAAM,MAAM,MAAY;AACtB,IAAA,KAAA,GAAQ,MAAA;AACR,IAAA,IAAI,YAAY,MAAA,EAAW;AAC3B,IAAA,MAAM,IAAA,GAAO,OAAA;AACb,IAAA,OAAA,GAAU,MAAA;AACV,IAAA,EAAA,CAAG,GAAG,IAAI,CAAA;AAAA,EACZ,CAAA;AACA,EAAA,MAAM,SAAA,IAAa,IAAI,IAAA,KAAkB;AACvC,IAAA,OAAA,GAAU,IAAA;AACV,IAAA,IAAI,KAAA,KAAU,MAAA,EAAW,YAAA,CAAa,KAAK,CAAA;AAC3C,IAAA,KAAA,GAAQ,UAAA,CAAW,KAAK,EAAE,CAAA;AAAA,EAC5B,CAAA,CAAA;AACA,EAAA,SAAA,CAAU,SAAS,MAAM;AAAE,IAAA,IAAI,KAAA,KAAU,MAAA,EAAW,YAAA,CAAa,KAAK,CAAA;AAAG,IAAA,KAAA,GAAQ,MAAA;AAAW,IAAA,OAAA,GAAU,MAAA;AAAA,EAAW,CAAA;AACjH,EAAA,SAAA,CAAU,QAAQ,MAAM;AAAE,IAAA,IAAI,KAAA,KAAU,MAAA,EAAW,YAAA,CAAa,KAAK,CAAA;AAAG,IAAA,GAAA,EAAI;AAAA,EAAG,CAAA;AAC/E,EAAA,OAAO,SAAA;AACT;AASO,SAAS,iBAAiB,CAAA,EAA4C;AAC3E,EAAA,IAAI,EAAE,CAAA,YAAa,OAAA,CAAA,EAAU,OAAO,KAAA;AACpC,EAAA,IAAI,aAAa,gBAAA,IAAoB,CAAA,YAAa,mBAAA,IAAuB,CAAA,YAAa,mBAAmB,OAAO,IAAA;AAChH,EAAA,IAAI,CAAA,YAAa,WAAA,IAAe,CAAA,CAAE,iBAAA,KAAsB,MAAM,OAAO,IAAA;AACrE,EAAA,MAAM,IAAA,GAAO,CAAA,CAAE,YAAA,CAAa,iBAAiB,CAAA;AAC7C,EAAA,OAAO,IAAA,KAAS,QAAQ,IAAA,KAAS,OAAA;AACnC;AAGO,SAAS,YAAY,CAAA,EAAmB;AAC7C,EAAA,OAAO,EAAE,cAAA,EAAe;AAC1B;AAEA,IAAM,UAAA,uBAAiB,OAAA,EAAgD;AAOhE,SAAS,gBAAA,CAAiB,EAAA,EAAa,IAAA,EAAc,EAAA,GAAK,GAAA,EAAqB;AACpF,EAAA,MAAM,KAAA,GAAQ,OAAO,SAAA,KAAc,WAAA,IAAe,SAAA,CAAU,SAAA,GACxD,SAAA,CAAU,SAAA,CAAU,SAAA,CAAU,IAAI,CAAA,CAAE,KAAA,CAAM,MAAM;AAAA,EAAC,CAAC,CAAA,GAClD,OAAA,CAAQ,OAAA,EAAQ;AACpB,EAAA,EAAA,CAAG,SAAA,CAAU,IAAI,QAAQ,CAAA;AACzB,EAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,GAAA,CAAI,EAAE,CAAA;AAC/B,EAAA,IAAI,KAAA,KAAU,MAAA,EAAW,YAAA,CAAa,KAAK,CAAA;AAC3C,EAAA,UAAA,CAAW,GAAA,CAAI,EAAA,EAAI,UAAA,CAAW,MAAM;AAAE,IAAA,EAAA,CAAG,SAAA,CAAU,OAAO,QAAQ,CAAA;AAAG,IAAA,UAAA,CAAW,OAAO,EAAE,CAAA;AAAA,EAAG,CAAA,EAAG,EAAE,CAAC,CAAA;AAClG,EAAA,OAAO,KAAA;AACT;AAQO,SAAS,YAAA,CAAa,IAAA,EAA8B,GAAA,GAAc,IAAA,CAAK,KAAI,EAAW;AAC3F,EAAA,MAAM,IAAA,GAAO,IAAA,YAAgB,IAAA,GAAO,IAAA,CAAK,OAAA,KAAY,IAAI,IAAA,CAAK,IAAI,CAAA,CAAE,OAAA,EAAQ;AAC5E,EAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,IAAI,GAAG,OAAO,EAAA;AACnC,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAA,CAAO,GAAA,GAAM,QAAQ,GAAK,CAAA;AAC5C,EAAA,IAAI,IAAA,GAAO,GAAG,OAAO,UAAA;AACrB,EAAA,IAAI,OAAO,EAAA,EAAI,OAAO,GAAG,MAAA,CAAO,IAAA,EAAM,QAAQ,CAAC,CAAA,IAAA,CAAA;AAC/C,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,IAAA,GAAO,EAAE,CAAA;AAClC,EAAA,IAAI,KAAA,KAAU,GAAG,OAAO,aAAA;AACxB,EAAA,IAAI,KAAA,GAAQ,EAAA,EAAI,OAAO,CAAA,EAAG,KAAK,CAAA,UAAA,CAAA;AAC/B,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,KAAA,GAAQ,EAAE,CAAA;AAClC,EAAA,IAAI,IAAA,KAAS,GAAG,OAAO,WAAA;AACvB,EAAA,IAAI,IAAA,GAAO,CAAA,EAAG,OAAO,CAAA,EAAG,IAAI,CAAA,SAAA,CAAA;AAC5B,EAAA,OAAO,IAAI,IAAA,CAAK,IAAI,CAAA,CAAE,kBAAA,CAAmB,MAAA,EAAW,EAAE,GAAA,EAAK,SAAA,EAAW,KAAA,EAAO,OAAA,EAAS,IAAA,EAAM,WAAW,CAAA;AACzG","file":"chunk-P6WLAEKZ.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The small idioms both apps wrote by hand and never gave a name: the plural\n// suffix (about forty sites), the debounce (eight copies), \"is the focus in a\n// field\" (four copies that disagreed about <select>), the count with grouping\n// separators, clipboard copy with the `.copied` revert (three timings), and\n// relative time (two wordings). One answer each, here.\n//\n// DOM-free except where the idiom is about the DOM (isEditableTarget,\n// copyWithFeedback), so main-process code can import the rest.\n// ---------------------------------------------------------------------------\n\n/** \"1 link\" / \"3 links\". The plural form defaults to `noun + \"s\"`; pass the\n * irregular one (\"entry\", \"entries\") when that is wrong. */\nexport function plural(n: number, noun: string, pluralForm = `${noun}s`): string {\n return `${n} ${n === 1 ? noun : pluralForm}`;\n}\n\nexport interface Debounced<A extends unknown[]> {\n (...args: A): void;\n /** Drop the pending call. */\n cancel(): void;\n /** Run the pending call now, if there is one. */\n flush(): void;\n}\n\n/** Trailing-edge debounce: the call runs `ms` after the LAST call. */\nexport function debounce<A extends unknown[]>(fn: (...args: A) => void, ms: number): Debounced<A> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let pending: A | undefined;\n const run = (): void => {\n timer = undefined;\n if (pending === undefined) return;\n const args = pending;\n pending = undefined;\n fn(...args);\n };\n const debounced = ((...args: A): void => {\n pending = args;\n if (timer !== undefined) clearTimeout(timer);\n timer = setTimeout(run, ms);\n }) as Debounced<A>;\n debounced.cancel = () => { if (timer !== undefined) clearTimeout(timer); timer = undefined; pending = undefined; };\n debounced.flush = () => { if (timer !== undefined) clearTimeout(timer); run(); };\n return debounced;\n}\n\n/**\n * Is the keyboard's focus in a field? The one agreed answer: an input, a\n * textarea, a select, or anything contenteditable (the attribute is read as\n * well as `isContentEditable`, which jsdom does not implement). A window-level\n * shortcut checks this before acting, so a bare letter typed into a field\n * stays a letter.\n */\nexport function isEditableTarget(t: EventTarget | null | undefined): boolean {\n if (!(t instanceof Element)) return false;\n if (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t instanceof HTMLSelectElement) return true;\n if (t instanceof HTMLElement && t.isContentEditable === true) return true;\n const attr = t.getAttribute(\"contenteditable\");\n return attr !== null && attr !== \"false\";\n}\n\n/** A count for reading: grouping separators in the user's locale. */\nexport function formatCount(n: number): string {\n return n.toLocaleString();\n}\n\nconst copyTimers = new WeakMap<Element, ReturnType<typeof setTimeout>>();\n\n/**\n * Copy `text` to the clipboard and light `el` with `.copied` for `ms` (the\n * family's 1000). A second click restarts the timer rather than stacking a\n * second revert under the first. Resolves when the clipboard write has.\n */\nexport function copyWithFeedback(el: Element, text: string, ms = 1000): Promise<void> {\n const write = typeof navigator !== \"undefined\" && navigator.clipboard\n ? navigator.clipboard.writeText(text).catch(() => {})\n : Promise.resolve();\n el.classList.add(\"copied\");\n const prior = copyTimers.get(el);\n if (prior !== undefined) clearTimeout(prior);\n copyTimers.set(el, setTimeout(() => { el.classList.remove(\"copied\"); copyTimers.delete(el); }, ms));\n return write;\n}\n\n/**\n * How long ago, in one wording: \"just now\", \"4 minutes ago\", \"an hour ago\",\n * \"3 hours ago\", \"yesterday\", \"5 days ago\", and past a week the date itself.\n * Coarse on purpose: the question a reader has is whether the thing is from\n * this sitting, not how many seconds ago it was. A future date is \"just now\".\n */\nexport function relativeTime(date: Date | number | string, now: number = Date.now()): string {\n const then = date instanceof Date ? date.getTime() : new Date(date).getTime();\n if (!Number.isFinite(then)) return \"\";\n const mins = Math.floor((now - then) / 60000);\n if (mins < 1) return \"just now\";\n if (mins < 60) return `${plural(mins, \"minute\")} ago`;\n const hours = Math.floor(mins / 60);\n if (hours === 1) return \"an hour ago\";\n if (hours < 24) return `${hours} hours ago`;\n const days = Math.floor(hours / 24);\n if (days === 1) return \"yesterday\";\n if (days < 7) return `${days} days ago`;\n return new Date(then).toLocaleDateString(undefined, { day: \"numeric\", month: \"short\", year: \"numeric\" });\n}\n"]}
|