@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.
Files changed (80) 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 +1287 -0
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +488 -1
  25. package/dist/index.d.ts +488 -1
  26. package/dist/index.js +1241 -1
  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 +19 -0
  37. package/dist/menu.cjs.map +1 -0
  38. package/dist/menu.d.cts +32 -0
  39. package/dist/menu.d.ts +32 -0
  40. package/dist/menu.js +3 -0
  41. package/dist/menu.js.map +1 -0
  42. package/dist/notes-editor.css +107 -0
  43. package/dist/notes-editor.css.map +1 -0
  44. package/dist/pane-shell.css +114 -0
  45. package/dist/pane-shell.css.map +1 -0
  46. package/dist/session.cjs +52 -0
  47. package/dist/session.cjs.map +1 -0
  48. package/dist/session.d.cts +79 -0
  49. package/dist/session.d.ts +79 -0
  50. package/dist/session.js +50 -0
  51. package/dist/session.js.map +1 -0
  52. package/dist/settings.css +266 -0
  53. package/dist/settings.css.map +1 -0
  54. package/dist/tool-window.cjs +79 -0
  55. package/dist/tool-window.cjs.map +1 -0
  56. package/dist/tool-window.css +60 -0
  57. package/dist/tool-window.css.map +1 -0
  58. package/dist/tool-window.d.cts +63 -0
  59. package/dist/tool-window.d.ts +63 -0
  60. package/dist/tool-window.js +73 -0
  61. package/dist/tool-window.js.map +1 -0
  62. package/dist/tooltip.css +22 -0
  63. package/dist/tooltip.css.map +1 -0
  64. package/dist/updater.cjs +315 -0
  65. package/dist/updater.cjs.map +1 -0
  66. package/dist/updater.d.cts +53 -0
  67. package/dist/updater.d.ts +53 -0
  68. package/dist/updater.js +306 -0
  69. package/dist/updater.js.map +1 -0
  70. package/dist/vc-status.cjs +77 -0
  71. package/dist/vc-status.cjs.map +1 -0
  72. package/dist/vc-status.d.cts +40 -0
  73. package/dist/vc-status.d.ts +40 -0
  74. package/dist/vc-status.js +71 -0
  75. package/dist/vc-status.js.map +1 -0
  76. package/dist/vc.css +41 -0
  77. package/dist/vc.css.map +1 -0
  78. package/package.json +101 -2
  79. package/dist/tokens.d.cts +0 -2
  80. package/dist/tokens.d.ts +0 -2
