@wildwinter/app-shell 0.4.1 → 0.14.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 (81) hide show
  1. package/README.md +35 -0
  2. package/dist/about.css +76 -0
  3. package/dist/about.css.map +1 -0
  4. package/dist/anchored.css +50 -0
  5. package/dist/anchored.css.map +1 -0
  6. package/dist/app-store.cjs +74 -0
  7. package/dist/app-store.cjs.map +1 -0
  8. package/dist/app-store.d.cts +75 -0
  9. package/dist/app-store.d.ts +75 -0
  10. package/dist/app-store.js +72 -0
  11. package/dist/app-store.js.map +1 -0
  12. package/dist/chunk-MDGOIT3O.js +58 -0
  13. package/dist/chunk-MDGOIT3O.js.map +1 -0
  14. package/dist/chunk-R6RO5TXB.js +16 -0
  15. package/dist/chunk-R6RO5TXB.js.map +1 -0
  16. package/dist/comments.css +109 -0
  17. package/dist/comments.css.map +1 -0
  18. package/dist/confirm.css +62 -0
  19. package/dist/confirm.css.map +1 -0
  20. package/dist/identity.css +62 -0
  21. package/dist/identity.css.map +1 -0
  22. package/dist/index.cjs +926 -6
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +371 -3
  25. package/dist/index.d.ts +371 -3
  26. package/dist/index.js +896 -8
  27. package/dist/index.js.map +1 -1
  28. package/dist/job.cjs +61 -0
  29. package/dist/job.cjs.map +1 -0
  30. package/dist/job.css +70 -0
  31. package/dist/job.css.map +1 -0
  32. package/dist/job.d.cts +56 -0
  33. package/dist/job.d.ts +56 -0
  34. package/dist/job.js +3 -0
  35. package/dist/job.js.map +1 -0
  36. package/dist/menu.cjs +7 -0
  37. package/dist/menu.cjs.map +1 -1
  38. package/dist/menu.d.cts +20 -1
  39. package/dist/menu.d.ts +20 -1
  40. package/dist/menu.js +1 -1
  41. package/dist/notes-editor.css +107 -0
  42. package/dist/notes-editor.css.map +1 -0
  43. package/dist/session.cjs +52 -0
  44. package/dist/session.cjs.map +1 -0
  45. package/dist/session.d.cts +79 -0
  46. package/dist/session.d.ts +79 -0
  47. package/dist/session.js +50 -0
  48. package/dist/session.js.map +1 -0
  49. package/dist/tool-window.cjs +79 -0
  50. package/dist/tool-window.cjs.map +1 -0
  51. package/dist/tool-window.css +60 -0
  52. package/dist/tool-window.css.map +1 -0
  53. package/dist/tool-window.d.cts +63 -0
  54. package/dist/tool-window.d.ts +63 -0
  55. package/dist/tool-window.js +73 -0
  56. package/dist/tool-window.js.map +1 -0
  57. package/dist/tooltip.css +22 -0
  58. package/dist/tooltip.css.map +1 -0
  59. package/dist/updater.cjs +315 -0
  60. package/dist/updater.cjs.map +1 -0
  61. package/dist/updater.d.cts +53 -0
  62. package/dist/updater.d.ts +53 -0
  63. package/dist/updater.js +306 -0
  64. package/dist/updater.js.map +1 -0
  65. package/dist/vc-status.cjs +77 -0
  66. package/dist/vc-status.cjs.map +1 -0
  67. package/dist/vc-status.d.cts +40 -0
  68. package/dist/vc-status.d.ts +40 -0
  69. package/dist/vc-status.js +71 -0
  70. package/dist/vc-status.js.map +1 -0
  71. package/dist/vc.css +41 -0
  72. package/dist/vc.css.map +1 -0
  73. package/package.json +90 -1
  74. package/dist/chunk-KQT3XYRS.js +0 -10
  75. package/dist/chunk-KQT3XYRS.js.map +0 -1
  76. package/dist/pane-shell.d.cts +0 -2
  77. package/dist/pane-shell.d.ts +0 -2
  78. package/dist/settings.d.cts +0 -2
  79. package/dist/settings.d.ts +0 -2
  80. package/dist/tokens.d.cts +0 -2
  81. package/dist/tokens.d.ts +0 -2
package/README.md CHANGED
@@ -29,6 +29,41 @@ package supplies the grammar and primitives beneath them.
29
29
  @import "@wildwinter/app-shell/tokens.css";
