@wildwinter/app-shell 0.1.0 → 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.
- package/README.md +35 -0
- package/dist/about.css +76 -0
- package/dist/about.css.map +1 -0
- package/dist/anchored.css +50 -0
- package/dist/anchored.css.map +1 -0
- package/dist/app-store.cjs +74 -0
- package/dist/app-store.cjs.map +1 -0
- package/dist/app-store.d.cts +75 -0
- package/dist/app-store.d.ts +75 -0
- package/dist/app-store.js +72 -0
- package/dist/app-store.js.map +1 -0
- package/dist/chunk-MDGOIT3O.js +58 -0
- package/dist/chunk-MDGOIT3O.js.map +1 -0
- package/dist/chunk-R6RO5TXB.js +16 -0
- package/dist/chunk-R6RO5TXB.js.map +1 -0
- package/dist/comments.css +109 -0
- package/dist/comments.css.map +1 -0
- package/dist/confirm.css +62 -0
- package/dist/confirm.css.map +1 -0
- package/dist/identity.css +62 -0
- package/dist/identity.css.map +1 -0
- package/dist/index.cjs +1287 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +488 -1
- package/dist/index.d.ts +488 -1
- package/dist/index.js +1241 -1
- package/dist/index.js.map +1 -1
- package/dist/job.cjs +61 -0
- package/dist/job.cjs.map +1 -0
- package/dist/job.css +70 -0
- package/dist/job.css.map +1 -0
- package/dist/job.d.cts +56 -0
- package/dist/job.d.ts +56 -0
- package/dist/job.js +3 -0
- package/dist/job.js.map +1 -0
- package/dist/menu.cjs +19 -0
- package/dist/menu.cjs.map +1 -0
- package/dist/menu.d.cts +32 -0
- package/dist/menu.d.ts +32 -0
- package/dist/menu.js +3 -0
- package/dist/menu.js.map +1 -0
- package/dist/notes-editor.css +107 -0
- package/dist/notes-editor.css.map +1 -0
- package/dist/pane-shell.css +114 -0
- package/dist/pane-shell.css.map +1 -0
- package/dist/session.cjs +52 -0
- package/dist/session.cjs.map +1 -0
- package/dist/session.d.cts +79 -0
- package/dist/session.d.ts +79 -0
- package/dist/session.js +50 -0
- package/dist/session.js.map +1 -0
- package/dist/settings.css +266 -0
- package/dist/settings.css.map +1 -0
- package/dist/tool-window.cjs +79 -0
- package/dist/tool-window.cjs.map +1 -0
- package/dist/tool-window.css +60 -0
- package/dist/tool-window.css.map +1 -0
- package/dist/tool-window.d.cts +63 -0
- package/dist/tool-window.d.ts +63 -0
- package/dist/tool-window.js +73 -0
- package/dist/tool-window.js.map +1 -0
- package/dist/tooltip.css +22 -0
- package/dist/tooltip.css.map +1 -0
- package/dist/updater.cjs +315 -0
- package/dist/updater.cjs.map +1 -0
- package/dist/updater.d.cts +53 -0
- package/dist/updater.d.ts +53 -0
- package/dist/updater.js +306 -0
- package/dist/updater.js.map +1 -0
- package/dist/vc-status.cjs +77 -0
- package/dist/vc-status.cjs.map +1 -0
- package/dist/vc-status.d.cts +40 -0
- package/dist/vc-status.d.ts +40 -0
- package/dist/vc-status.js +71 -0
- package/dist/vc-status.js.map +1 -0
- package/dist/vc.css +41 -0
- package/dist/vc.css.map +1 -0
- package/package.json +101 -2
- package/dist/tokens.d.cts +0 -2
- package/dist/tokens.d.ts +0 -2
package/dist/job.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** The channel a host should send progress on, and the shape it carries. */
|
|
2
|
+
declare const JOB_PROGRESS = "job:progress";
|
|
3
|
+
interface JobProgress {
|
|
4
|
+
/** Which job this is about ("coverage"), so one listener can serve many. */
|
|
5
|
+
kind: string;
|
|
6
|
+
/** Units finished and expected. `total` 0 means "no idea yet". */
|
|
7
|
+
done: number;
|
|
8
|
+
total: number;
|
|
9
|
+
/** Milliseconds since the job started, for elapsed + estimated remaining. */
|
|
10
|
+
elapsedMs: number;
|
|
11
|
+
}
|
|
12
|
+
/** What the work is given: a cancellation flag and the one call that both
|
|
13
|
+
* reports progress and yields. */
|
|
14
|
+
interface JobContext {
|
|
15
|
+
/** True once someone has cancelled. Check it to bail out early, or to
|
|
16
|
+
* return a partial result rather than nothing. */
|
|
17
|
+
readonly cancelled: boolean;
|
|
18
|
+
/** Report progress and, when enough time has passed, hand the event loop
|
|
19
|
+
* back. Await it: that await IS the yield. */
|
|
20
|
+
step(done: number, total: number): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
type JobResult<T> = {
|
|
23
|
+
ok: true;
|
|
24
|
+
value: T;
|
|
25
|
+
} | {
|
|
26
|
+
cancelled: true;
|
|
27
|
+
value?: T;
|
|
28
|
+
} | {
|
|
29
|
+
error: string;
|
|
30
|
+
};
|
|
31
|
+
interface JobHost {
|
|
32
|
+
/** Run `work` as the job of this kind. Rejects a second start of the same
|
|
33
|
+
* kind while one is in flight (the caller should have disabled the button;
|
|
34
|
+
* this is the backstop). */
|
|
35
|
+
start<T>(kind: string, work: (ctx: JobContext) => Promise<T>): Promise<JobResult<T>>;
|
|
36
|
+
/** Ask the running job of this kind to stop. It stops at its next step(). */
|
|
37
|
+
cancel(kind: string): void;
|
|
38
|
+
running(kind: string): boolean;
|
|
39
|
+
}
|
|
40
|
+
interface JobHostOptions {
|
|
41
|
+
/** Deliver a progress message to whoever is watching (typically
|
|
42
|
+
* `window.webContents.send(JOB_PROGRESS, payload)` for each live window). */
|
|
43
|
+
send: (channel: string, payload: JobProgress) => void;
|
|
44
|
+
/** How long the work may hold the event loop between yields (default 16ms,
|
|
45
|
+
* about one frame). Smaller is smoother and slower. */
|
|
46
|
+
sliceMs?: number;
|
|
47
|
+
/** How often a progress message may be sent (default 100ms). Progress is
|
|
48
|
+
* for a human to read; flooding the channel just costs IPC. */
|
|
49
|
+
reportMs?: number;
|
|
50
|
+
/** Injectable clock + yield, for tests. */
|
|
51
|
+
now?: () => number;
|
|
52
|
+
yieldToLoop?: () => Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
declare function createJobHost(opts: JobHostOptions): JobHost;
|
|
55
|
+
|
|
56
|
+
export { JOB_PROGRESS, type JobContext, type JobHost, type JobHostOptions, type JobProgress, type JobResult, createJobHost };
|
package/dist/job.js
ADDED
package/dist/job.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"job.js"}
|
package/dist/menu.cjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/menu.ts
|
|
4
|
+
var EDIT_MENU = {
|
|
5
|
+
undo: { label: "Undo", accelerator: "CmdOrCtrl+Z" },
|
|
6
|
+
redo: { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z" },
|
|
7
|
+
duplicate: { label: "Duplicate", accelerator: "CmdOrCtrl+D" },
|
|
8
|
+
find: { label: "Find\u2026", accelerator: "CmdOrCtrl+F" }
|
|
9
|
+
};
|
|
10
|
+
var PANE_MENU = {
|
|
11
|
+
showNav: { label: "Show Navigator", accelerator: "CmdOrCtrl+1" },
|
|
12
|
+
showInspector: { label: "Show Inspector", accelerator: "CmdOrCtrl+2" },
|
|
13
|
+
resetView: { label: "Reset View" }
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
exports.EDIT_MENU = EDIT_MENU;
|
|
17
|
+
exports.PANE_MENU = PANE_MENU;
|
|
18
|
+
//# sourceMappingURL=menu.cjs.map
|
|
19
|
+
//# sourceMappingURL=menu.cjs.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":"menu.cjs","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"]}
|
package/dist/menu.d.cts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
interface MenuLabel {
|
|
2
|
+
label: string;
|
|
3
|
+
/** Electron accelerator string, when the item has one. */
|
|
4
|
+
accelerator?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The Edit menu items the family shares.
|
|
8
|
+
*
|
|
9
|
+
* Undo and Redo are here and the MECHANISM is not, which is the whole finding
|
|
10
|
+
* about undo: Patterpad's is ProseMirror's document history, Storyletter's is a
|
|
11
|
+
* replay of file bytes, and those have nothing in common but this. See
|
|
12
|
+
* `design/shared-shell.md`.
|
|
13
|
+
*
|
|
14
|
+
* The rule both apps discovered separately, and the reason these are spelled
|
|
15
|
+
* out rather than left as Electron's `role: "undo"`: the native role runs a DOM
|
|
16
|
+
* text-field undo, which knows nothing about either app's model and will
|
|
17
|
+
* silently undo the wrong thing.
|
|
18
|
+
*/
|
|
19
|
+
declare const EDIT_MENU: {
|
|
20
|
+
undo: MenuLabel;
|
|
21
|
+
redo: MenuLabel;
|
|
22
|
+
duplicate: MenuLabel;
|
|
23
|
+
find: MenuLabel;
|
|
24
|
+
};
|
|
25
|
+
/** The pane-frame menu items (a View menu, conventionally). */
|
|
26
|
+
declare const PANE_MENU: {
|
|
27
|
+
showNav: MenuLabel;
|
|
28
|
+
showInspector: MenuLabel;
|
|
29
|
+
resetView: MenuLabel;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export { EDIT_MENU, type MenuLabel, PANE_MENU };
|
package/dist/menu.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
interface MenuLabel {
|
|
2
|
+
label: string;
|
|
3
|
+
/** Electron accelerator string, when the item has one. */
|
|
4
|
+
accelerator?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The Edit menu items the family shares.
|
|
8
|
+
*
|
|
9
|
+
* Undo and Redo are here and the MECHANISM is not, which is the whole finding
|
|
10
|
+
* about undo: Patterpad's is ProseMirror's document history, Storyletter's is a
|
|
11
|
+
* replay of file bytes, and those have nothing in common but this. See
|
|
12
|
+
* `design/shared-shell.md`.
|
|
13
|
+
*
|
|
14
|
+
* The rule both apps discovered separately, and the reason these are spelled
|
|
15
|
+
* out rather than left as Electron's `role: "undo"`: the native role runs a DOM
|
|
16
|
+
* text-field undo, which knows nothing about either app's model and will
|
|
17
|
+
* silently undo the wrong thing.
|
|
18
|
+
*/
|
|
19
|
+
declare const EDIT_MENU: {
|
|
20
|
+
undo: MenuLabel;
|
|
21
|
+
redo: MenuLabel;
|
|
22
|
+
duplicate: MenuLabel;
|
|
23
|
+
find: MenuLabel;
|
|
24
|
+
};
|
|
25
|
+
/** The pane-frame menu items (a View menu, conventionally). */
|
|
26
|
+
declare const PANE_MENU: {
|
|
27
|
+
showNav: MenuLabel;
|
|
28
|
+
showInspector: MenuLabel;
|
|
29
|
+
resetView: MenuLabel;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export { EDIT_MENU, type MenuLabel, PANE_MENU };
|
package/dist/menu.js
ADDED
package/dist/menu.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"menu.js"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/* src/notes-editor.css */
|
|
2
|
+
.shell-notes {
|
|
3
|
+
width: min(560px, 92vw);
|
|
4
|
+
border: 1px solid var(--line);
|
|
5
|
+
border-radius: var(--radius-md);
|
|
6
|
+
background: var(--surface);
|
|
7
|
+
color: var(--ink);
|
|
8
|
+
padding: 16px 18px;
|
|
9
|
+
box-shadow: var(--shadow-2, var(--shadow-1));
|
|
10
|
+
}
|
|
11
|
+
.shell-notes::backdrop {
|
|
12
|
+
background: rgba(0, 0, 0, 0.28);
|
|
13
|
+
}
|
|
14
|
+
.shell-notes-title {
|
|
15
|
+
font: 600 0.95rem/1.3 var(--font-ui);
|
|
16
|
+
margin-bottom: 10px;
|
|
17
|
+
}
|
|
18
|
+
.shell-notes-body {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
gap: 10px;
|
|
22
|
+
max-height: 60vh;
|
|
23
|
+
overflow-y: auto;
|
|
24
|
+
}
|
|
25
|
+
.shell-notes-caption {
|
|
26
|
+
font: 0.72rem/1 var(--font-ui);
|
|
27
|
+
color: var(--muted);
|
|
28
|
+
text-transform: uppercase;
|
|
29
|
+
letter-spacing: 0.04em;
|
|
30
|
+
}
|
|
31
|
+
.shell-notes-class {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
gap: 4px;
|
|
35
|
+
}
|
|
36
|
+
.shell-notes-text {
|
|
37
|
+
width: 100%;
|
|
38
|
+
box-sizing: border-box;
|
|
39
|
+
resize: vertical;
|
|
40
|
+
border: 1px solid var(--line-soft);
|
|
41
|
+
border-radius: var(--radius-sm);
|
|
42
|
+
background: var(--card);
|
|
43
|
+
color: var(--ink);
|
|
44
|
+
font: 0.85rem/1.45 var(--font-read, var(--font-ui));
|
|
45
|
+
padding: 6px 8px;
|
|
46
|
+
}
|
|
47
|
+
.shell-notes-text:focus {
|
|
48
|
+
outline: none;
|
|
49
|
+
border-color: var(--accent);
|
|
50
|
+
}
|
|
51
|
+
.shell-notes-inherited {
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-direction: column;
|
|
54
|
+
gap: 6px;
|
|
55
|
+
padding-bottom: 4px;
|
|
56
|
+
border-bottom: 1px solid var(--line-soft);
|
|
57
|
+
}
|
|
58
|
+
.shell-notes-level {
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
gap: 2px;
|
|
62
|
+
}
|
|
63
|
+
.shell-notes-level-label {
|
|
64
|
+
font: 0.72rem/1 var(--font-ui);
|
|
65
|
+
color: var(--muted);
|
|
66
|
+
}
|
|
67
|
+
.shell-notes-line {
|
|
68
|
+
font: 0.82rem/1.4 var(--font-read, var(--font-ui));
|
|
69
|
+
color: var(--muted);
|
|
70
|
+
}
|
|
71
|
+
.shell-notes-cls {
|
|
72
|
+
font: 0.68rem/1 var(--font-ui);
|
|
73
|
+
color: var(--muted);
|
|
74
|
+
border: 1px solid var(--line-soft);
|
|
75
|
+
border-radius: 999px;
|
|
76
|
+
padding: 1px 6px;
|
|
77
|
+
margin-right: 6px;
|
|
78
|
+
}
|
|
79
|
+
.shell-notes-hint {
|
|
80
|
+
font: 0.72rem/1 var(--font-ui);
|
|
81
|
+
color: var(--muted);
|
|
82
|
+
margin: 0;
|
|
83
|
+
}
|
|
84
|
+
.shell-notes-add {
|
|
85
|
+
font: 0.78rem/1 var(--font-ui);
|
|
86
|
+
align-self: flex-start;
|
|
87
|
+
}
|
|
88
|
+
.shell-notes-actions {
|
|
89
|
+
display: flex;
|
|
90
|
+
justify-content: flex-end;
|
|
91
|
+
margin-top: 12px;
|
|
92
|
+
}
|
|
93
|
+
.shell-notes-btn {
|
|
94
|
+
font: 0.8rem/1 var(--font-ui);
|
|
95
|
+
padding: 6px 14px;
|
|
96
|
+
border-radius: var(--radius-sm);
|
|
97
|
+
border: 1px solid var(--line);
|
|
98
|
+
background: var(--card);
|
|
99
|
+
color: var(--ink);
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
}
|
|
102
|
+
.shell-notes-btn.primary {
|
|
103
|
+
background: var(--accent);
|
|
104
|
+
border-color: var(--accent);
|
|
105
|
+
color: var(--on-accent, #fff);
|
|
106
|
+
}
|
|
107
|
+
/*# sourceMappingURL=notes-editor.css.map */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/notes-editor.css"],"sourcesContent":["/* The documentation-notes editor (notes-editor.ts): a modal, because notes are\n prose and the popover Patterpad tried first was too cramped. */\n.shell-notes {\n width: min(560px, 92vw);\n border: 1px solid var(--line); border-radius: var(--radius-md);\n background: var(--surface); color: var(--ink);\n padding: 16px 18px;\n box-shadow: var(--shadow-2, var(--shadow-1));\n}\n.shell-notes::backdrop { background: rgba(0, 0, 0, 0.28); }\n.shell-notes-title { font: 600 0.95rem/1.3 var(--font-ui); margin-bottom: 10px; }\n.shell-notes-body { display: flex; flex-direction: column; gap: 10px; max-height: 60vh; overflow-y: auto; }\n.shell-notes-caption {\n font: 0.72rem/1 var(--font-ui); color: var(--muted);\n text-transform: uppercase; letter-spacing: 0.04em;\n}\n.shell-notes-class { display: flex; flex-direction: column; gap: 4px; }\n.shell-notes-text {\n width: 100%; box-sizing: border-box; resize: vertical;\n border: 1px solid var(--line-soft); border-radius: var(--radius-sm);\n background: var(--card); color: var(--ink);\n font: 0.85rem/1.45 var(--font-read, var(--font-ui)); padding: 6px 8px;\n}\n.shell-notes-text:focus { outline: none; border-color: var(--accent); }\n/* Inherited notes read as CONTEXT rather than as fields: no box, no cursor. */\n.shell-notes-inherited {\n display: flex; flex-direction: column; gap: 6px;\n padding-bottom: 4px; border-bottom: 1px solid var(--line-soft);\n}\n.shell-notes-level { display: flex; flex-direction: column; gap: 2px; }\n.shell-notes-level-label { font: 0.72rem/1 var(--font-ui); color: var(--muted); }\n.shell-notes-line { font: 0.82rem/1.4 var(--font-read, var(--font-ui)); color: var(--muted); }\n.shell-notes-cls {\n font: 0.68rem/1 var(--font-ui); color: var(--muted);\n border: 1px solid var(--line-soft); border-radius: 999px;\n padding: 1px 6px; margin-right: 6px;\n}\n.shell-notes-hint { font: 0.72rem/1 var(--font-ui); color: var(--muted); margin: 0; }\n.shell-notes-add { font: 0.78rem/1 var(--font-ui); align-self: flex-start; }\n.shell-notes-actions { display: flex; justify-content: flex-end; margin-top: 12px; }\n.shell-notes-btn {\n font: 0.8rem/1 var(--font-ui); padding: 6px 14px;\n border-radius: var(--radius-sm); border: 1px solid var(--line);\n background: var(--card); color: var(--ink); cursor: pointer;\n}\n.shell-notes-btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent, #fff); }\n"],"mappings":";AAEA,CAAC;AACC,SAAO,IAAI,KAAK,EAAE;AAClB,UAAQ,IAAI,MAAM,IAAI;AAAS,iBAAe,IAAI;AAClD,cAAY,IAAI;AAAY,SAAO,IAAI;AACvC,WAAS,KAAK;AACd,cAAY,IAAI,UAAU,EAAE,IAAI;AAClC;AACA,CAPC,WAOW;AAAa,cAAY,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;AAAO;AAC1D,CAAC;AAAoB,QAAM,IAAI,OAAO,CAAC,IAAI,IAAI;AAAY,iBAAe;AAAM;AAChF,CAAC;AAAmB,WAAS;AAAM,kBAAgB;AAAQ,OAAK;AAAM,cAAY;AAAM,cAAY;AAAM;AAC1G,CAAC;AACC,QAAM,OAAO,CAAC,EAAE,IAAI;AAAY,SAAO,IAAI;AAC3C,kBAAgB;AAAW,kBAAgB;AAC7C;AACA,CAAC;AAAoB,WAAS;AAAM,kBAAgB;AAAQ,OAAK;AAAK;AACtE,CAAC;AACC,SAAO;AAAM,cAAY;AAAY,UAAQ;AAC7C,UAAQ,IAAI,MAAM,IAAI;AAAc,iBAAe,IAAI;AACvD,cAAY,IAAI;AAAS,SAAO,IAAI;AACpC,QAAM,OAAO,CAAC,KAAK,IAAI,WAAW,EAAE,IAAI;AAAa,WAAS,IAAI;AACpE;AACA,CANC,gBAMgB;AAAS,WAAS;AAAM,gBAAc,IAAI;AAAW;AAEtE,CAAC;AACC,WAAS;AAAM,kBAAgB;AAAQ,OAAK;AAC5C,kBAAgB;AAAK,iBAAe,IAAI,MAAM,IAAI;AACpD;AACA,CAAC;AAAoB,WAAS;AAAM,kBAAgB;AAAQ,OAAK;AAAK;AACtE,CAAC;AAA0B,QAAM,OAAO,CAAC,EAAE,IAAI;AAAY,SAAO,IAAI;AAAU;AAChF,CAAC;AAAmB,QAAM,OAAO,CAAC,IAAI,IAAI,WAAW,EAAE,IAAI;AAAa,SAAO,IAAI;AAAU;AAC7F,CAAC;AACC,QAAM,OAAO,CAAC,EAAE,IAAI;AAAY,SAAO,IAAI;AAC3C,UAAQ,IAAI,MAAM,IAAI;AAAc,iBAAe;AACnD,WAAS,IAAI;AAAK,gBAAc;AAClC;AACA,CAAC;AAAmB,QAAM,OAAO,CAAC,EAAE,IAAI;AAAY,SAAO,IAAI;AAAU,UAAQ;AAAG;AACpF,CAAC;AAAkB,QAAM,OAAO,CAAC,EAAE,IAAI;AAAY,cAAY;AAAY;AAC3E,CAAC;AAAsB,WAAS;AAAM,mBAAiB;AAAU,cAAY;AAAM;AACnF,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,eAKe,CAAC;AAAU,cAAY,IAAI;AAAW,gBAAc,IAAI;AAAW,SAAO,IAAI,WAAW,EAAE;AAAO;","names":[]}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/* src/pane-shell.css */
|
|
2
|
+
.pane-shell {
|
|
3
|
+
flex: 1;
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
min-height: 0;
|
|
7
|
+
}
|
|
8
|
+
.topbar {
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
gap: 10px;
|
|
12
|
+
padding: 8px 14px;
|
|
13
|
+
border-bottom: 1px solid var(--line-soft, var(--line));
|
|
14
|
+
background: var(--surface);
|
|
15
|
+
}
|
|
16
|
+
.topbar-lead {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 8px;
|
|
20
|
+
min-width: 0;
|
|
21
|
+
}
|
|
22
|
+
.topbar-spacer {
|
|
23
|
+
flex: 1;
|
|
24
|
+
}
|
|
25
|
+
.topbar-trail {
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
gap: 10px;
|
|
29
|
+
}
|
|
30
|
+
.pane-toggle {
|
|
31
|
+
color: var(--muted);
|
|
32
|
+
font: inherit;
|
|
33
|
+
font-size: 0.95rem;
|
|
34
|
+
background: none;
|
|
35
|
+
border: 0;
|
|
36
|
+
border-radius: var(--radius-sm);
|
|
37
|
+
padding: 2px 8px;
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
}
|
|
40
|
+
.pane-toggle:hover {
|
|
41
|
+
color: var(--accent);
|
|
42
|
+
}
|
|
43
|
+
.panes {
|
|
44
|
+
--nav-open-w: 14rem;
|
|
45
|
+
--insp-open-w: 22rem;
|
|
46
|
+
--nav-w: var(--nav-open-w);
|
|
47
|
+
--insp-w: var(--insp-open-w);
|
|
48
|
+
position: relative;
|
|
49
|
+
flex: 1;
|
|
50
|
+
min-height: 0;
|
|
51
|
+
display: grid;
|
|
52
|
+
grid-template-columns: var(--nav-w) minmax(0, 1fr) var(--insp-w);
|
|
53
|
+
transition: grid-template-columns var(--dur-panel) var(--ease-standard);
|
|
54
|
+
}
|
|
55
|
+
.panes.no-nav {
|
|
56
|
+
--nav-w: 0px;
|
|
57
|
+
}
|
|
58
|
+
.panes.no-inspector {
|
|
59
|
+
--insp-w: 0px;
|
|
60
|
+
}
|
|
61
|
+
.pane {
|
|
62
|
+
overflow: hidden;
|
|
63
|
+
min-height: 0;
|
|
64
|
+
}
|
|
65
|
+
.pane-inner {
|
|
66
|
+
height: 100%;
|
|
67
|
+
overflow: auto;
|
|
68
|
+
}
|
|
69
|
+
.pane-nav .pane-inner {
|
|
70
|
+
width: var(--nav-open-w);
|
|
71
|
+
}
|
|
72
|
+
.pane-inspector .pane-inner {
|
|
73
|
+
width: var(--insp-open-w);
|
|
74
|
+
}
|
|
75
|
+
.pane-centre {
|
|
76
|
+
min-width: 0;
|
|
77
|
+
min-height: 0;
|
|
78
|
+
overflow: auto;
|
|
79
|
+
}
|
|
80
|
+
.panes > .pane {
|
|
81
|
+
transition: opacity var(--dur-panel) var(--ease-standard);
|
|
82
|
+
}
|
|
83
|
+
.panes.no-nav > .pane-nav,
|
|
84
|
+
.panes.no-inspector > .pane-inspector {
|
|
85
|
+
opacity: 0;
|
|
86
|
+
pointer-events: none;
|
|
87
|
+
}
|
|
88
|
+
.pane-resizer {
|
|
89
|
+
position: absolute;
|
|
90
|
+
top: 0;
|
|
91
|
+
bottom: 0;
|
|
92
|
+
width: 7px;
|
|
93
|
+
z-index: 6;
|
|
94
|
+
cursor: col-resize;
|
|
95
|
+
touch-action: none;
|
|
96
|
+
}
|
|
97
|
+
.pane-resizer-nav {
|
|
98
|
+
left: calc(var(--nav-w) - 3px);
|
|
99
|
+
}
|
|
100
|
+
.pane-resizer-insp {
|
|
101
|
+
right: calc(var(--insp-w) - 3px);
|
|
102
|
+
}
|
|
103
|
+
.panes.no-nav .pane-resizer-nav,
|
|
104
|
+
.panes.no-inspector .pane-resizer-insp {
|
|
105
|
+
display: none;
|
|
106
|
+
}
|
|
107
|
+
body.pane-resizing {
|
|
108
|
+
cursor: col-resize;
|
|
109
|
+
user-select: none;
|
|
110
|
+
}
|
|
111
|
+
body.pane-resizing .panes {
|
|
112
|
+
transition: none;
|
|
113
|
+
}
|
|
114
|
+
/*# sourceMappingURL=pane-shell.css.map */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/pane-shell.css"],"sourcesContent":["/* ============================================================================\n The pane-shell frame: top bar + nav | centre | inspector grid with collapse\n and drag-resize. Reads the shared token grammar (tokens.css) and the app's\n palette vars (--surface / --ink / --muted / --line / --accent, and an\n optional --line-soft). App-specifics stay in the host: the window-drag region\n (-webkit-app-region) and traffic-light padding, if the app is frameless, are\n applied by the host over .topbar; the host also fills the pane bodies and the\n top-bar lead/trail slots.\n ============================================================================ */\n\n.pane-shell { flex: 1; display: flex; flex-direction: column; min-height: 0; }\n\n.topbar {\n display: flex; align-items: center; gap: 10px;\n padding: 8px 14px;\n border-bottom: 1px solid var(--line-soft, var(--line));\n background: var(--surface);\n}\n.topbar-lead { display: flex; align-items: center; gap: 8px; min-width: 0; }\n.topbar-spacer { flex: 1; }\n.topbar-trail { display: flex; align-items: center; gap: 10px; }\n.pane-toggle {\n color: var(--muted); font: inherit; font-size: 0.95rem;\n background: none; border: 0; border-radius: var(--radius-sm); padding: 2px 8px; cursor: pointer;\n}\n.pane-toggle:hover { color: var(--accent); }\n\n.panes {\n --nav-open-w: 14rem; --insp-open-w: 22rem;\n --nav-w: var(--nav-open-w); --insp-w: var(--insp-open-w);\n position: relative; flex: 1; min-height: 0;\n display: grid; grid-template-columns: var(--nav-w) minmax(0, 1fr) var(--insp-w);\n transition: grid-template-columns var(--dur-panel) var(--ease-standard);\n}\n.panes.no-nav { --nav-w: 0px; }\n.panes.no-inspector { --insp-w: 0px; }\n\n/* Side panes clip; their inner holds a fixed width so a collapse wipes cleanly\n rather than reflowing while the column animates to 0. */\n.pane { overflow: hidden; min-height: 0; }\n.pane-inner { height: 100%; overflow: auto; }\n.pane-nav .pane-inner { width: var(--nav-open-w); }\n.pane-inspector .pane-inner { width: var(--insp-open-w); }\n.pane-centre { min-width: 0; min-height: 0; overflow: auto; }\n\n.panes > .pane { transition: opacity var(--dur-panel) var(--ease-standard); }\n.panes.no-nav > .pane-nav, .panes.no-inspector > .pane-inspector { opacity: 0; pointer-events: none; }\n\n/* Drag seams over each side pane's inner edge, tracking the grid columns. */\n.pane-resizer { position: absolute; top: 0; bottom: 0; width: 7px; z-index: 6; cursor: col-resize; touch-action: none; }\n.pane-resizer-nav { left: calc(var(--nav-w) - 3px); }\n.pane-resizer-insp { right: calc(var(--insp-w) - 3px); }\n.panes.no-nav .pane-resizer-nav, .panes.no-inspector .pane-resizer-insp { display: none; }\nbody.pane-resizing { cursor: col-resize; user-select: none; }\nbody.pane-resizing .panes { transition: none; } /* 1:1 feel: suspend the grid tween mid-drag */\n"],"mappings":";AAUA,CAAC;AAAa,QAAM;AAAG,WAAS;AAAM,kBAAgB;AAAQ,cAAY;AAAG;AAE7E,CAAC;AACC,WAAS;AAAM,eAAa;AAAQ,OAAK;AACzC,WAAS,IAAI;AACb,iBAAe,IAAI,MAAM,IAAI,WAAW,EAAE,IAAI;AAC9C,cAAY,IAAI;AAClB;AACA,CAAC;AAAc,WAAS;AAAM,eAAa;AAAQ,OAAK;AAAK,aAAW;AAAG;AAC3E,CAAC;AAAgB,QAAM;AAAG;AAC1B,CAAC;AAAe,WAAS;AAAM,eAAa;AAAQ,OAAK;AAAM;AAC/D,CAAC;AACC,SAAO,IAAI;AAAU,QAAM;AAAS,aAAW;AAC/C,cAAY;AAAM,UAAQ;AAAG,iBAAe,IAAI;AAAc,WAAS,IAAI;AAAK,UAAQ;AAC1F;AACA,CAJC,WAIW;AAAS,SAAO,IAAI;AAAW;AAE3C,CAAC;AACC,gBAAc;AAAO,iBAAe;AACpC,WAAS,IAAI;AAAe,YAAU,IAAI;AAC1C,YAAU;AAAU,QAAM;AAAG,cAAY;AACzC,WAAS;AAAM,yBAAuB,IAAI,SAAS,OAAO,CAAC,EAAE,KAAK,IAAI;AACtE,cAAY,sBAAsB,IAAI,aAAa,IAAI;AACzD;AACA,CAPC,KAOK,CAAC;AAAS,WAAS;AAAK;AAC9B,CARC,KAQK,CAAC;AAAe,YAAU;AAAK;AAIrC,CAAC;AAAO,YAAU;AAAQ,cAAY;AAAG;AACzC,CAAC;AAAa,UAAQ;AAAM,YAAU;AAAM;AAC5C,CAAC,SAAS,CADT;AACuB,SAAO,IAAI;AAAe;AAClD,CAAC,eAAe,CAFf;AAE6B,SAAO,IAAI;AAAgB;AACzD,CAAC;AAAc,aAAW;AAAG,cAAY;AAAG,YAAU;AAAM;AAE5D,CAlBC,MAkBM,EAAE,CANR;AAMgB,cAAY,QAAQ,IAAI,aAAa,IAAI;AAAkB;AAC5E,CAnBC,KAmBK,CAZC,OAYO,EAAE,CALf;AAK0B,CAnB1B,KAmBgC,CAX1B,aAWwC,EAAE,CAJhD;AAIkE,WAAS;AAAG,kBAAgB;AAAM;AAGrG,CAAC;AAAe,YAAU;AAAU,OAAK;AAAG,UAAQ;AAAG,SAAO;AAAK,WAAS;AAAG,UAAQ;AAAY,gBAAc;AAAM;AACvH,CAAC;AAAmB,QAAM,KAAK,IAAI,SAAS,EAAE;AAAM;AACpD,CAAC;AAAoB,SAAO,KAAK,IAAI,UAAU,EAAE;AAAM;AACvD,CAzBC,KAyBK,CAlBC,OAkBO,CAFb;AAEgC,CAzBhC,KAyBsC,CAjBhC,aAiB8C,CADpD;AACyE,WAAS;AAAM;AACzF,IAAI,CAAC;AAAgB,UAAQ;AAAY,eAAa;AAAM;AAC5D,IAAI,CADC,cACc,CA3BlB;AA2B2B,cAAY;AAAM;","names":[]}
|
package/dist/session.cjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var path = require('path');
|
|
4
|
+
|
|
5
|
+
// src/session.ts
|
|
6
|
+
function createProjectSession(opts) {
|
|
7
|
+
let session;
|
|
8
|
+
const satellites = [...opts.satellites ?? []];
|
|
9
|
+
const invalidateSatellites = () => {
|
|
10
|
+
for (const sat of satellites) {
|
|
11
|
+
sat.clear?.();
|
|
12
|
+
const win = sat.window();
|
|
13
|
+
if (win && !win.isDestroyed()) win.webContents.send(sat.channel);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
current: () => session,
|
|
18
|
+
openAt(path) {
|
|
19
|
+
const opened = opts.open(path);
|
|
20
|
+
if ("error" in opened) {
|
|
21
|
+
opts.store.forgetProject(path);
|
|
22
|
+
opts.refreshMenu?.();
|
|
23
|
+
return opened;
|
|
24
|
+
}
|
|
25
|
+
if (session !== void 0) opts.close?.(session);
|
|
26
|
+
session = opened.session;
|
|
27
|
+
opts.store.touchProject(opened.root);
|
|
28
|
+
invalidateSatellites();
|
|
29
|
+
opts.refreshMenu?.();
|
|
30
|
+
return opened.reply;
|
|
31
|
+
},
|
|
32
|
+
isKnownPath(path$1) {
|
|
33
|
+
const s = opts.store.get();
|
|
34
|
+
const known = new Set(
|
|
35
|
+
[s.lastProject, ...s.recents].filter((p) => typeof p === "string" && p !== "").map((p) => path.resolve(p))
|
|
36
|
+
);
|
|
37
|
+
return known.has(path.resolve(path$1));
|
|
38
|
+
},
|
|
39
|
+
addSatellite(satellite) {
|
|
40
|
+
satellites.push(satellite);
|
|
41
|
+
return () => {
|
|
42
|
+
const i = satellites.indexOf(satellite);
|
|
43
|
+
if (i >= 0) satellites.splice(i, 1);
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
invalidateSatellites
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
exports.createProjectSession = createProjectSession;
|
|
51
|
+
//# sourceMappingURL=session.cjs.map
|
|
52
|
+
//# sourceMappingURL=session.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/session.ts"],"names":["path","resolve"],"mappings":";;;;;AAwGO,SAAS,qBAA2B,IAAA,EAAyD;AAClG,EAAA,IAAI,OAAA;AACJ,EAAA,MAAM,aAA0B,CAAC,GAAI,IAAA,CAAK,UAAA,IAAc,EAAG,CAAA;AAE3D,EAAA,MAAM,uBAAuB,MAAY;AACvC,IAAA,KAAA,MAAW,OAAO,UAAA,EAAY;AAC5B,MAAA,GAAA,CAAI,KAAA,IAAQ;AACZ,MAAA,MAAM,GAAA,GAAM,IAAI,MAAA,EAAO;AACvB,MAAA,IAAI,GAAA,IAAO,CAAC,GAAA,CAAI,WAAA,IAAe,GAAA,CAAI,WAAA,CAAY,IAAA,CAAK,GAAA,CAAI,OAAO,CAAA;AAAA,IACjE;AAAA,EACF,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,SAAS,MAAM,OAAA;AAAA,IAEf,OAAO,IAAA,EAAM;AACX,MAAA,MAAM,MAAA,GAAS,IAAA,CAAK,IAAA,CAAK,IAAI,CAAA;AAC7B,MAAA,IAAI,WAAW,MAAA,EAAQ;AACrB,QAAA,IAAA,CAAK,KAAA,CAAM,cAAc,IAAI,CAAA;AAC7B,QAAA,IAAA,CAAK,WAAA,IAAc;AACnB,QAAA,OAAO,MAAA;AAAA,MACT;AACA,MAAA,IAAI,OAAA,KAAY,MAAA,EAAW,IAAA,CAAK,KAAA,GAAQ,OAAO,CAAA;AAC/C,MAAA,OAAA,GAAU,MAAA,CAAO,OAAA;AACjB,MAAA,IAAA,CAAK,KAAA,CAAM,YAAA,CAAa,MAAA,CAAO,IAAI,CAAA;AACnC,MAAA,oBAAA,EAAqB;AACrB,MAAA,IAAA,CAAK,WAAA,IAAc;AACnB,MAAA,OAAO,MAAA,CAAO,KAAA;AAAA,IAChB,CAAA;AAAA,IAEA,YAAYA,MAAA,EAAM;AAChB,MAAA,MAAM,CAAA,GAAI,IAAA,CAAK,KAAA,CAAM,GAAA,EAAI;AACzB,MAAA,MAAM,QAAQ,IAAI,GAAA;AAAA,QAChB,CAAC,EAAE,WAAA,EAAa,GAAG,EAAE,OAAO,CAAA,CAAE,OAAO,CAAC,CAAA,KAAmB,OAAO,CAAA,KAAM,QAAA,IAAY,MAAM,EAAE,CAAA,CAAE,IAAI,CAAC,CAAA,KAAMC,YAAA,CAAQ,CAAC,CAAC;AAAA,OACnH;AACA,MAAA,OAAO,KAAA,CAAM,GAAA,CAAIA,YAAA,CAAQD,MAAI,CAAC,CAAA;AAAA,IAChC,CAAA;AAAA,IAEA,aAAa,SAAA,EAAW;AACtB,MAAA,UAAA,CAAW,KAAK,SAAS,CAAA;AACzB,MAAA,OAAO,MAAM;AACX,QAAA,MAAM,CAAA,GAAI,UAAA,CAAW,OAAA,CAAQ,SAAS,CAAA;AACtC,QAAA,IAAI,CAAA,IAAK,CAAA,EAAG,UAAA,CAAW,MAAA,CAAO,GAAG,CAAC,CAAA;AAAA,MACpC,CAAA;AAAA,IACF,CAAA;AAAA,IAEA;AAAA,GACF;AACF","file":"session.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// The project session: what happens around opening a project, as opposed to\n// the opening itself.\n//\n// Both apps have a function - `openAt` here, `openAndRecord` there - and under\n// two different names they do the same six things in the same order:\n//\n// 1. Ask the app to open it, which is the only app-specific step.\n// 2. On failure, drop it from recents (a project that has moved should stop\n// being offered) and rebuild the menu.\n// 3. On success, finish with the outgoing session.\n// 4. Record the opened ROOT in the store, which also clears the remembered\n// place when it is a different project.\n// 5. Invalidate everything main was caching FOR THE OLD PROJECT, and tell\n// each satellite tool window that the ground moved.\n// 6. Rebuild the menu, so File > Open Recent is current.\n//\n// Step 5 is the one worth extracting on its own. Both apps hand-wire it per\n// window - a cached result nulled, then `if (win && !win.isDestroyed())\n// win.webContents.send(...)` - Storyletter three times, Patterpad twice, and\n// every new tool window is another chance to forget one and leave a window\n// describing a project that is no longer open. Registering a satellite once\n// makes forgetting it impossible.\n//\n// It also carries `isKnownPath`, which only Patterpad had and which is a real\n// guard rather than a tidiness: a renderer must not be able to name an\n// arbitrary directory and have main open it. Fresh paths enter through the\n// native dialogs, which run in main where the person actually chose them;\n// everything else must already be in recents.\n//\n// Electron-free: a satellite is anything with the two methods it needs, which\n// a BrowserWindow satisfies and a test can fake.\n// ---------------------------------------------------------------------------\n\nimport { resolve } from \"node:path\";\n\n/** As much of a BrowserWindow as this file has any business knowing. */\nexport interface SatelliteWindow {\n isDestroyed(): boolean;\n webContents: { send(channel: string, ...args: unknown[]): void };\n}\n\n/**\n * A tool window that shows something about the open project, and any state\n * main is holding on its behalf.\n */\nexport interface Satellite {\n /** Looked up each time rather than held: these windows open and close. */\n window: () => SatelliteWindow | undefined | null;\n /** What to send when the project changes. The window re-fetches; it is a\n * nudge, not a payload, because by then the new project is already open. */\n channel: string;\n /** Drop what main was caching for the old project - a coverage report, a\n * search anchor. Called before the window is told. */\n clear?: () => void;\n}\n\n/** The slice of the app store this needs. Structural, so the real store fits\n * without dragging its generic parameters in here. */\nexport interface SessionStore {\n get(): { recents: string[]; lastProject?: string };\n touchProject(path: string): void;\n forgetProject(path: string): void;\n}\n\nexport interface ProjectSessionOptions<S, R> {\n store: SessionStore;\n /**\n * Open it. The one app-specific step.\n *\n * `root` is what gets recorded, and it is not always the path asked for: a\n * file-association launch names a shard INSIDE a project, and it is the\n * project that belongs in recents. `reply` is whatever the app sends back to\n * its renderer - this never looks inside it.\n */\n open: (path: string) => { session: S; root: string; reply: R } | { error: string };\n /**\n * The outgoing session is finished with: sweep its temporary files, close its\n * handles. Called only once the replacement has opened successfully, so a\n * failed open leaves the current project entirely alone.\n */\n close?: (session: S) => void;\n /** Rebuild the application menu. Called after a change either way, because a\n * failed open changes recents too. */\n refreshMenu?: () => void;\n satellites?: Satellite[];\n}\n\nexport interface ProjectSession<S, R> {\n /** The open project, or undefined before the first one. */\n current: () => S | undefined;\n /** Open, with everything in the comment above. */\n openAt: (path: string) => R | { error: string };\n /** May the renderer ask for this path? See the note above: only paths the app\n * already knows, because a native dialog put them there. */\n isKnownPath: (path: string) => boolean;\n /** Register a tool window after construction, which is when they usually\n * appear. Returns a function that unregisters it. */\n addSatellite: (satellite: Satellite) => () => void;\n /** Fire every satellite by hand. For the cases that are not an open but do\n * invalidate the same things - a merge landing new content underneath. */\n invalidateSatellites: () => void;\n}\n\nexport function createProjectSession<S, R>(opts: ProjectSessionOptions<S, R>): ProjectSession<S, R> {\n let session: S | undefined;\n const satellites: Satellite[] = [...(opts.satellites ?? [])];\n\n const invalidateSatellites = (): void => {\n for (const sat of satellites) {\n sat.clear?.();\n const win = sat.window();\n if (win && !win.isDestroyed()) win.webContents.send(sat.channel);\n }\n };\n\n return {\n current: () => session,\n\n openAt(path) {\n const opened = opts.open(path);\n if (\"error\" in opened) {\n opts.store.forgetProject(path);\n opts.refreshMenu?.();\n return opened;\n }\n if (session !== undefined) opts.close?.(session);\n session = opened.session;\n opts.store.touchProject(opened.root);\n invalidateSatellites();\n opts.refreshMenu?.();\n return opened.reply;\n },\n\n isKnownPath(path) {\n const s = opts.store.get();\n const known = new Set(\n [s.lastProject, ...s.recents].filter((p): p is string => typeof p === \"string\" && p !== \"\").map((p) => resolve(p)),\n );\n return known.has(resolve(path));\n },\n\n addSatellite(satellite) {\n satellites.push(satellite);\n return () => {\n const i = satellites.indexOf(satellite);\n if (i >= 0) satellites.splice(i, 1);\n };\n },\n\n invalidateSatellites,\n };\n}\n"]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** As much of a BrowserWindow as this file has any business knowing. */
|
|
2
|
+
interface SatelliteWindow {
|
|
3
|
+
isDestroyed(): boolean;
|
|
4
|
+
webContents: {
|
|
5
|
+
send(channel: string, ...args: unknown[]): void;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A tool window that shows something about the open project, and any state
|
|
10
|
+
* main is holding on its behalf.
|
|
11
|
+
*/
|
|
12
|
+
interface Satellite {
|
|
13
|
+
/** Looked up each time rather than held: these windows open and close. */
|
|
14
|
+
window: () => SatelliteWindow | undefined | null;
|
|
15
|
+
/** What to send when the project changes. The window re-fetches; it is a
|
|
16
|
+
* nudge, not a payload, because by then the new project is already open. */
|
|
17
|
+
channel: string;
|
|
18
|
+
/** Drop what main was caching for the old project - a coverage report, a
|
|
19
|
+
* search anchor. Called before the window is told. */
|
|
20
|
+
clear?: () => void;
|
|
21
|
+
}
|
|
22
|
+
/** The slice of the app store this needs. Structural, so the real store fits
|
|
23
|
+
* without dragging its generic parameters in here. */
|
|
24
|
+
interface SessionStore {
|
|
25
|
+
get(): {
|
|
26
|
+
recents: string[];
|
|
27
|
+
lastProject?: string;
|
|
28
|
+
};
|
|
29
|
+
touchProject(path: string): void;
|
|
30
|
+
forgetProject(path: string): void;
|
|
31
|
+
}
|
|
32
|
+
interface ProjectSessionOptions<S, R> {
|
|
33
|
+
store: SessionStore;
|
|
34
|
+
/**
|
|
35
|
+
* Open it. The one app-specific step.
|
|
36
|
+
*
|
|
37
|
+
* `root` is what gets recorded, and it is not always the path asked for: a
|
|
38
|
+
* file-association launch names a shard INSIDE a project, and it is the
|
|
39
|
+
* project that belongs in recents. `reply` is whatever the app sends back to
|
|
40
|
+
* its renderer - this never looks inside it.
|
|
41
|
+
*/
|
|
42
|
+
open: (path: string) => {
|
|
43
|
+
session: S;
|
|
44
|
+
root: string;
|
|
45
|
+
reply: R;
|
|
46
|
+
} | {
|
|
47
|
+
error: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The outgoing session is finished with: sweep its temporary files, close its
|
|
51
|
+
* handles. Called only once the replacement has opened successfully, so a
|
|
52
|
+
* failed open leaves the current project entirely alone.
|
|
53
|
+
*/
|
|
54
|
+
close?: (session: S) => void;
|
|
55
|
+
/** Rebuild the application menu. Called after a change either way, because a
|
|
56
|
+
* failed open changes recents too. */
|
|
57
|
+
refreshMenu?: () => void;
|
|
58
|
+
satellites?: Satellite[];
|
|
59
|
+
}
|
|
60
|
+
interface ProjectSession<S, R> {
|
|
61
|
+
/** The open project, or undefined before the first one. */
|
|
62
|
+
current: () => S | undefined;
|
|
63
|
+
/** Open, with everything in the comment above. */
|
|
64
|
+
openAt: (path: string) => R | {
|
|
65
|
+
error: string;
|
|
66
|
+
};
|
|
67
|
+
/** May the renderer ask for this path? See the note above: only paths the app
|
|
68
|
+
* already knows, because a native dialog put them there. */
|
|
69
|
+
isKnownPath: (path: string) => boolean;
|
|
70
|
+
/** Register a tool window after construction, which is when they usually
|
|
71
|
+
* appear. Returns a function that unregisters it. */
|
|
72
|
+
addSatellite: (satellite: Satellite) => () => void;
|
|
73
|
+
/** Fire every satellite by hand. For the cases that are not an open but do
|
|
74
|
+
* invalidate the same things - a merge landing new content underneath. */
|
|
75
|
+
invalidateSatellites: () => void;
|
|
76
|
+
}
|
|
77
|
+
declare function createProjectSession<S, R>(opts: ProjectSessionOptions<S, R>): ProjectSession<S, R>;
|
|
78
|
+
|
|
79
|
+
export { type ProjectSession, type ProjectSessionOptions, type Satellite, type SatelliteWindow, type SessionStore, createProjectSession };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** As much of a BrowserWindow as this file has any business knowing. */
|
|
2
|
+
interface SatelliteWindow {
|
|
3
|
+
isDestroyed(): boolean;
|
|
4
|
+
webContents: {
|
|
5
|
+
send(channel: string, ...args: unknown[]): void;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A tool window that shows something about the open project, and any state
|
|
10
|
+
* main is holding on its behalf.
|
|
11
|
+
*/
|
|
12
|
+
interface Satellite {
|
|
13
|
+
/** Looked up each time rather than held: these windows open and close. */
|
|
14
|
+
window: () => SatelliteWindow | undefined | null;
|
|
15
|
+
/** What to send when the project changes. The window re-fetches; it is a
|
|
16
|
+
* nudge, not a payload, because by then the new project is already open. */
|
|
17
|
+
channel: string;
|
|
18
|
+
/** Drop what main was caching for the old project - a coverage report, a
|
|
19
|
+
* search anchor. Called before the window is told. */
|
|
20
|
+
clear?: () => void;
|
|
21
|
+
}
|
|
22
|
+
/** The slice of the app store this needs. Structural, so the real store fits
|
|
23
|
+
* without dragging its generic parameters in here. */
|
|
24
|
+
interface SessionStore {
|
|
25
|
+
get(): {
|
|
26
|
+
recents: string[];
|
|
27
|
+
lastProject?: string;
|
|
28
|
+
};
|
|
29
|
+
touchProject(path: string): void;
|
|
30
|
+
forgetProject(path: string): void;
|
|
31
|
+
}
|
|
32
|
+
interface ProjectSessionOptions<S, R> {
|
|
33
|
+
store: SessionStore;
|
|
34
|
+
/**
|
|
35
|
+
* Open it. The one app-specific step.
|
|
36
|
+
*
|
|
37
|
+
* `root` is what gets recorded, and it is not always the path asked for: a
|
|
38
|
+
* file-association launch names a shard INSIDE a project, and it is the
|
|
39
|
+
* project that belongs in recents. `reply` is whatever the app sends back to
|
|
40
|
+
* its renderer - this never looks inside it.
|
|
41
|
+
*/
|
|
42
|
+
open: (path: string) => {
|
|
43
|
+
session: S;
|
|
44
|
+
root: string;
|
|
45
|
+
reply: R;
|
|
46
|
+
} | {
|
|
47
|
+
error: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The outgoing session is finished with: sweep its temporary files, close its
|
|
51
|
+
* handles. Called only once the replacement has opened successfully, so a
|
|
52
|
+
* failed open leaves the current project entirely alone.
|
|
53
|
+
*/
|
|
54
|
+
close?: (session: S) => void;
|
|
55
|
+
/** Rebuild the application menu. Called after a change either way, because a
|
|
56
|
+
* failed open changes recents too. */
|
|
57
|
+
refreshMenu?: () => void;
|
|
58
|
+
satellites?: Satellite[];
|
|
59
|
+
}
|
|
60
|
+
interface ProjectSession<S, R> {
|
|
61
|
+
/** The open project, or undefined before the first one. */
|
|
62
|
+
current: () => S | undefined;
|
|
63
|
+
/** Open, with everything in the comment above. */
|
|
64
|
+
openAt: (path: string) => R | {
|
|
65
|
+
error: string;
|
|
66
|
+
};
|
|
67
|
+
/** May the renderer ask for this path? See the note above: only paths the app
|
|
68
|
+
* already knows, because a native dialog put them there. */
|
|
69
|
+
isKnownPath: (path: string) => boolean;
|
|
70
|
+
/** Register a tool window after construction, which is when they usually
|
|
71
|
+
* appear. Returns a function that unregisters it. */
|
|
72
|
+
addSatellite: (satellite: Satellite) => () => void;
|
|
73
|
+
/** Fire every satellite by hand. For the cases that are not an open but do
|
|
74
|
+
* invalidate the same things - a merge landing new content underneath. */
|
|
75
|
+
invalidateSatellites: () => void;
|
|
76
|
+
}
|
|
77
|
+
declare function createProjectSession<S, R>(opts: ProjectSessionOptions<S, R>): ProjectSession<S, R>;
|
|
78
|
+
|
|
79
|
+
export { type ProjectSession, type ProjectSessionOptions, type Satellite, type SatelliteWindow, type SessionStore, createProjectSession };
|