@@ -0,0 +1,50 @@
1
+ import { resolve } from 'path';
2
+
3
+ // src/session.ts
4
+ function createProjectSession(opts) {
5
+ let session;
6
+ const satellites = [...opts.satellites ?? []];
7
+ const invalidateSatellites = () => {
8
+ for (const sat of satellites) {
9
+ sat.clear?.();
10
+ const win = sat.window();
11
+ if (win && !win.isDestroyed()) win.webContents.send(sat.channel);
12
+ }
13
+ };
14
+ return {
15
+ current: () => session,
16
+ openAt(path) {
17
+ const opened = opts.open(path);
18
+ if ("error" in opened) {
19
+ opts.store.forgetProject(path);
20
+ opts.refreshMenu?.();
21
+ return opened;
22
+ }
23
+ if (session !== void 0) opts.close?.(session);
24
+ session = opened.session;
25
+ opts.store.touchProject(opened.root);
26
+ invalidateSatellites();
27
+ opts.refreshMenu?.();
28
+ return opened.reply;
29
+ },
30
+ isKnownPath(path) {
31
+ const s = opts.store.get();
32
+ const known = new Set(
33
+ [s.lastProject, ...s.recents].filter((p) => typeof p === "string" && p !== "").map((p) => resolve(p))
34
+ );
35
+ return known.has(resolve(path));
36
+ },
37
+ addSatellite(satellite) {
38
+ satellites.push(satellite);
39
+ return () => {
40
+ const i = satellites.indexOf(satellite);
41
+ if (i >= 0) satellites.splice(i, 1);
42
+ };
43
+ },
44
+ invalidateSatellites
45
+ };
46
+ }
47
+
48
+ export { createProjectSession };
49
+ //# sourceMappingURL=session.js.map
50
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/session.ts"],"names":[],"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,YAAY,IAAA,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,KAAM,OAAA,CAAQ,CAAC,CAAC;AAAA,OACnH;AACA,MAAA,OAAO,KAAA,CAAM,GAAA,CAAI,OAAA,CAAQ,IAAI,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.js","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,266 @@
1
+ /* src/settings.css */
2
+ .settings-dialog {
3
+ padding: 0;
4
+ border: 1px solid var(--line);
5
+ border-radius: var(--radius-lg);
6
+ background: var(--surface);
7
+ color: var(--ink);
8
+ width: min(760px, 92vw);
9
+ max-height: 82vh;
10
+ box-shadow: var(--shadow-3);
11
+ font: 14px var(--font-ui, system-ui, sans-serif);
12
+ }
13
+ .settings-dialog::backdrop {
14
+ background: color-mix(in oklab, #000 42%, transparent);
15
+ }
16
+ .settings-form {
17
+ display: flex;
18
+ flex-direction: column;
19
+ max-height: 82vh;
20
+ }
21
+ .settings-title {
22
+ margin: 0;
23
+ padding: 16px 20px;
24
+ font-family: var(--font-read, inherit);
25
+ font-size: 1.15rem;
26
+ font-weight: 600;
27
+ border-bottom: 1px solid var(--line-soft, var(--line));
28
+ }
29
+ .settings-body {
30
+ display: flex;
31
+ min-height: 0;
32
+ flex: 1;
33
+ }
34
+ .settings-tabs {
35
+ flex: none;
36
+ width: 12rem;
37
+ overflow-y: auto;
38
+ padding: 10px 8px;
39
+ border-right: 1px solid var(--line-soft, var(--line));
40
+ display: flex;
41
+ flex-direction: column;
42
+ gap: 1px;
43
+ }
44
+ .settings-group {
45
+ padding: 12px 8px 4px;
46
+ font: 600 0.62rem/1 var(--font-ui);
47
+ text-transform: uppercase;
48
+ letter-spacing: 0.07em;
49
+ color: var(--muted);
50
+ }
51
+ .settings-tab {
52
+ text-align: left;
53
+ width: 100%;
54
+ padding: 6px 10px;
55
+ border: 0;
56
+ border-radius: var(--radius-sm);
57
+ background: none;
58
+ color: var(--ink);
59
+ font: inherit;
60
+ font-size: 0.88rem;
61
+ cursor: pointer;
62
+ }
63
+ .settings-tab:hover {
64
+ background: var(--accent-soft, color-mix(in oklab, var(--accent) 12%, transparent));
65
+ }
66
+ .settings-tab.active {
67
+ background: var(--accent-soft, color-mix(in oklab, var(--accent) 16%, transparent));
68
+ color: var(--accent);
69
+ font-weight: 500;
70
+ }
71
+ .settings-panels {
72
+ flex: 1;
73
+ min-width: 0;
74
+ overflow-y: auto;
75
+ padding: 18px 22px;
76
+ }
77
+ .settings-panel {
78
+ display: flex;
79
+ flex-direction: column;
80
+ gap: 14px;
81
+ }
82
+ .settings-panel[hidden] {
83
+ display: none;
84
+ }
85
+ .settings-actions {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: 10px;
89
+ padding: 12px 20px;
90
+ border-top: 1px solid var(--line-soft, var(--line));
91
+ }
92
+ .settings-error {
93
+ flex: 1;
94
+ margin: 0;
95
+ color: var(--danger);
96
+ font-size: 0.82rem;
97
+ }
98
+ .settings-actions button {
99
+ font: inherit;
100
+ padding: 6px 16px;
101
+ border: 1px solid var(--line);
102
+ border-radius: var(--radius-sm);
103
+ background: none;
104
+ color: var(--muted);
105
+ cursor: pointer;
106
+ }
107
+ .settings-actions button:hover {
108
+ color: var(--accent);
109
+ border-color: var(--accent);
110
+ }
111
+ .settings-actions .primary {
112
+ background: var(--accent);
113
+ color: var(--surface);
114
+ border-color: transparent;
115
+ font-weight: 500;
116
+ }
117
+ .settings-actions .primary:hover {
118
+ color: var(--surface);
119
+ filter: brightness(1.06);
120
+ }
121
+ .set-row {
122
+ display: flex;
123
+ flex-direction: column;
124
+ gap: 6px;
125
+ padding: 6px 0;
126
+ border-bottom: 1px solid var(--line-soft, var(--line));
127
+ }
128
+ .set-rowline {
129
+ display: flex;
130
+ align-items: center;
131
+ gap: 8px;
132
+ }
133
+ .set-details {
134
+ display: flex;
135
+ flex-direction: column;
136
+ gap: 8px;
137
+ padding: 6px 0 4px 26px;
138
+ }
139
+ .set-details[hidden] {
140
+ display: none;
141
+ }
142
+ .set-expand {
143
+ flex: none;
144
+ width: 18px;
145
+ height: 18px;
146
+ border: 0;
147
+ background: none;
148
+ cursor: pointer;
149
+ display: flex;
150
+ align-items: center;
151
+ justify-content: center;
152
+ padding: 0;
153
+ }
154
+ .set-expand::before {
155
+ content: "";
156
+ width: 0;
157
+ height: 0;
158
+ border-left: 6px solid currentColor;
159
+ border-top: 4.5px solid transparent;
160
+ border-bottom: 4.5px solid transparent;
161
+ transform-origin: 35% 50%;
162
+ transition: transform 0.12s ease;
163
+ }
164
+ .set-expand {
165
+ color: var(--muted);
166
+ }
167
+ .set-expand:hover,
168
+ .set-expand.open {
169
+ color: var(--accent);
170
+ }
171
+ .set-expand.open::before {
172
+ transform: rotate(90deg);
173
+ }
174
+ .set-rowline input,
175
+ .set-rowline select,
176
+ .set-details input,
177
+ .set-details select {
178
+ font: inherit;
179
+ font-size: 0.85rem;
180
+ background: var(--bg);
181
+ color: var(--ink);
182
+ border: 1px solid var(--line);
183
+ border-radius: var(--radius-sm);
184
+ padding: 4px 8px;
185
+ }
186
+ .set-rowline input.set-name {
187
+ flex: 1;
188
+ min-width: 6rem;
189
+ }
190
+ .set-rowline input.invalid,
191
+ .set-details input.invalid {
192
+ border-color: var(--danger);
193
+ }
194
+ .set-rowline input:focus-visible,
195
+ .set-rowline select:focus-visible,
196
+ .set-details input:focus-visible,
197
+ .set-details select:focus-visible {
198
+ outline: 2px solid var(--accent);
199
+ outline-offset: 0;
200
+ border-color: var(--accent);
201
+ }
202
+ .shell-icon {
203
+ flex: none;
204
+ width: 24px;
205
+ height: 24px;
206
+ border: 1px solid var(--line);
207
+ border-radius: var(--radius-sm);
208
+ background: none;
209
+ color: var(--muted);
210
+ cursor: pointer;
211
+ font-size: 0.8rem;
212
+ }
213
+ .shell-icon:hover:not(:disabled) {
214
+ color: var(--accent);
215
+ border-color: var(--accent);
216
+ }
217
+ .shell-icon:disabled {
218
+ opacity: 0.4;
219
+ cursor: default;
220
+ }
221
+ .shell-icon.danger:hover:not(:disabled) {
222
+ color: var(--surface);
223
+ background: var(--danger);
224
+ border-color: var(--danger);
225
+ }
226
+ .shell-labelled {
227
+ display: flex;
228
+ align-items: center;
229
+ gap: 8px;
230
+ font-size: 0.85rem;
231
+ }
232
+ .shell-fieldcap {
233
+ color: var(--muted);
234
+ min-width: 5rem;
235
+ }
236
+ .shell-tags {
237
+ display: flex;
238
+ flex-wrap: wrap;
239
+ gap: 5px;
240
+ align-items: center;
241
+ }
242
+ .shell-tag {
243
+ display: inline-flex;
244
+ align-items: center;
245
+ gap: 4px;
246
+ font-size: 0.76rem;
247
+ padding: 2px 4px 2px 9px;
248
+ border: 1px solid var(--line);
249
+ border-radius: var(--radius-pill);
250
+ color: var(--ink);
251
+ }
252
+ .shell-tag-x {
253
+ border: 0;
254
+ background: none;
255
+ color: var(--muted);
256
+ cursor: pointer;
257
+ font-size: 0.7rem;
258
+ padding: 0 2px;
259
+ }
260
+ .shell-tag-x:hover {
261
+ color: var(--danger);
262
+ }
263
+ .shell-tag-input {
264
+ min-width: 8rem;
265
+ }
266
+ /*# sourceMappingURL=settings.css.map */
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/settings.css"],"sourcesContent":["/* ============================================================================\n The settings dialog + list-editor styling. Reads the shared token grammar\n (tokens.css) and the app's palette vars (--surface / --ink / --muted / --line\n / --accent / --danger / --bg, optional --line-soft / --accent-soft). Neutral\n class names; the app supplies section content.\n ============================================================================ */\n\n.settings-dialog {\n padding: 0; border: 1px solid var(--line); border-radius: var(--radius-lg);\n background: var(--surface); color: var(--ink);\n width: min(760px, 92vw); max-height: 82vh;\n box-shadow: var(--shadow-3);\n font: 14px var(--font-ui, system-ui, sans-serif);\n}\n.settings-dialog::backdrop { background: color-mix(in oklab, #000 42%, transparent); }\n.settings-form { display: flex; flex-direction: column; max-height: 82vh; }\n.settings-title { margin: 0; padding: 16px 20px; font-family: var(--font-read, inherit); font-size: 1.15rem; font-weight: 600; border-bottom: 1px solid var(--line-soft, var(--line)); }\n\n.settings-body { display: flex; min-height: 0; flex: 1; }\n.settings-tabs { flex: none; width: 12rem; overflow-y: auto; padding: 10px 8px; border-right: 1px solid var(--line-soft, var(--line)); display: flex; flex-direction: column; gap: 1px; }\n.settings-group { padding: 12px 8px 4px; font: 600 0.62rem/1 var(--font-ui); text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); }\n.settings-tab { text-align: left; width: 100%; padding: 6px 10px; border: 0; border-radius: var(--radius-sm); background: none; color: var(--ink); font: inherit; font-size: 0.88rem; cursor: pointer; }\n.settings-tab:hover { background: var(--accent-soft, color-mix(in oklab, var(--accent) 12%, transparent)); }\n.settings-tab.active { background: var(--accent-soft, color-mix(in oklab, var(--accent) 16%, transparent)); color: var(--accent); font-weight: 500; }\n\n.settings-panels { flex: 1; min-width: 0; overflow-y: auto; padding: 18px 22px; }\n.settings-panel { display: flex; flex-direction: column; gap: 14px; }\n/* A `display` on the class beats the UA's `[hidden] { display: none }`, so the\n panels the tab switcher hides would all render at once. Opt back out. */\n.settings-panel[hidden] { display: none; }\n\n.settings-actions { display: flex; align-items: center; gap: 10px; padding: 12px 20px; border-top: 1px solid var(--line-soft, var(--line)); }\n.settings-error { flex: 1; margin: 0; color: var(--danger); font-size: 0.82rem; }\n.settings-actions button { font: inherit; padding: 6px 16px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: none; color: var(--muted); cursor: pointer; }\n.settings-actions button:hover { color: var(--accent); border-color: var(--accent); }\n.settings-actions .primary { background: var(--accent); color: var(--surface); border-color: transparent; font-weight: 500; }\n.settings-actions .primary:hover { color: var(--surface); filter: brightness(1.06); }\n\n/* --- list editor (rows + disclosure + inline field kit) --------------------- */\n.set-row { display: flex; flex-direction: column; gap: 6px; padding: 6px 0; border-bottom: 1px solid var(--line-soft, var(--line)); }\n.set-rowline { display: flex; align-items: center; gap: 8px; }\n.set-details { display: flex; flex-direction: column; gap: 8px; padding: 6px 0 4px 26px; }\n.set-details[hidden] { display: none; } /* see .settings-panel[hidden] */\n/* The disclosure triangle is drawn, not typed: U+25B8/25BE are \"small\"\n triangles that stay hairline-thin at any font size. Rotates when open. */\n.set-expand { flex: none; width: 18px; height: 18px; border: 0; background: none; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; }\n.set-expand::before {\n content: \"\"; width: 0; height: 0;\n border-left: 6px solid currentColor; border-top: 4.5px solid transparent; border-bottom: 4.5px solid transparent;\n transform-origin: 35% 50%; transition: transform 0.12s ease;\n}\n.set-expand { color: var(--muted); }\n.set-expand:hover, .set-expand.open { color: var(--accent); }\n.set-expand.open::before { transform: rotate(90deg); }\n\n.set-rowline input, .set-rowline select, .set-details input, .set-details select {\n font: inherit; font-size: 0.85rem; background: var(--bg); color: var(--ink);\n border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 4px 8px;\n}\n.set-rowline input.set-name { flex: 1; min-width: 6rem; }\n.set-rowline input.invalid, .set-details input.invalid { border-color: var(--danger); }\n.set-rowline input:focus-visible, .set-rowline select:focus-visible,\n.set-details input:focus-visible, .set-details select:focus-visible { outline: 2px solid var(--accent); outline-offset: 0; border-color: var(--accent); }\n\n.shell-icon { flex: none; width: 24px; height: 24px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: none; color: var(--muted); cursor: pointer; font-size: 0.8rem; }\n.shell-icon:hover:not(:disabled) { color: var(--accent); border-color: var(--accent); }\n.shell-icon:disabled { opacity: 0.4; cursor: default; }\n.shell-icon.danger:hover:not(:disabled) { color: var(--surface); background: var(--danger); border-color: var(--danger); }\n\n.shell-labelled { display: flex; align-items: center; gap: 8px; font-size: 0.85rem; }\n.shell-fieldcap { color: var(--muted); min-width: 5rem; }\n\n.shell-tags { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; }\n.shell-tag { display: inline-flex; align-items: center; gap: 4px; font-size: 0.76rem; padding: 2px 4px 2px 9px; border: 1px solid var(--line); border-radius: var(--radius-pill); color: var(--ink); }\n.shell-tag-x { border: 0; background: none; color: var(--muted); cursor: pointer; font-size: 0.7rem; padding: 0 2px; }\n.shell-tag-x:hover { color: var(--danger); }\n.shell-tag-input { min-width: 8rem; }\n"],"mappings":"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","names":[]}
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ var electron = require('electron');
4
+ var path = require('path');
5
+
6
+ // src/tool-window.ts
7
+ function savedWindowRect(saved, def, min) {
8
+ if (!saved) return { ...def };
9
+ const w = Math.max(min.width, saved.width), h = Math.max(min.height, saved.height);
10
+ if (saved.x != null && saved.y != null) {
11
+ const onScreen = electron.screen.getAllDisplays().some((d) => {
12
+ const a = d.workArea;
13
+ return saved.x + w > a.x + 40 && saved.x < a.x + a.width - 40 && saved.y + h > a.y + 20 && saved.y < a.y + a.height - 20;
14
+ });
15
+ if (onScreen) return { x: saved.x, y: saved.y, width: w, height: h };
16
+ }
17
+ return { width: w, height: h };
18
+ }
19
+ function rememberBounds(w, write) {
20
+ const saveBounds = () => {
21
+ if (!w.isDestroyed()) write(w.getBounds());
22
+ };
23
+ let boundsTimer;
24
+ const queueSave = () => {
25
+ clearTimeout(boundsTimer);
26
+ boundsTimer = setTimeout(saveBounds, 400);
27
+ };
28
+ w.on("resize", queueSave);
29
+ w.on("move", queueSave);
30
+ w.on("close", () => {
31
+ clearTimeout(boundsTimer);
32
+ saveBounds();
33
+ });
34
+ }
35
+ function centeredOnPrimary(size) {
36
+ const a = electron.screen.getPrimaryDisplay().workArea;
37
+ return { x: Math.round(a.x + (a.width - size.width) / 2), y: Math.round(a.y + (a.height - size.height) / 2) };
38
+ }
39
+ function rescueToolWindow(w, def) {
40
+ if (!w || w.isDestroyed()) return;
41
+ if (w.isMinimized()) w.restore();
42
+ w.setAlwaysOnTop(true);
43
+ w.setBounds({ ...def, ...centeredOnPrimary(def) });
44
+ w.show();
45
+ }
46
+ function openToolWindow(existing, opts) {
47
+ if (existing && !existing.isDestroyed()) {
48
+ existing.focus();
49
+ return existing;
50
+ }
51
+ const w = new electron.BrowserWindow({
52
+ ...opts.rect,
53
+ ...opts.min !== void 0 ? { minWidth: opts.min.width, minHeight: opts.min.height } : {},
54
+ show: false,
55
+ title: opts.title,
56
+ ...opts.frame !== void 0 ? { frame: opts.frame } : {},
57
+ ...opts.alwaysOnTop !== void 0 ? { alwaysOnTop: opts.alwaysOnTop } : {},
58
+ webPreferences: {
59
+ contextIsolation: true,
60
+ nodeIntegration: false,
61
+ sandbox: true,
62
+ preload: opts.preload
63
+ }
64
+ });
65
+ if (opts.remember) rememberBounds(w, opts.remember);
66
+ w.once("ready-to-show", () => w.show());
67
+ if (opts.onClosed) w.on("closed", opts.onClosed);
68
+ if (process.env["ELECTRON_RENDERER_URL"]) void w.loadURL(`${process.env["ELECTRON_RENDERER_URL"]}/${opts.page}`);
69
+ else void w.loadFile(path.join(opts.rendererDir, opts.page));
70
+ return w;
71
+ }
72
+
73
+ exports.centeredOnPrimary = centeredOnPrimary;
74
+ exports.openToolWindow = openToolWindow;
75
+ exports.rememberBounds = rememberBounds;
76
+ exports.rescueToolWindow = rescueToolWindow;
77
+ exports.savedWindowRect = savedWindowRect;
78
+ //# sourceMappingURL=tool-window.cjs.map
79
+ //# sourceMappingURL=tool-window.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tool-window.ts"],"names":["screen","BrowserWindow","join"],"mappings":";;;;;;AA4CO,SAAS,eAAA,CACd,KAAA,EACA,GAAA,EACA,GAAA,EACkB;AAClB,EAAA,IAAI,CAAC,KAAA,EAAO,OAAO,EAAE,GAAG,GAAA,EAAI;AAC5B,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,OAAO,KAAA,CAAM,KAAK,CAAA,EAAG,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,MAAA,EAAQ,MAAM,MAAM,CAAA;AACjF,EAAA,IAAI,KAAA,CAAM,CAAA,IAAK,IAAA,IAAQ,KAAA,CAAM,KAAK,IAAA,EAAM;AACtC,IAAA,MAAM,WAAWA,eAAA,CAAO,cAAA,EAAe,CAAE,IAAA,CAAK,CAAC,CAAA,KAAM;AACnD,MAAA,MAAM,IAAI,CAAA,CAAE,QAAA;AACZ,MAAA,OAAO,KAAA,CAAM,CAAA,GAAK,CAAA,GAAI,CAAA,CAAE,CAAA,GAAI,MAAM,KAAA,CAAM,CAAA,GAAK,CAAA,CAAE,CAAA,GAAI,CAAA,CAAE,KAAA,GAAQ,MAAM,KAAA,CAAM,CAAA,GAAK,CAAA,GAAI,CAAA,CAAE,CAAA,GAAI,EAAA,IAAM,MAAM,CAAA,GAAK,CAAA,CAAE,CAAA,GAAI,CAAA,CAAE,MAAA,GAAS,EAAA;AAAA,IAC5H,CAAC,CAAA;AACD,IAAA,IAAI,QAAA,EAAU,OAAO,EAAE,CAAA,EAAG,KAAA,CAAM,CAAA,EAAG,CAAA,EAAG,KAAA,CAAM,CAAA,EAAG,KAAA,EAAO,CAAA,EAAG,MAAA,EAAQ,CAAA,EAAE;AAAA,EACrE;AACA,EAAA,OAAO,EAAE,KAAA,EAAO,CAAA,EAAG,MAAA,EAAQ,CAAA,EAAE;AAC/B;AAIO,SAAS,cAAA,CAAe,GAAkB,KAAA,EAAiD;AAChG,EAAA,MAAM,aAAa,MAAY;AAAE,IAAA,IAAI,CAAC,CAAA,CAAE,WAAA,IAAe,KAAA,CAAM,CAAA,CAAE,WAAW,CAAA;AAAA,EAAG,CAAA;AAC7E,EAAA,IAAI,WAAA;AACJ,EAAA,MAAM,YAAY,MAAY;AAAE,IAAA,YAAA,CAAa,WAAW,CAAA;AAAG,IAAA,WAAA,GAAc,UAAA,CAAW,YAAY,GAAG,CAAA;AAAA,EAAG,CAAA;AACtG,EAAA,CAAA,CAAE,EAAA,CAAG,UAAU,SAAS,CAAA;AACxB,EAAA,CAAA,CAAE,EAAA,CAAG,QAAQ,SAAS,CAAA;AACtB,EAAA,CAAA,CAAE,EAAA,CAAG,SAAS,MAAM;AAAE,IAAA,YAAA,CAAa,WAAW,CAAA;AAAG,IAAA,UAAA,EAAW;AAAA,EAAG,CAAC,CAAA;AAClE;AAGO,SAAS,kBAAkB,IAAA,EAAgD;AAChF,EAAA,MAAM,CAAA,GAAIA,eAAA,CAAO,iBAAA,EAAkB,CAAE,QAAA;AACrC,EAAA,OAAO,EAAE,GAAG,IAAA,CAAK,KAAA,CAAM,EAAE,CAAA,GAAA,CAAK,CAAA,CAAE,KAAA,GAAQ,IAAA,CAAK,KAAA,IAAS,CAAC,GAAG,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,CAAE,CAAA,GAAA,CAAK,EAAE,MAAA,GAAS,IAAA,CAAK,MAAA,IAAU,CAAC,CAAA,EAAE;AAC9G;AAOO,SAAS,gBAAA,CAAiB,GAAqC,GAAA,EAA2B;AAC/F,EAAA,IAAI,CAAC,CAAA,IAAK,CAAA,CAAE,WAAA,EAAY,EAAG;AAC3B,EAAA,IAAI,CAAA,CAAE,WAAA,EAAY,EAAG,CAAA,CAAE,OAAA,EAAQ;AAC/B,EAAA,CAAA,CAAE,eAAe,IAAI,CAAA;AACrB,EAAA,CAAA,CAAE,SAAA,CAAU,EAAE,GAAG,GAAA,EAAK,GAAG,iBAAA,CAAkB,GAAG,GAAG,CAAA;AACjD,EAAA,CAAA,CAAE,IAAA,EAAK;AACT;AA0BO,SAAS,cAAA,CAAe,UAA4C,IAAA,EAAwC;AACjH,EAAA,IAAI,QAAA,IAAY,CAAC,QAAA,CAAS,WAAA,EAAY,EAAG;AAAE,IAAA,QAAA,CAAS,KAAA,EAAM;AAAG,IAAA,OAAO,QAAA;AAAA,EAAU;AAC9E,EAAA,MAAM,CAAA,GAAI,IAAIC,sBAAA,CAAc;AAAA,IAC1B,GAAG,IAAA,CAAK,IAAA;AAAA,IACR,GAAI,IAAA,CAAK,GAAA,KAAQ,MAAA,GAAY,EAAE,QAAA,EAAU,IAAA,CAAK,GAAA,CAAI,KAAA,EAAO,SAAA,EAAW,IAAA,CAAK,GAAA,CAAI,MAAA,KAAW,EAAC;AAAA,IACzF,IAAA,EAAM,KAAA;AAAA,IACN,OAAO,IAAA,CAAK,KAAA;AAAA,IACZ,GAAI,KAAK,KAAA,KAAU,MAAA,GAAY,EAAE,KAAA,EAAO,IAAA,CAAK,KAAA,EAAM,GAAI,EAAC;AAAA,IACxD,GAAI,KAAK,WAAA,KAAgB,MAAA,GAAY,EAAE,WAAA,EAAa,IAAA,CAAK,WAAA,EAAY,GAAI,EAAC;AAAA,IAC1E,cAAA,EAAgB;AAAA,MACd,gBAAA,EAAkB,IAAA;AAAA,MAAM,eAAA,EAAiB,KAAA;AAAA,MAAO,OAAA,EAAS,IAAA;AAAA,MACzD,SAAS,IAAA,CAAK;AAAA;AAChB,GACD,CAAA;AACD,EAAA,IAAI,IAAA,CAAK,QAAA,EAAU,cAAA,CAAe,CAAA,EAAG,KAAK,QAAQ,CAAA;AAClD,EAAA,CAAA,CAAE,IAAA,CAAK,eAAA,EAAiB,MAAM,CAAA,CAAE,MAAM,CAAA;AACtC,EAAA,IAAI,KAAK,QAAA,EAAU,CAAA,CAAE,EAAA,CAAG,QAAA,EAAU,KAAK,QAAQ,CAAA;AAC/C,EAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,uBAAuB,CAAA,OAAQ,CAAA,CAAE,OAAA,CAAQ,CAAA,EAAG,OAAA,CAAQ,IAAI,uBAAuB,CAAC,CAAA,CAAA,EAAI,IAAA,CAAK,IAAI,CAAA,CAAE,CAAA;AAAA,OAC1G,KAAK,EAAE,QAAA,CAASC,SAAA,CAAK,KAAK,WAAA,EAAa,IAAA,CAAK,IAAI,CAAC,CAAA;AACtD,EAAA,OAAO,CAAA;AACT","file":"tool-window.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// The tool-window kit, MAIN-PROCESS half: the detached helper-window\n// machinery both apps had grown four copies of (Patterpad's Play + Search +\n// Coverage, Storyletter's Board + Find). Remembered bounds with a\n// disconnected-monitor guard, debounced persistence, the Reset View rescue,\n// and the pinnable window factory they compose into.\n//\n// This module imports electron, so it ships as its own subpath\n// (@wildwinter/app-shell/tool-window) - never import it from renderer code.\n// The renderer half (pin button, .swin drag-bar grammar) lives in the root\n// export + tool-window.css.\n//\n// Two deliberate corrections over the Patterpad originals, for when it\n// migrates here: rescueToolWindow re-pins EVERY window it rescues (Patterpad\n// re-pinned only Play, so its store lied about Search + Coverage), and the\n// factory always wires closed-cleanup through an identity guard.\n// ---------------------------------------------------------------------------\n\nimport { BrowserWindow, screen } from \"electron\";\nimport { join } from \"node:path\";\n\nexport interface ToolWindowSize {\n width: number;\n height: number;\n}\n\nexport interface ToolWindowBounds {\n x?: number;\n y?: number;\n width: number;\n height: number;\n}\n\n/** The per-window store slice every app remembers: where it was, whether it\n * floats. (Patterpad's PlayWindowState, generalised.) */\nexport interface ToolWindowState {\n bounds?: ToolWindowBounds;\n pinned: boolean;\n}\n\n/** Use a remembered rect only if it still intersects a live display (a window\n * saved on a now-disconnected monitor must not open offscreen). Falls back\n * to the default size; a remembered size is kept even when the position is\n * dropped (Electron then centres it). */\nexport function savedWindowRect(\n saved: ToolWindowBounds | undefined,\n def: ToolWindowSize,\n min: ToolWindowSize,\n): ToolWindowBounds {\n if (!saved) return { ...def };\n const w = Math.max(min.width, saved.width), h = Math.max(min.height, saved.height);\n if (saved.x != null && saved.y != null) {\n const onScreen = screen.getAllDisplays().some((d) => {\n const a = d.workArea;\n return saved.x! + w > a.x + 40 && saved.x! < a.x + a.width - 40 && saved.y! + h > a.y + 20 && saved.y! < a.y + a.height - 20;\n });\n if (onScreen) return { x: saved.x, y: saved.y, width: w, height: h };\n }\n return { width: w, height: h };\n}\n\n/** Persist a helper window's bounds as the user moves / resizes / closes it\n * (debounced 400ms; close flushes synchronously). */\nexport function rememberBounds(w: BrowserWindow, write: (bounds: ToolWindowBounds) => void): void {\n const saveBounds = (): void => { if (!w.isDestroyed()) write(w.getBounds()); };\n let boundsTimer: ReturnType<typeof setTimeout> | undefined;\n const queueSave = (): void => { clearTimeout(boundsTimer); boundsTimer = setTimeout(saveBounds, 400); };\n w.on(\"resize\", queueSave);\n w.on(\"move\", queueSave);\n w.on(\"close\", () => { clearTimeout(boundsTimer); saveBounds(); });\n}\n\n/** Centre a rect on the primary display's work area. */\nexport function centeredOnPrimary(size: ToolWindowSize): { x: number; y: number } {\n const a = screen.getPrimaryDisplay().workArea;\n return { x: Math.round(a.x + (a.width - size.width) / 2), y: Math.round(a.y + (a.height - size.height) / 2) };\n}\n\n/** Reset View's per-window half: un-minimise, re-pin (always - the store\n * says pinned:true after a rescue, so the window must agree), default size,\n * centred on the primary display, shown. The host clears its remembered\n * bounds itself (that's a store concern). No focus steal: helpers show\n * behind the editor. */\nexport function rescueToolWindow(w: BrowserWindow | undefined | null, def: ToolWindowSize): void {\n if (!w || w.isDestroyed()) return;\n if (w.isMinimized()) w.restore();\n w.setAlwaysOnTop(true);\n w.setBounds({ ...def, ...centeredOnPrimary(def) });\n w.show();\n}\n\nexport interface ToolWindowOptions {\n title: string;\n /** Renderer entry, relative to rendererDir (e.g. \"table.html\", \"search/index.html\"). */\n page: string;\n /** Absolute path of the built renderer dir (join(import.meta.dirname, \"../renderer\")). */\n rendererDir: string;\n /** Absolute path of the preload bridge. */\n preload: string;\n rect: ToolWindowBounds;\n min?: ToolWindowSize;\n /** false = frameless: the renderer draws its own slim drag bar (.swin-head). */\n frame?: boolean;\n /** Floats over the editor (the pin); remembered by the host. */\n alwaysOnTop?: boolean;\n /** Wire debounced bounds persistence into the host's store slice. */\n remember?: (bounds: ToolWindowBounds) => void;\n /** Fired when THIS window closes (identity-guarded; a stale close never\n * clears a newer window). Null the host's handle here. */\n onClosed?: () => void;\n}\n\n/** The pinnable detached-window factory (hardened webPreferences, one\n * preload, electron-vite's dev-URL/file split). Focuses and returns the\n * existing window when there is one; otherwise creates, wires, and loads. */\nexport function openToolWindow(existing: BrowserWindow | undefined | null, opts: ToolWindowOptions): BrowserWindow {\n if (existing && !existing.isDestroyed()) { existing.focus(); return existing; }\n const w = new BrowserWindow({\n ...opts.rect,\n ...(opts.min !== undefined ? { minWidth: opts.min.width, minHeight: opts.min.height } : {}),\n show: false,\n title: opts.title,\n ...(opts.frame !== undefined ? { frame: opts.frame } : {}),\n ...(opts.alwaysOnTop !== undefined ? { alwaysOnTop: opts.alwaysOnTop } : {}),\n webPreferences: {\n contextIsolation: true, nodeIntegration: false, sandbox: true,\n preload: opts.preload,\n },\n });\n if (opts.remember) rememberBounds(w, opts.remember);\n w.once(\"ready-to-show\", () => w.show());\n if (opts.onClosed) w.on(\"closed\", opts.onClosed);\n if (process.env[\"ELECTRON_RENDERER_URL\"]) void w.loadURL(`${process.env[\"ELECTRON_RENDERER_URL\"]}/${opts.page}`);\n else void w.loadFile(join(opts.rendererDir, opts.page));\n return w;\n}\n"]}
@@ -0,0 +1,60 @@
1
+ /* src/tool-window.css */
2
+ .swin-head {
3
+ -webkit-app-region: drag;
4
+ display: flex;
5
+ align-items: center;
6
+ gap: 6px;
7
+ padding: 7px 10px;
8
+ background: var(--surface);
9
+ border-bottom: 1px solid var(--line-soft, var(--line));
10
+ }
11
+ .swin-head button,
12
+ .swin-head select,
13
+ .swin-head input {
14
+ -webkit-app-region: no-drag;
15
+ }
16
+ .swin-title {
17
+ font: 600 0.68rem/1 var(--font-ui);
18
+ text-transform: uppercase;
19
+ letter-spacing: 0.07em;
20
+ color: var(--muted);
21
+ }
22
+ .swin-spacer {
23
+ flex: 1;
24
+ }
25
+ .swin-pin,
26
+ .swin-close {
27
+ flex: none;
28
+ display: inline-flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ background: none;
32
+ border: 1px solid transparent;
33
+ border-radius: var(--radius-sm);
34
+ cursor: pointer;
35
+ color: var(--muted);
36
+ line-height: 0;
37
+ padding: 4px;
38
+ transition:
39
+ background var(--dur-quick) var(--ease-standard),
40
+ border-color var(--dur-quick) var(--ease-standard),
41
+ color var(--dur-quick) var(--ease-standard);
42
+ }
43
+ .swin-pin:hover,
44
+ .swin-close:hover {
45
+ border-color: var(--line);
46
+ color: var(--ink);
47
+ }
48
+ .swin-pin svg {
49
+ display: block;
50
+ }
51
+ .swin-pin.on {
52
+ color: var(--accent);
53
+ border-color: var(--accent);
54
+ background: color-mix(in oklab, var(--accent) 12%, transparent);
55
+ }
56
+ .swin-close {
57
+ font: 0.8rem var(--font-ui);
58
+ padding: 2px 7px;
59
+ }
60
+ /*# sourceMappingURL=tool-window.css.map */
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tool-window.css"],"sourcesContent":["/* ---------------------------------------------------------------------------\n The tool-window kit, CHROME grammar: the slim drag bar a frameless helper\n window draws in place of the OS title bar, and the pin / close buttons any\n helper (framed or not) carries. Consumes the host palette (--surface /\n --ink / --muted / --line / --line-soft / --accent / --font-ui) over the\n shared tokens. Content styles stay app-side; this is the chrome only.\n --------------------------------------------------------------------------- */\n\n/* The drag bar standing in for the OS title bar (frame: false). Buttons and\n other controls inside it opt out so they stay clickable. */\n.swin-head {\n -webkit-app-region: drag;\n display: flex; align-items: center; gap: 6px;\n padding: 7px 10px;\n background: var(--surface);\n /* --line-soft is an optional host token (Storyletter defines it; Patterpad\n falls back to --line). */\n border-bottom: 1px solid var(--line-soft, var(--line));\n}\n.swin-head button, .swin-head select, .swin-head input { -webkit-app-region: no-drag; }\n.swin-title {\n font: 600 0.68rem/1 var(--font-ui);\n text-transform: uppercase; letter-spacing: 0.07em;\n color: var(--muted);\n}\n.swin-spacer { flex: 1; }\n\n/* The pin + close pair; motion is interpolation, never a jump. */\n.swin-pin, .swin-close {\n flex: none; display: inline-flex; align-items: center; justify-content: center;\n background: none; border: 1px solid transparent; border-radius: var(--radius-sm);\n cursor: pointer; color: var(--muted); line-height: 0; padding: 4px;\n transition: background var(--dur-quick) var(--ease-standard),\n border-color var(--dur-quick) var(--ease-standard),\n color var(--dur-quick) var(--ease-standard);\n}\n.swin-pin:hover, .swin-close:hover { border-color: var(--line); color: var(--ink); }\n.swin-pin svg { display: block; }\n.swin-pin.on {\n color: var(--accent); border-color: var(--accent);\n background: color-mix(in oklab, var(--accent) 12%, transparent);\n}\n.swin-close { font: 0.8rem var(--font-ui); padding: 2px 7px; }\n"],"mappings":";AAUA,CAAC;AACC,sBAAoB;AACpB,WAAS;AAAM,eAAa;AAAQ,OAAK;AACzC,WAAS,IAAI;AACb,cAAY,IAAI;AAGhB,iBAAe,IAAI,MAAM,IAAI,WAAW,EAAE,IAAI;AAChD;AACA,CATC,UASU;AAAQ,CATlB,UAS6B;AAAQ,CATrC,UASgD;AAAQ,sBAAoB;AAAS;AACtF,CAAC;AACC,QAAM,IAAI,OAAO,CAAC,EAAE,IAAI;AACxB,kBAAgB;AAAW,kBAAgB;AAC3C,SAAO,IAAI;AACb;AACA,CAAC;AAAc,QAAM;AAAG;AAGxB,CAAC;AAAU,CAAC;AACV,QAAM;AAAM,WAAS;AAAa,eAAa;AAAQ,mBAAiB;AACxE,cAAY;AAAM,UAAQ,IAAI,MAAM;AAAa,iBAAe,IAAI;AACpE,UAAQ;AAAS,SAAO,IAAI;AAAU,eAAa;AAAG,WAAS;AAC/D;AAAA,IAAY,WAAW,IAAI,aAAa,IAAI,gBAAgB;AAAA,IAC1D,aAAa,IAAI,aAAa,IAAI,gBAAgB;AAAA,IAClD,MAAM,IAAI,aAAa,IAAI;AAC/B;AACA,CARC,QAQQ;AAAQ,CARL,UAQgB;AAAS,gBAAc,IAAI;AAAS,SAAO,IAAI;AAAQ;AACnF,CATC,SASS;AAAM,WAAS;AAAO;AAChC,CAVC,QAUQ,CAAC;AACR,SAAO,IAAI;AAAW,gBAAc,IAAI;AACxC,cAAY,UAAU,GAAG,KAAK,EAAE,IAAI,UAAU,GAAG,EAAE;AACrD;AACA,CAdY;AAcE,QAAM,OAAO,IAAI;AAAY,WAAS,IAAI;AAAK;","names":[]}
@@ -0,0 +1,63 @@
1
+ import { BrowserWindow } from 'electron';
2
+
3
+ interface ToolWindowSize {
4
+ width: number;
5
+ height: number;
6
+ }
7
+ interface ToolWindowBounds {
8
+ x?: number;
9
+ y?: number;
10
+ width: number;
11
+ height: number;
12
+ }
13
+ /** The per-window store slice every app remembers: where it was, whether it
14
+ * floats. (Patterpad's PlayWindowState, generalised.) */
15
+ interface ToolWindowState {
16
+ bounds?: ToolWindowBounds;
17
+ pinned: boolean;
18
+ }
19
+ /** Use a remembered rect only if it still intersects a live display (a window
20
+ * saved on a now-disconnected monitor must not open offscreen). Falls back
21
+ * to the default size; a remembered size is kept even when the position is
22
+ * dropped (Electron then centres it). */
23
+ declare function savedWindowRect(saved: ToolWindowBounds | undefined, def: ToolWindowSize, min: ToolWindowSize): ToolWindowBounds;
24
+ /** Persist a helper window's bounds as the user moves / resizes / closes it
25
+ * (debounced 400ms; close flushes synchronously). */
26
+ declare function rememberBounds(w: BrowserWindow, write: (bounds: ToolWindowBounds) => void): void;
27
+ /** Centre a rect on the primary display's work area. */
28
+ declare function centeredOnPrimary(size: ToolWindowSize): {
29
+ x: number;
30
+ y: number;
31
+ };
32
+ /** Reset View's per-window half: un-minimise, re-pin (always - the store
33
+ * says pinned:true after a rescue, so the window must agree), default size,
34
+ * centred on the primary display, shown. The host clears its remembered
35
+ * bounds itself (that's a store concern). No focus steal: helpers show
36
+ * behind the editor. */
37
+ declare function rescueToolWindow(w: BrowserWindow | undefined | null, def: ToolWindowSize): void;
38
+ interface ToolWindowOptions {
39
+ title: string;
40
+ /** Renderer entry, relative to rendererDir (e.g. "table.html", "search/index.html"). */
41
+ page: string;
42
+ /** Absolute path of the built renderer dir (join(import.meta.dirname, "../renderer")). */
43
+ rendererDir: string;
44
+ /** Absolute path of the preload bridge. */
45
+ preload: string;
46
+ rect: ToolWindowBounds;
47
+ min?: ToolWindowSize;
48
+ /** false = frameless: the renderer draws its own slim drag bar (.swin-head). */
49
+ frame?: boolean;
50
+ /** Floats over the editor (the pin); remembered by the host. */
51
+ alwaysOnTop?: boolean;
52
+ /** Wire debounced bounds persistence into the host's store slice. */
53
+ remember?: (bounds: ToolWindowBounds) => void;
54
+ /** Fired when THIS window closes (identity-guarded; a stale close never
55
+ * clears a newer window). Null the host's handle here. */
56
+ onClosed?: () => void;
57
+ }
58
+ /** The pinnable detached-window factory (hardened webPreferences, one
59
+ * preload, electron-vite's dev-URL/file split). Focuses and returns the
60
+ * existing window when there is one; otherwise creates, wires, and loads. */
61
+ declare function openToolWindow(existing: BrowserWindow | undefined | null, opts: ToolWindowOptions): BrowserWindow;
62
+
63
+ export { type ToolWindowBounds, type ToolWindowOptions, type ToolWindowSize, type ToolWindowState, centeredOnPrimary, openToolWindow, rememberBounds, rescueToolWindow, savedWindowRect };
@@ -0,0 +1,63 @@
1
+ import { BrowserWindow } from 'electron';
2
+
3
+ interface ToolWindowSize {
4
+ width: number;
5
+ height: number;
6
+ }
7
+ interface ToolWindowBounds {
8
+ x?: number;
9
+ y?: number;
10
+ width: number;
11
+ height: number;
12
+ }
13
+ /** The per-window store slice every app remembers: where it was, whether it
14
+ * floats. (Patterpad's PlayWindowState, generalised.) */
15
+ interface ToolWindowState {
16
+ bounds?: ToolWindowBounds;
17
+ pinned: boolean;
18
+ }
19
+ /** Use a remembered rect only if it still intersects a live display (a window
20
+ * saved on a now-disconnected monitor must not open offscreen). Falls back
21
+ * to the default size; a remembered size is kept even when the position is
22
+ * dropped (Electron then centres it). */
23
+ declare function savedWindowRect(saved: ToolWindowBounds | undefined, def: ToolWindowSize, min: ToolWindowSize): ToolWindowBounds;
24
+ /** Persist a helper window's bounds as the user moves / resizes / closes it
25
+ * (debounced 400ms; close flushes synchronously). */
26
+ declare function rememberBounds(w: BrowserWindow, write: (bounds: ToolWindowBounds) => void): void;
27
+ /** Centre a rect on the primary display's work area. */
28
+ declare function centeredOnPrimary(size: ToolWindowSize): {
29
+ x: number;
30
+ y: number;
31
+ };
32
+ /** Reset View's per-window half: un-minimise, re-pin (always - the store
33
+ * says pinned:true after a rescue, so the window must agree), default size,
34
+ * centred on the primary display, shown. The host clears its remembered
35
+ * bounds itself (that's a store concern). No focus steal: helpers show
36
+ * behind the editor. */
37
+ declare function rescueToolWindow(w: BrowserWindow | undefined | null, def: ToolWindowSize): void;
38
+ interface ToolWindowOptions {
39
+ title: string;
40
+ /** Renderer entry, relative to rendererDir (e.g. "table.html", "search/index.html"). */
41
+ page: string;
42
+ /** Absolute path of the built renderer dir (join(import.meta.dirname, "../renderer")). */
43
+ rendererDir: string;
44
+ /** Absolute path of the preload bridge. */
45
+ preload: string;
46
+ rect: ToolWindowBounds;
47
+ min?: ToolWindowSize;
48
+ /** false = frameless: the renderer draws its own slim drag bar (.swin-head). */
49
+ frame?: boolean;
50
+ /** Floats over the editor (the pin); remembered by the host. */
51
+ alwaysOnTop?: boolean;
52
+ /** Wire debounced bounds persistence into the host's store slice. */
53
+ remember?: (bounds: ToolWindowBounds) => void;
54
+ /** Fired when THIS window closes (identity-guarded; a stale close never
55
+ * clears a newer window). Null the host's handle here. */
56
+ onClosed?: () => void;
57
+ }
58
+ /** The pinnable detached-window factory (hardened webPreferences, one
59
+ * preload, electron-vite's dev-URL/file split). Focuses and returns the
60
+ * existing window when there is one; otherwise creates, wires, and loads. */
61
+ declare function openToolWindow(existing: BrowserWindow | undefined | null, opts: ToolWindowOptions): BrowserWindow;
62
+
63
+ export { type ToolWindowBounds, type ToolWindowOptions, type ToolWindowSize, type ToolWindowState, centeredOnPrimary, openToolWindow, rememberBounds, rescueToolWindow, savedWindowRect };