30
30
  ```
31
31
 
32
+ ## v0.6 (the long-job kit)
33
+
34
+ For work that takes long enough to need a progress bar and a Cancel button:
35
+ coverage sweeps, big exports, corpus runs.
36
+
37
+ ```ts
38
+ // main
39
+ import { createJobHost, JOB_PROGRESS } from "@wildwinter/app-shell/job";
40
+ const jobs = createJobHost({ send: (ch, p) => window.webContents.send(ch, p) });
41
+ const outcome = await jobs.start("coverage", async (ctx) => {
42
+ for (let i = 0; i < runs; i++) {
43
+ if (ctx.cancelled) break; // cancellation is a boundary, not an interrupt
44
+ doOneRun();
45
+ await ctx.step(i + 1, runs); // reports progress AND yields the loop
46
+ }
47
+ return report; // a cancelled job may still hand back a partial
48
+ });
49
+ jobs.cancel("coverage");
50
+ ```
51
+
52
+ ```ts
53
+ // renderer
54
+ import { mountJobProgress } from "@wildwinter/app-shell";
55
+ import "@wildwinter/app-shell/job.css";
56
+ const strip = mountJobProgress(host, { units: "runs", onCancel: () => api.cancel() });
57
+ ```
58
+
59
+ **Cooperative, not parallel.** The work still runs in the main process; it just
60
+ stops hogging it, so IPC keeps flowing and the renderer keeps painting.
61
+ `ctx.step()` throttles progress messages (100ms) separately from yielding the
62
+ event loop (16ms), so a fast inner loop neither floods IPC nor stutters.
63
+
64
+ **A strip, not a modal.** The window that started the job shows it; the rest of
65
+ the app stays usable.
66
+
32
67
  ## Roadmap
33
68
 
34
69
  Later slices lift the rest of the common shell from Patterpad (canonical),
package/dist/about.css ADDED
@@ -0,0 +1,76 @@
1
+ /* src/about.css */
2
+ .shell-about {
3
+ width: min(420px, 90vw);
4
+ border: 1px solid var(--line);
5
+ border-radius: var(--radius-md);
6
+ background: var(--surface);
7
+ color: var(--ink);
8
+ padding: 22px 24px 16px;
9
+ box-shadow: var(--shadow-2, var(--shadow-1));
10
+ text-align: center;
11
+ }
12
+ .shell-about::backdrop {
13
+ background: rgba(0, 0, 0, 0.28);
14
+ }
15
+ .shell-about-mark {
16
+ margin-bottom: 12px;
17
+ }
18
+ .shell-about-mark svg {
19
+ max-width: 220px;
20
+ height: auto;
21
+ }
22
+ .shell-about-name {
23
+ font: 600 1.15rem/1.2 var(--font-read, var(--font-ui));
24
+ }
25
+ .shell-about-version {
26
+ font: 0.78rem/1.4 var(--font-ui);
27
+ color: var(--muted);
28
+ margin-top: 2px;
29
+ }
30
+ .shell-about-blurb {
31
+ font: 0.85rem/1.5 var(--font-read, var(--font-ui));
32
+ margin: 14px 0 0;
33
+ }
34
+ .shell-about-credits {
35
+ font: 0.78rem/1.55 var(--font-ui);
36
+ color: var(--muted);
37
+ margin: 10px 0 0;
38
+ white-space: pre-line;
39
+ }
40
+ .shell-about-links {
41
+ display: flex;
42
+ gap: 14px;
43
+ justify-content: center;
44
+ margin-top: 14px;
45
+ }
46
+ .shell-about-link {
47
+ border: 0;
48
+ background: none;
49
+ padding: 0;
50
+ cursor: pointer;
51
+ font: 0.78rem/1 var(--font-ui);
52
+ color: var(--accent);
53
+ text-decoration: underline;
54
+ }
55
+ .shell-about-link:hover {
56
+ text-decoration: none;
57
+ }
58
+ .shell-about-actions {
59
+ display: flex;
60
+ justify-content: center;
61
+ margin-top: 18px;
62
+ }
63
+ .shell-about-close {
64
+ font: 0.8rem/1 var(--font-ui);
65
+ padding: 6px 18px;
66
+ border-radius: var(--radius-sm);
67
+ border: 1px solid var(--line);
68
+ background: var(--card);
69
+ color: var(--ink);
70
+ cursor: pointer;
71
+ }
72
+ .shell-about-close:hover {
73
+ border-color: var(--accent);
74
+ color: var(--accent);
75
+ }
76
+ /*# sourceMappingURL=about.css.map */
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/about.css"],"sourcesContent":["/* The About surface (about.ts): the family's, so two apps' About boxes are\n recognisably siblings. Quiet, centred, one way out. */\n.shell-about {\n width: min(420px, 90vw);\n border: 1px solid var(--line);\n border-radius: var(--radius-md);\n background: var(--surface); color: var(--ink);\n padding: 22px 24px 16px;\n box-shadow: var(--shadow-2, var(--shadow-1));\n text-align: center;\n}\n.shell-about::backdrop { background: rgba(0, 0, 0, 0.28); }\n.shell-about-mark { margin-bottom: 12px; }\n.shell-about-mark svg { max-width: 220px; height: auto; }\n.shell-about-name { font: 600 1.15rem/1.2 var(--font-read, var(--font-ui)); }\n.shell-about-version { font: 0.78rem/1.4 var(--font-ui); color: var(--muted); margin-top: 2px; }\n.shell-about-blurb { font: 0.85rem/1.5 var(--font-read, var(--font-ui)); margin: 14px 0 0; }\n.shell-about-credits { font: 0.78rem/1.55 var(--font-ui); color: var(--muted); margin: 10px 0 0; white-space: pre-line; }\n.shell-about-links { display: flex; gap: 14px; justify-content: center; margin-top: 14px; }\n.shell-about-link {\n border: 0; background: none; padding: 0; cursor: pointer;\n font: 0.78rem/1 var(--font-ui); color: var(--accent); text-decoration: underline;\n}\n.shell-about-link:hover { text-decoration: none; }\n.shell-about-actions { display: flex; justify-content: center; margin-top: 18px; }\n.shell-about-close {\n font: 0.8rem/1 var(--font-ui); padding: 6px 18px;\n border-radius: var(--radius-sm); border: 1px solid var(--line);\n background: var(--card); color: var(--ink); cursor: pointer;\n}\n.shell-about-close:hover { border-color: var(--accent); color: var(--accent); }\n"],"mappings":";AAEA,CAAC;AACC,SAAO,IAAI,KAAK,EAAE;AAClB,UAAQ,IAAI,MAAM,IAAI;AACtB,iBAAe,IAAI;AACnB,cAAY,IAAI;AAAY,SAAO,IAAI;AACvC,WAAS,KAAK,KAAK;AACnB,cAAY,IAAI,UAAU,EAAE,IAAI;AAChC,cAAY;AACd;AACA,CATC,WASW;AAAa,cAAY,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;AAAO;AAC1D,CAAC;AAAmB,iBAAe;AAAM;AACzC,CADC,iBACiB;AAAM,aAAW;AAAO,UAAQ;AAAM;AACxD,CAAC;AAAmB,QAAM,IAAI,OAAO,CAAC,IAAI,IAAI,WAAW,EAAE,IAAI;AAAa;AAC5E,CAAC;AAAsB,QAAM,OAAO,CAAC,IAAI,IAAI;AAAY,SAAO,IAAI;AAAU,cAAY;AAAK;AAC/F,CAAC;AAAoB,QAAM,OAAO,CAAC,IAAI,IAAI,WAAW,EAAE,IAAI;AAAa,UAAQ,KAAK,EAAE;AAAG;AAC3F,CAAC;AAAsB,QAAM,OAAO,CAAC,KAAK,IAAI;AAAY,SAAO,IAAI;AAAU,UAAQ,KAAK,EAAE;AAAG,eAAa;AAAU;AACxH,CAAC;AAAoB,WAAS;AAAM,OAAK;AAAM,mBAAiB;AAAQ,cAAY;AAAM;AAC1F,CAAC;AACC,UAAQ;AAAG,cAAY;AAAM,WAAS;AAAG,UAAQ;AACjD,QAAM,OAAO,CAAC,EAAE,IAAI;AAAY,SAAO,IAAI;AAAW,mBAAiB;AACzE;AACA,CAJC,gBAIgB;AAAS,mBAAiB;AAAM;AACjD,CAAC;AAAsB,WAAS;AAAM,mBAAiB;AAAQ,cAAY;AAAM;AACjF,CAAC;AACC,QAAM,MAAM,CAAC,EAAE,IAAI;AAAY,WAAS,IAAI;AAC5C,iBAAe,IAAI;AAAc,UAAQ,IAAI,MAAM,IAAI;AACvD,cAAY,IAAI;AAAS,SAAO,IAAI;AAAQ,UAAQ;AACtD;AACA,CALC,iBAKiB;AAAS,gBAAc,IAAI;AAAW,SAAO,IAAI;AAAW;","names":[]}
@@ -0,0 +1,50 @@
1
+ /* src/anchored.css */
2
+ .shell-anchored {
3
+ position: fixed;
4
+ z-index: 40;
5
+ display: flex;
6
+ flex-direction: column;
7
+ border: 1px solid var(--line);
8
+ border-radius: var(--radius-md);
9
+ background: var(--surface);
10
+ color: var(--ink);
11
+ box-shadow: var(--shadow-2, var(--shadow-1));
12
+ overflow: hidden;
13
+ }
14
+ .shell-anchored-head {
15
+ flex: none;
16
+ display: flex;
17
+ align-items: center;
18
+ justify-content: space-between;
19
+ gap: 8px;
20
+ padding: 8px 10px;
21
+ border-bottom: 1px solid var(--line-soft);
22
+ }
23
+ .shell-anchored-title {
24
+ font: 600 0.72rem/1 var(--font-ui);
25
+ text-transform: uppercase;
26
+ letter-spacing: 0.04em;
27
+ color: var(--muted);
28
+ }
29
+ .shell-anchored-close {
30
+ border: 0;
31
+ background: none;
32
+ color: var(--muted);
33
+ font-size: 0.8rem;
34
+ line-height: 1;
35
+ padding: 0 2px;
36
+ cursor: pointer;
37
+ }
38
+ .shell-anchored-close:hover {
39
+ color: var(--accent);
40
+ }
41
+ .shell-anchored-body {
42
+ flex: 1;
43
+ min-height: 0;
44
+ overflow-y: auto;
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: 8px;
48
+ padding: 10px;
49
+ }
50
+ /*# sourceMappingURL=anchored.css.map */
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/anchored.css"],"sourcesContent":["/* The anchored panel (anchored.ts): a floating panel hung off the thing it is\n about. Sized and positioned in script (the placement has to measure); this is\n only what it looks like. Its body scrolls, because the placement caps the\n height rather than letting a long thread run off the screen. */\n.shell-anchored {\n position: fixed; z-index: 40;\n display: flex; flex-direction: column;\n border: 1px solid var(--line);\n border-radius: var(--radius-md);\n background: var(--surface); color: var(--ink);\n box-shadow: var(--shadow-2, var(--shadow-1));\n overflow: hidden;\n}\n.shell-anchored-head {\n flex: none;\n display: flex; align-items: center; justify-content: space-between; gap: 8px;\n padding: 8px 10px;\n border-bottom: 1px solid var(--line-soft);\n}\n.shell-anchored-title {\n font: 600 0.72rem/1 var(--font-ui);\n text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted);\n}\n.shell-anchored-close {\n border: 0; background: none; color: var(--muted);\n font-size: 0.8rem; line-height: 1; padding: 0 2px; cursor: pointer;\n}\n.shell-anchored-close:hover { color: var(--accent); }\n.shell-anchored-body {\n flex: 1; min-height: 0; overflow-y: auto;\n display: flex; flex-direction: column; gap: 8px;\n padding: 10px;\n}\n"],"mappings":";AAIA,CAAC;AACC,YAAU;AAAO,WAAS;AAC1B,WAAS;AAAM,kBAAgB;AAC/B,UAAQ,IAAI,MAAM,IAAI;AACtB,iBAAe,IAAI;AACnB,cAAY,IAAI;AAAY,SAAO,IAAI;AACvC,cAAY,IAAI,UAAU,EAAE,IAAI;AAChC,YAAU;AACZ;AACA,CAAC;AACC,QAAM;AACN,WAAS;AAAM,eAAa;AAAQ,mBAAiB;AAAe,OAAK;AACzE,WAAS,IAAI;AACb,iBAAe,IAAI,MAAM,IAAI;AAC/B;AACA,CAAC;AACC,QAAM,IAAI,OAAO,CAAC,EAAE,IAAI;AACxB,kBAAgB;AAAW,kBAAgB;AAAQ,SAAO,IAAI;AAChE;AACA,CAAC;AACC,UAAQ;AAAG,cAAY;AAAM,SAAO,IAAI;AACxC,aAAW;AAAQ,eAAa;AAAG,WAAS,EAAE;AAAK,UAAQ;AAC7D;AACA,CAJC,oBAIoB;AAAS,SAAO,IAAI;AAAW;AACpD,CAAC;AACC,QAAM;AAAG,cAAY;AAAG,cAAY;AACpC,WAAS;AAAM,kBAAgB;AAAQ,OAAK;AAC5C,WAAS;AACX;","names":[]}
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs');
4
+ var path = require('path');
5
+
6
+ // src/app-store.ts
7
+ function createAppStore(opts) {
8
+ const file = path.join(opts.dir, opts.fileName ?? "app-settings.json");
9
+ const maxRecents = opts.maxRecents ?? 8;
10
+ let loaded = {};
11
+ try {
12
+ loaded = JSON.parse(fs.readFileSync(file, "utf8"));
13
+ } catch {
14
+ }
15
+ const state = {
16
+ recents: [],
17
+ panes: opts.panes ?? {},
18
+ windows: {},
19
+ ...loaded,
20
+ // The app's slice merges FIELD BY FIELD, so a key added in a new version
21
+ // arrives with its default rather than being absent for everybody who has
22
+ // run the app before.
23
+ app: { ...opts.defaults, ...loaded.app ?? {} }
24
+ };
25
+ const save = () => {
26
+ try {
27
+ fs.mkdirSync(opts.dir, { recursive: true });
28
+ const temp = `${file}.tmp`;
29
+ fs.writeFileSync(temp, `${JSON.stringify(state, null, 2)}
30
+ `, "utf8");
31
+ fs.renameSync(temp, file);
32
+ } catch {
33
+ }
34
+ };
35
+ return {
36
+ get: () => structuredClone(state),
37
+ patchApp(patch) {
38
+ Object.assign(state.app, patch);
39
+ save();
40
+ },
41
+ touchProject(path) {
42
+ if (state.lastProject !== path) delete state.place;
43
+ state.recents = [path, ...state.recents.filter((p) => p !== path)].slice(0, maxRecents);
44
+ state.lastProject = path;
45
+ save();
46
+ },
47
+ forgetProject(path) {
48
+ state.recents = state.recents.filter((p) => p !== path);
49
+ if (state.lastProject === path) delete state.lastProject;
50
+ save();
51
+ },
52
+ setPlace(place) {
53
+ if (JSON.stringify(state.place) === JSON.stringify(place)) return;
54
+ state.place = place;
55
+ save();
56
+ },
57
+ setPanes(panes) {
58
+ state.panes = { ...state.panes, ...panes };
59
+ save();
60
+ },
61
+ setIdentity(identity) {
62
+ state.identity = identity;
63
+ save();
64
+ },
65
+ setWindow(key, next) {
66
+ state.windows[key] = { ...state.windows[key], ...next };
67
+ save();
68
+ }
69
+ };
70
+ }
71
+
72
+ exports.createAppStore = createAppStore;
73
+ //# sourceMappingURL=app-store.cjs.map
74
+ //# sourceMappingURL=app-store.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/app-store.ts"],"names":["join","readFileSync","mkdirSync","writeFileSync","renameSync"],"mappings":"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","file":"app-store.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// The user's settings for THIS app: a small JSON file in the app's userData.\n//\n// Every app in the family has one, and both of the two that exist wrote it\n// separately: the same tolerant read, the same merged defaults, the same capped\n// recents, the same panes, the same per-tool-window bounds and pin, the same\n// identity. That is the definition of shell work.\n//\n// WHAT BELONGS HERE is settings that follow the PERSON, not the project: where\n// their windows were, which panes they like open, what they are called, what\n// they had open last. Project content never comes near this file - it is not\n// version controlled, it is not shared, and losing it should cost an author\n// nothing but a re-arranged window.\n//\n// The shell owns the CORE it can reason about and hands the app one slice of\n// its own (`app`) for everything else - a theme value, a view mode, whatever it\n// has. Two stores in one file, one atomic write, so a crash cannot leave an app\n// with half its settings.\n//\n// Electron-free on purpose: it takes a directory. That keeps it unit-testable\n// and lets a test point it at a temp dir.\n// ---------------------------------------------------------------------------\n\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/** Who is at the keyboard. Never in a project: see identity.ts. */\nexport interface Identity { name: string; email?: string }\n\n/** A remembered window rectangle. Absent = the app's default, centred. */\nexport interface WindowBounds { x?: number; y?: number; width: number; height: number }\n\n/** One helper window's remembered state. */\nexport interface WindowState { bounds?: WindowBounds; pinned?: boolean }\n\n/** Side panes: open/closed, and any dragged width. */\nexport interface PaneState {\n nav?: boolean;\n inspector?: boolean;\n navW?: number;\n inspW?: number;\n}\n\n/**\n * The part every app has.\n *\n * `place` is deliberately opaque: \"open where you left off\" is a family idea,\n * but WHERE is a scene and a caret in one app and a box, a tab and a card in\n * another. The shell stores it and clears it when the project changes, which is\n * the one rule about it that is not app-specific.\n */\nexport interface AppSettingsCore<Place> {\n recents: string[];\n lastProject?: string;\n place?: Place;\n panes: PaneState;\n identity?: Identity;\n /** Keyed by whatever the app calls each window (\"board\", \"search\"). */\n windows: Record<string, WindowState>;\n}\n\nexport type AppSettings<Place, App> = AppSettingsCore<Place> & { app: App };\n\nexport interface AppStoreOptions<Place, App> {\n /** The app's userData directory. */\n dir: string;\n /** The file inside it. One per app; two apps never share. */\n fileName?: string;\n /** The app's own slice, as it should read on a first run. */\n defaults: App;\n /** How the panes should sit before anybody touches them. */\n panes?: PaneState;\n maxRecents?: number;\n}\n\nexport interface AppStore<Place, App> {\n /** A copy, always: nothing outside can mutate the state behind the store's back. */\n get(): AppSettings<Place, App>;\n /** Merge into the app's own slice and save. */\n patchApp(patch: Partial<App>): void;\n /** Record an opened project: front of recents, deduped, capped. */\n touchProject(path: string): void;\n /** Drop one that failed to open (moved, deleted). */\n forgetProject(path: string): void;\n /** Where the author was. Compared before writing: this is called on every\n * navigation and must not mean a disk write per click. */\n setPlace(place: Place): void;\n setPanes(panes: PaneState): void;\n setIdentity(identity: Identity): void;\n /** One window's bounds and/or pin, merged with what it already had. */\n setWindow(key: string, state: WindowState): void;\n}\n\nexport function createAppStore<Place, App extends object>(\n opts: AppStoreOptions<Place, App>,\n): AppStore<Place, App> {\n const file = join(opts.dir, opts.fileName ?? \"app-settings.json\");\n const maxRecents = opts.maxRecents ?? 8;\n\n let loaded: Partial<AppSettings<Place, App>> = {};\n try {\n loaded = JSON.parse(readFileSync(file, \"utf8\")) as Partial<AppSettings<Place, App>>;\n } catch {\n // First run, or a file we cannot read. Defaults, never a crash: settings\n // are not worth failing to start over.\n }\n\n const state: AppSettings<Place, App> = {\n recents: [],\n panes: opts.panes ?? {},\n windows: {},\n ...loaded,\n // The app's slice merges FIELD BY FIELD, so a key added in a new version\n // arrives with its default rather than being absent for everybody who has\n // run the app before.\n app: { ...opts.defaults, ...(loaded.app ?? {}) },\n };\n\n const save = (): void => {\n try {\n mkdirSync(opts.dir, { recursive: true });\n // Write beside, then rename: a rename is atomic on every platform this\n // ships to, so a crash mid-write cannot leave a half-written settings\n // file - which both apps' hand-written stores could.\n const temp = `${file}.tmp`;\n writeFileSync(temp, `${JSON.stringify(state, null, 2)}\\n`, \"utf8\");\n renameSync(temp, file);\n } catch {\n // A read-only home directory, a full disk: the app carries on with the\n // settings it has in memory rather than dying over a preference.\n }\n };\n\n return {\n get: () => structuredClone(state),\n patchApp(patch) {\n Object.assign(state.app, patch);\n save();\n },\n touchProject(path) {\n // A DIFFERENT project invalidates the remembered place; the same one must\n // keep it, or \"open where you left off\" could never work, since reopening\n // goes through here.\n if (state.lastProject !== path) delete state.place;\n state.recents = [path, ...state.recents.filter((p) => p !== path)].slice(0, maxRecents);\n state.lastProject = path;\n save();\n },\n forgetProject(path) {\n state.recents = state.recents.filter((p) => p !== path);\n if (state.lastProject === path) delete state.lastProject;\n save();\n },\n setPlace(place) {\n if (JSON.stringify(state.place) === JSON.stringify(place)) return;\n state.place = place;\n save();\n },\n setPanes(panes) {\n state.panes = { ...state.panes, ...panes };\n save();\n },\n setIdentity(identity) {\n state.identity = identity;\n save();\n },\n setWindow(key, next) {\n state.windows[key] = { ...state.windows[key], ...next };\n save();\n },\n };\n}\n"]}
@@ -0,0 +1,75 @@
1
+ /** Who is at the keyboard. Never in a project: see identity.ts. */
2
+ interface Identity {
3
+ name: string;
4
+ email?: string;
5
+ }
6
+ /** A remembered window rectangle. Absent = the app's default, centred. */
7
+ interface WindowBounds {
8
+ x?: number;
9
+ y?: number;
10
+ width: number;
11
+ height: number;
12
+ }
13
+ /** One helper window's remembered state. */
14
+ interface WindowState {
15
+ bounds?: WindowBounds;
16
+ pinned?: boolean;
17
+ }
18
+ /** Side panes: open/closed, and any dragged width. */
19
+ interface PaneState {
20
+ nav?: boolean;
21
+ inspector?: boolean;
22
+ navW?: number;
23
+ inspW?: number;
24
+ }
25
+ /**
26
+ * The part every app has.
27
+ *
28
+ * `place` is deliberately opaque: "open where you left off" is a family idea,
29
+ * but WHERE is a scene and a caret in one app and a box, a tab and a card in
30
+ * another. The shell stores it and clears it when the project changes, which is
31
+ * the one rule about it that is not app-specific.
32
+ */
33
+ interface AppSettingsCore<Place> {
34
+ recents: string[];
35
+ lastProject?: string;
36
+ place?: Place;
37
+ panes: PaneState;
38
+ identity?: Identity;
39
+ /** Keyed by whatever the app calls each window ("board", "search"). */
40
+ windows: Record<string, WindowState>;
41
+ }
42
+ type AppSettings<Place, App> = AppSettingsCore<Place> & {
43
+ app: App;
44
+ };
45
+ interface AppStoreOptions<Place, App> {
46
+ /** The app's userData directory. */
47
+ dir: string;
48
+ /** The file inside it. One per app; two apps never share. */
49
+ fileName?: string;
50
+ /** The app's own slice, as it should read on a first run. */
51
+ defaults: App;
52
+ /** How the panes should sit before anybody touches them. */
53
+ panes?: PaneState;
54
+ maxRecents?: number;
55
+ }
56
+ interface AppStore<Place, App> {
57
+ /** A copy, always: nothing outside can mutate the state behind the store's back. */
58
+ get(): AppSettings<Place, App>;
59
+ /** Merge into the app's own slice and save. */
60
+ patchApp(patch: Partial<App>): void;
61
+ /** Record an opened project: front of recents, deduped, capped. */
62
+ touchProject(path: string): void;
63
+ /** Drop one that failed to open (moved, deleted). */
64
+ forgetProject(path: string): void;
65
+ /** Where the author was. Compared before writing: this is called on every
66
+ * navigation and must not mean a disk write per click. */
67
+ setPlace(place: Place): void;
68
+ setPanes(panes: PaneState): void;
69
+ setIdentity(identity: Identity): void;
70
+ /** One window's bounds and/or pin, merged with what it already had. */
71
+ setWindow(key: string, state: WindowState): void;
72
+ }
73
+ declare function createAppStore<Place, App extends object>(opts: AppStoreOptions<Place, App>): AppStore<Place, App>;
74
+
75
+ export { type AppSettings, type AppSettingsCore, type AppStore, type AppStoreOptions, type Identity, type PaneState, type WindowBounds, type WindowState, createAppStore };
@@ -0,0 +1,75 @@
1
+ /** Who is at the keyboard. Never in a project: see identity.ts. */
2
+ interface Identity {
3
+ name: string;
4
+ email?: string;
5
+ }
6
+ /** A remembered window rectangle. Absent = the app's default, centred. */
7
+ interface WindowBounds {
8
+ x?: number;
9
+ y?: number;
10
+ width: number;
11
+ height: number;
12
+ }
13
+ /** One helper window's remembered state. */
14
+ interface WindowState {
15
+ bounds?: WindowBounds;
16
+ pinned?: boolean;
17
+ }
18
+ /** Side panes: open/closed, and any dragged width. */
19
+ interface PaneState {
20
+ nav?: boolean;
21
+ inspector?: boolean;
22
+ navW?: number;
23
+ inspW?: number;
24
+ }
25
+ /**
26
+ * The part every app has.
27
+ *
28
+ * `place` is deliberately opaque: "open where you left off" is a family idea,
29
+ * but WHERE is a scene and a caret in one app and a box, a tab and a card in
30
+ * another. The shell stores it and clears it when the project changes, which is
31
+ * the one rule about it that is not app-specific.
32
+ */
33
+ interface AppSettingsCore<Place> {
34
+ recents: string[];
35
+ lastProject?: string;
36
+ place?: Place;
37
+ panes: PaneState;
38
+ identity?: Identity;
39
+ /** Keyed by whatever the app calls each window ("board", "search"). */
40
+ windows: Record<string, WindowState>;
41
+ }
42
+ type AppSettings<Place, App> = AppSettingsCore<Place> & {
43
+ app: App;
44
+ };
45
+ interface AppStoreOptions<Place, App> {
46
+ /** The app's userData directory. */
47
+ dir: string;
48
+ /** The file inside it. One per app; two apps never share. */
49
+ fileName?: string;
50
+ /** The app's own slice, as it should read on a first run. */
51
+ defaults: App;
52
+ /** How the panes should sit before anybody touches them. */
53
+ panes?: PaneState;
54
+ maxRecents?: number;
55
+ }
56
+ interface AppStore<Place, App> {
57
+ /** A copy, always: nothing outside can mutate the state behind the store's back. */
58
+ get(): AppSettings<Place, App>;
59
+ /** Merge into the app's own slice and save. */
60
+ patchApp(patch: Partial<App>): void;
61
+ /** Record an opened project: front of recents, deduped, capped. */
62
+ touchProject(path: string): void;
63
+ /** Drop one that failed to open (moved, deleted). */
64
+ forgetProject(path: string): void;
65
+ /** Where the author was. Compared before writing: this is called on every
66
+ * navigation and must not mean a disk write per click. */
67
+ setPlace(place: Place): void;
68
+ setPanes(panes: PaneState): void;
69
+ setIdentity(identity: Identity): void;
70
+ /** One window's bounds and/or pin, merged with what it already had. */
71
+ setWindow(key: string, state: WindowState): void;
72
+ }
73
+ declare function createAppStore<Place, App extends object>(opts: AppStoreOptions<Place, App>): AppStore<Place, App>;
74
+
75
+ export { type AppSettings, type AppSettingsCore, type AppStore, type AppStoreOptions, type Identity, type PaneState, type WindowBounds, type WindowState, createAppStore };
@@ -0,0 +1,72 @@
1
+ import { readFileSync, mkdirSync, writeFileSync, renameSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ // src/app-store.ts
5
+ function createAppStore(opts) {
6
+ const file = join(opts.dir, opts.fileName ?? "app-settings.json");
7
+ const maxRecents = opts.maxRecents ?? 8;
8
+ let loaded = {};
9
+ try {
10
+ loaded = JSON.parse(readFileSync(file, "utf8"));
11
+ } catch {
12
+ }
13
+ const state = {
14
+ recents: [],
15
+ panes: opts.panes ?? {},
16
+ windows: {},
17
+ ...loaded,
18
+ // The app's slice merges FIELD BY FIELD, so a key added in a new version
19
+ // arrives with its default rather than being absent for everybody who has
20
+ // run the app before.
21
+ app: { ...opts.defaults, ...loaded.app ?? {} }
22
+ };
23
+ const save = () => {
24
+ try {
25
+ mkdirSync(opts.dir, { recursive: true });
26
+ const temp = `${file}.tmp`;
27
+ writeFileSync(temp, `${JSON.stringify(state, null, 2)}
28
+ `, "utf8");
29
+ renameSync(temp, file);
30
+ } catch {
31
+ }
32
+ };
33
+ return {
34
+ get: () => structuredClone(state),
35
+ patchApp(patch) {
36
+ Object.assign(state.app, patch);
37
+ save();
38
+ },
39
+ touchProject(path) {
40
+ if (state.lastProject !== path) delete state.place;
41
+ state.recents = [path, ...state.recents.filter((p) => p !== path)].slice(0, maxRecents);
42
+ state.lastProject = path;
43
+ save();
44
+ },
45
+ forgetProject(path) {
46
+ state.recents = state.recents.filter((p) => p !== path);
47
+ if (state.lastProject === path) delete state.lastProject;
48
+ save();
49
+ },
50
+ setPlace(place) {
51
+ if (JSON.stringify(state.place) === JSON.stringify(place)) return;
52
+ state.place = place;
53
+ save();
54
+ },
55
+ setPanes(panes) {
56
+ state.panes = { ...state.panes, ...panes };
57
+ save();
58
+ },
59
+ setIdentity(identity) {
60
+ state.identity = identity;
61
+ save();
62
+ },
63
+ setWindow(key, next) {
64
+ state.windows[key] = { ...state.windows[key], ...next };
65
+ save();
66
+ }
67
+ };
68
+ }
69
+
70
+ export { createAppStore };
71
+ //# sourceMappingURL=app-store.js.map
72
+ //# sourceMappingURL=app-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/app-store.ts"],"names":[],"mappings":"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","file":"app-store.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The user's settings for THIS app: a small JSON file in the app's userData.\n//\n// Every app in the family has one, and both of the two that exist wrote it\n// separately: the same tolerant read, the same merged defaults, the same capped\n// recents, the same panes, the same per-tool-window bounds and pin, the same\n// identity. That is the definition of shell work.\n//\n// WHAT BELONGS HERE is settings that follow the PERSON, not the project: where\n// their windows were, which panes they like open, what they are called, what\n// they had open last. Project content never comes near this file - it is not\n// version controlled, it is not shared, and losing it should cost an author\n// nothing but a re-arranged window.\n//\n// The shell owns the CORE it can reason about and hands the app one slice of\n// its own (`app`) for everything else - a theme value, a view mode, whatever it\n// has. Two stores in one file, one atomic write, so a crash cannot leave an app\n// with half its settings.\n//\n// Electron-free on purpose: it takes a directory. That keeps it unit-testable\n// and lets a test point it at a temp dir.\n// ---------------------------------------------------------------------------\n\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/** Who is at the keyboard. Never in a project: see identity.ts. */\nexport interface Identity { name: string; email?: string }\n\n/** A remembered window rectangle. Absent = the app's default, centred. */\nexport interface WindowBounds { x?: number; y?: number; width: number; height: number }\n\n/** One helper window's remembered state. */\nexport interface WindowState { bounds?: WindowBounds; pinned?: boolean }\n\n/** Side panes: open/closed, and any dragged width. */\nexport interface PaneState {\n nav?: boolean;\n inspector?: boolean;\n navW?: number;\n inspW?: number;\n}\n\n/**\n * The part every app has.\n *\n * `place` is deliberately opaque: \"open where you left off\" is a family idea,\n * but WHERE is a scene and a caret in one app and a box, a tab and a card in\n * another. The shell stores it and clears it when the project changes, which is\n * the one rule about it that is not app-specific.\n */\nexport interface AppSettingsCore<Place> {\n recents: string[];\n lastProject?: string;\n place?: Place;\n panes: PaneState;\n identity?: Identity;\n /** Keyed by whatever the app calls each window (\"board\", \"search\"). */\n windows: Record<string, WindowState>;\n}\n\nexport type AppSettings<Place, App> = AppSettingsCore<Place> & { app: App };\n\nexport interface AppStoreOptions<Place, App> {\n /** The app's userData directory. */\n dir: string;\n /** The file inside it. One per app; two apps never share. */\n fileName?: string;\n /** The app's own slice, as it should read on a first run. */\n defaults: App;\n /** How the panes should sit before anybody touches them. */\n panes?: PaneState;\n maxRecents?: number;\n}\n\nexport interface AppStore<Place, App> {\n /** A copy, always: nothing outside can mutate the state behind the store's back. */\n get(): AppSettings<Place, App>;\n /** Merge into the app's own slice and save. */\n patchApp(patch: Partial<App>): void;\n /** Record an opened project: front of recents, deduped, capped. */\n touchProject(path: string): void;\n /** Drop one that failed to open (moved, deleted). */\n forgetProject(path: string): void;\n /** Where the author was. Compared before writing: this is called on every\n * navigation and must not mean a disk write per click. */\n setPlace(place: Place): void;\n setPanes(panes: PaneState): void;\n setIdentity(identity: Identity): void;\n /** One window's bounds and/or pin, merged with what it already had. */\n setWindow(key: string, state: WindowState): void;\n}\n\nexport function createAppStore<Place, App extends object>(\n opts: AppStoreOptions<Place, App>,\n): AppStore<Place, App> {\n const file = join(opts.dir, opts.fileName ?? \"app-settings.json\");\n const maxRecents = opts.maxRecents ?? 8;\n\n let loaded: Partial<AppSettings<Place, App>> = {};\n try {\n loaded = JSON.parse(readFileSync(file, \"utf8\")) as Partial<AppSettings<Place, App>>;\n } catch {\n // First run, or a file we cannot read. Defaults, never a crash: settings\n // are not worth failing to start over.\n }\n\n const state: AppSettings<Place, App> = {\n recents: [],\n panes: opts.panes ?? {},\n windows: {},\n ...loaded,\n // The app's slice merges FIELD BY FIELD, so a key added in a new version\n // arrives with its default rather than being absent for everybody who has\n // run the app before.\n app: { ...opts.defaults, ...(loaded.app ?? {}) },\n };\n\n const save = (): void => {\n try {\n mkdirSync(opts.dir, { recursive: true });\n // Write beside, then rename: a rename is atomic on every platform this\n // ships to, so a crash mid-write cannot leave a half-written settings\n // file - which both apps' hand-written stores could.\n const temp = `${file}.tmp`;\n writeFileSync(temp, `${JSON.stringify(state, null, 2)}\\n`, \"utf8\");\n renameSync(temp, file);\n } catch {\n // A read-only home directory, a full disk: the app carries on with the\n // settings it has in memory rather than dying over a preference.\n }\n };\n\n return {\n get: () => structuredClone(state),\n patchApp(patch) {\n Object.assign(state.app, patch);\n save();\n },\n touchProject(path) {\n // A DIFFERENT project invalidates the remembered place; the same one must\n // keep it, or \"open where you left off\" could never work, since reopening\n // goes through here.\n if (state.lastProject !== path) delete state.place;\n state.recents = [path, ...state.recents.filter((p) => p !== path)].slice(0, maxRecents);\n state.lastProject = path;\n save();\n },\n forgetProject(path) {\n state.recents = state.recents.filter((p) => p !== path);\n if (state.lastProject === path) delete state.lastProject;\n save();\n },\n setPlace(place) {\n if (JSON.stringify(state.place) === JSON.stringify(place)) return;\n state.place = place;\n save();\n },\n setPanes(panes) {\n state.panes = { ...state.panes, ...panes };\n save();\n },\n setIdentity(identity) {\n state.identity = identity;\n save();\n },\n setWindow(key, next) {\n state.windows[key] = { ...state.windows[key], ...next };\n save();\n },\n };\n}\n"]}
@@ -0,0 +1,58 @@
1
+ // src/job.ts
2
+ var JOB_PROGRESS = "job:progress";
3
+ var defaultYield = () => new Promise((resolve) => {
4
+ setTimeout(resolve, 0);
5
+ });
6
+ function createJobHost(opts) {
7
+ const now = opts.now ?? (() => Date.now());
8
+ const handOver = opts.yieldToLoop ?? defaultYield;
9
+ const sliceMs = opts.sliceMs ?? 16;
10
+ const reportMs = opts.reportMs ?? 100;
11
+ const live = /* @__PURE__ */ new Map();
12
+ return {
13
+ running: (kind) => live.has(kind),
14
+ cancel(kind) {
15
+ const job = live.get(kind);
16
+ if (job) job.cancelled = true;
17
+ },
18
+ async start(kind, work) {
19
+ if (live.has(kind)) return { error: `a ${kind} job is already running` };
20
+ const job = { cancelled: false };
21
+ live.set(kind, job);
22
+ const started = now();
23
+ let lastYield = started;
24
+ let reported = false;
25
+ let lastReport = 0;
26
+ const ctx = {
27
+ get cancelled() {
28
+ return job.cancelled;
29
+ },
30
+ async step(done, total) {
31
+ const at = now();
32
+ if (!reported || at - lastReport >= reportMs || done >= total) {
33
+ reported = true;
34
+ lastReport = at;
35
+ opts.send(JOB_PROGRESS, { kind, done, total, elapsedMs: at - started });
36
+ }
37
+ if (at - lastYield >= sliceMs) {
38
+ lastYield = at;
39
+ await handOver();
40
+ }
41
+ }
42
+ };
43
+ try {
44
+ const value = await work(ctx);
45
+ return job.cancelled ? { cancelled: true, value } : { ok: true, value };
46
+ } catch (e) {
47
+ if (job.cancelled) return { cancelled: true };
48
+ return { error: e instanceof Error ? e.message : String(e) };
49
+ } finally {
50
+ live.delete(kind);
51
+ }
52
+ }
53
+ };
54
+ }
55
+
56
+ export { JOB_PROGRESS, createJobHost };
57
+ //# sourceMappingURL=chunk-MDGOIT3O.js.map
58
+ //# sourceMappingURL=chunk-MDGOIT3O.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/job.ts"],"names":[],"mappings":";AAwBO,IAAM,YAAA,GAAe;AAqD5B,IAAM,YAAA,GAAe,MACnB,IAAI,OAAA,CAAQ,CAAC,OAAA,KAAY;AAAE,EAAA,UAAA,CAAW,SAAS,CAAC,CAAA;AAAG,CAAC,CAAA;AAE/C,SAAS,cAAc,IAAA,EAA+B;AAC3D,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,KAAQ,MAAM,KAAK,GAAA,EAAI,CAAA;AACxC,EAAA,MAAM,QAAA,GAAW,KAAK,WAAA,IAAe,YAAA;AACrC,EAAA,MAAM,OAAA,GAAU,KAAK,OAAA,IAAW,EAAA;AAChC,EAAA,MAAM,QAAA,GAAW,KAAK,QAAA,IAAY,GAAA;AAClC,EAAA,MAAM,IAAA,uBAAW,GAAA,EAAoC;AAErD,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,CAAC,IAAA,KAAS,IAAA,CAAK,IAAI,IAAI,CAAA;AAAA,IAEhC,OAAO,IAAA,EAAM;AACX,MAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,IAAI,CAAA;AACzB,MAAA,IAAI,GAAA,MAAS,SAAA,GAAY,IAAA;AAAA,IAC3B,CAAA;AAAA,IAEA,MAAM,KAAA,CAAS,IAAA,EAAc,IAAA,EAA8D;AACzF,MAAA,IAAI,IAAA,CAAK,IAAI,IAAI,CAAA,SAAU,EAAE,KAAA,EAAO,CAAA,EAAA,EAAK,IAAI,CAAA,uBAAA,CAAA,EAA0B;AACvE,MAAA,MAAM,GAAA,GAAM,EAAE,SAAA,EAAW,KAAA,EAAM;AAC/B,MAAA,IAAA,CAAK,GAAA,CAAI,MAAM,GAAG,CAAA;AAElB,MAAA,MAAM,UAAU,GAAA,EAAI;AACpB,MAAA,IAAI,SAAA,GAAY,OAAA;AAIhB,MAAA,IAAI,QAAA,GAAW,KAAA;AACf,MAAA,IAAI,UAAA,GAAa,CAAA;AAEjB,MAAA,MAAM,GAAA,GAAkB;AAAA,QACtB,IAAI,SAAA,GAAY;AAAE,UAAA,OAAO,GAAA,CAAI,SAAA;AAAA,QAAW,CAAA;AAAA,QACxC,MAAM,IAAA,CAAK,IAAA,EAAc,KAAA,EAA8B;AACrD,UAAA,MAAM,KAAK,GAAA,EAAI;AACf,UAAA,IAAI,CAAC,QAAA,IAAY,EAAA,GAAK,UAAA,IAAc,QAAA,IAAY,QAAQ,KAAA,EAAO;AAC7D,YAAA,QAAA,GAAW,IAAA;AACX,YAAA,UAAA,GAAa,EAAA;AACb,YAAA,IAAA,CAAK,IAAA,CAAK,cAAc,EAAE,IAAA,EAAM,MAAM,KAAA,EAAO,SAAA,EAAW,EAAA,GAAK,OAAA,EAAS,CAAA;AAAA,UACxE;AACA,UAAA,IAAI,EAAA,GAAK,aAAa,OAAA,EAAS;AAC7B,YAAA,SAAA,GAAY,EAAA;AACZ,YAAA,MAAM,QAAA,EAAS;AAAA,UACjB;AAAA,QACF;AAAA,OACF;AAEA,MAAA,IAAI;AACF,QAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,GAAG,CAAA;AAC5B,QAAA,OAAO,GAAA,CAAI,SAAA,GAAY,EAAE,SAAA,EAAW,IAAA,EAAM,OAAM,GAAI,EAAE,EAAA,EAAI,IAAA,EAAM,KAAA,EAAM;AAAA,MACxE,SAAS,CAAA,EAAG;AAGV,QAAA,IAAI,GAAA,CAAI,SAAA,EAAW,OAAO,EAAE,WAAW,IAAA,EAAK;AAC5C,QAAA,OAAO,EAAE,OAAO,CAAA,YAAa,KAAA,GAAQ,EAAE,OAAA,GAAU,MAAA,CAAO,CAAC,CAAA,EAAE;AAAA,MAC7D,CAAA,SAAE;AACA,QAAA,IAAA,CAAK,OAAO,IAAI,CAAA;AAAA,MAClB;AAAA,IACF;AAAA,GACF;AACF","file":"chunk-MDGOIT3O.js","sourcesContent":["// ---------------------------------------------------------------------------\n// The long-job kit (main process): run a piece of work that takes long enough\n// to need a progress bar and a Cancel button, without freezing the app.\n//\n// The shape every editor in the family needs: coverage sweeps, big exports,\n// corpus runs. One kind of job at a time, progress reported to the windows\n// that asked for it, cancellation honoured at the next yield point, and a\n// partial result when the work can offer one.\n//\n// COOPERATIVE, NOT PARALLEL. The work still runs in the main process; it just\n// stops hogging it. `ctx.step()` is both the progress report and the yield:\n// the work hands the event loop back every few milliseconds, so IPC keeps\n// flowing, the renderer keeps painting, and Cancel is heard. The alternative\n// (a utilityProcess or a worker) buys true parallelism at the cost of\n// serialising the job's inputs and outputs across a process boundary, and a\n// second entry point per app. Reach for that only when the work is heavy\n// enough that a responsive main process is not enough.\n//\n// Electron is an OPTIONAL peer here: nothing in this file imports it. The host\n// supplies `send`, so the kit is testable headlessly and usable from any\n// message transport.\n// ---------------------------------------------------------------------------\n\n/** The channel a host should send progress on, and the shape it carries. */\nexport const JOB_PROGRESS = \"job:progress\";\n\nexport interface JobProgress {\n /** Which job this is about (\"coverage\"), so one listener can serve many. */\n kind: string;\n /** Units finished and expected. `total` 0 means \"no idea yet\". */\n done: number;\n total: number;\n /** Milliseconds since the job started, for elapsed + estimated remaining. */\n elapsedMs: number;\n}\n\n/** What the work is given: a cancellation flag and the one call that both\n * reports progress and yields. */\nexport interface JobContext {\n /** True once someone has cancelled. Check it to bail out early, or to\n * return a partial result rather than nothing. */\n readonly cancelled: boolean;\n /** Report progress and, when enough time has passed, hand the event loop\n * back. Await it: that await IS the yield. */\n step(done: number, total: number): Promise<void>;\n}\n\nexport type JobResult<T> =\n | { ok: true; value: T }\n | { cancelled: true; value?: T }\n | { error: string };\n\nexport interface JobHost {\n /** Run `work` as the job of this kind. Rejects a second start of the same\n * kind while one is in flight (the caller should have disabled the button;\n * this is the backstop). */\n start<T>(kind: string, work: (ctx: JobContext) => Promise<T>): Promise<JobResult<T>>;\n /** Ask the running job of this kind to stop. It stops at its next step(). */\n cancel(kind: string): void;\n running(kind: string): boolean;\n}\n\nexport interface JobHostOptions {\n /** Deliver a progress message to whoever is watching (typically\n * `window.webContents.send(JOB_PROGRESS, payload)` for each live window). */\n send: (channel: string, payload: JobProgress) => void;\n /** How long the work may hold the event loop between yields (default 16ms,\n * about one frame). Smaller is smoother and slower. */\n sliceMs?: number;\n /** How often a progress message may be sent (default 100ms). Progress is\n * for a human to read; flooding the channel just costs IPC. */\n reportMs?: number;\n /** Injectable clock + yield, for tests. */\n now?: () => number;\n yieldToLoop?: () => Promise<void>;\n}\n\nconst defaultYield = (): Promise<void> =>\n new Promise((resolve) => { setTimeout(resolve, 0); });\n\nexport function createJobHost(opts: JobHostOptions): JobHost {\n const now = opts.now ?? (() => Date.now());\n const handOver = opts.yieldToLoop ?? defaultYield;\n const sliceMs = opts.sliceMs ?? 16;\n const reportMs = opts.reportMs ?? 100;\n const live = new Map<string, { cancelled: boolean }>();\n\n return {\n running: (kind) => live.has(kind),\n\n cancel(kind) {\n const job = live.get(kind);\n if (job) job.cancelled = true;\n },\n\n async start<T>(kind: string, work: (ctx: JobContext) => Promise<T>): Promise<JobResult<T>> {\n if (live.has(kind)) return { error: `a ${kind} job is already running` };\n const job = { cancelled: false };\n live.set(kind, job);\n\n const started = now();\n let lastYield = started;\n // A flag, not a timestamp sentinel: on a clock that can legitimately\n // read 0, \"never reported yet\" and \"reported at 0\" must not be the\n // same state. The first step always reports, so the bar moves at once.\n let reported = false;\n let lastReport = 0;\n\n const ctx: JobContext = {\n get cancelled() { return job.cancelled; },\n async step(done: number, total: number): Promise<void> {\n const at = now();\n if (!reported || at - lastReport >= reportMs || done >= total) {\n reported = true;\n lastReport = at;\n opts.send(JOB_PROGRESS, { kind, done, total, elapsedMs: at - started });\n }\n if (at - lastYield >= sliceMs) {\n lastYield = at;\n await handOver();\n }\n },\n };\n\n try {\n const value = await work(ctx);\n return job.cancelled ? { cancelled: true, value } : { ok: true, value };\n } catch (e) {\n // A cancelled job that threw on the way out is still a cancellation:\n // the author asked it to stop, and an error report would be noise.\n if (job.cancelled) return { cancelled: true };\n return { error: e instanceof Error ? e.message : String(e) };\n } finally {\n live.delete(kind);\n }\n },\n };\n}\n"]}
@@ -0,0 +1,16 @@
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
+ };
8
+ var PANE_MENU = {
9
+ showNav: { label: "Show Navigator", accelerator: "CmdOrCtrl+1" },
10
+ showInspector: { label: "Show Inspector", accelerator: "CmdOrCtrl+2" },
11
+ resetView: { label: "Reset View" }
12
+ };
13
+
14
+ export { EDIT_MENU, PANE_MENU };
15
+ //# sourceMappingURL=chunk-R6RO5TXB.js.map
16
+ //# sourceMappingURL=chunk-R6RO5TXB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/menu.ts"],"names":[],"mappings":";AA2BO,IAAM,SAAA,GAKT;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;AACvC;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","file":"chunk-R6RO5TXB.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\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} = {\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};\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"]}