@ai-matrx/canvas 0.1.1 → 0.2.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/CHANGELOG.md +26 -0
- package/README.md +114 -4
- package/dist/controller-Dv6J6XZm.d.cts +576 -0
- package/dist/controller-Dv6J6XZm.d.ts +576 -0
- package/dist/index.cjs +743 -104
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +118 -11
- package/dist/index.d.ts +118 -11
- package/dist/index.js +743 -104
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1476 -406
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +242 -22
- package/dist/react.d.ts +242 -22
- package/dist/react.js +1486 -404
- package/dist/react.js.map +1 -1
- package/dist/styles.css +346 -51
- package/dist/tokens.css +58 -36
- package/package.json +1 -1
- package/dist/controller-oY84BN2c.d.cts +0 -345
- package/dist/controller-oY84BN2c.d.ts +0 -345
package/dist/index.cjs
CHANGED
|
@@ -21,26 +21,46 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
23
|
CANVAS_DEFAULT_WIDTH: () => CANVAS_DEFAULT_WIDTH,
|
|
24
|
+
CANVAS_DESKTOP_LAYOUT_RULES: () => CANVAS_DESKTOP_LAYOUT_RULES,
|
|
25
|
+
CANVAS_MAIN_WINDOW: () => CANVAS_MAIN_WINDOW,
|
|
24
26
|
CANVAS_MIN_SPLIT_FRACTION: () => CANVAS_MIN_SPLIT_FRACTION,
|
|
25
27
|
CANVAS_MIN_WIDTH: () => CANVAS_MIN_WIDTH,
|
|
28
|
+
CANVAS_SHORTCUTS: () => CANVAS_SHORTCUTS,
|
|
26
29
|
CANVAS_STORAGE_KEY: () => CANVAS_STORAGE_KEY,
|
|
30
|
+
CANVAS_WEB_LAYOUT_RULES: () => CANVAS_WEB_LAYOUT_RULES,
|
|
27
31
|
bindCanvasToReduxStore: () => bindCanvasToReduxStore,
|
|
28
32
|
canvasActions: () => canvasActions,
|
|
29
33
|
canvasItemId: () => canvasItemId,
|
|
30
34
|
canvasReducer: () => canvasReducer,
|
|
35
|
+
canvasShortcutHint: () => canvasShortcutHint,
|
|
36
|
+
canvasWindowFrame: () => canvasWindowFrame,
|
|
37
|
+
canvasWindowId: () => canvasWindowId,
|
|
38
|
+
canvasWindowOfItem: () => canvasWindowOfItem,
|
|
39
|
+
canvasWindowOfPane: () => canvasWindowOfPane,
|
|
40
|
+
clampDraggedCanvasWidth: () => clampDraggedCanvasWidth,
|
|
31
41
|
consoleCanvasErrorSink: () => consoleCanvasErrorSink,
|
|
32
42
|
createCanvasController: () => createCanvasController,
|
|
33
43
|
createCanvasStore: () => createCanvasStore,
|
|
34
44
|
createInitialCanvasState: () => createInitialCanvasState,
|
|
45
|
+
createKeyValueCanvasPersistence: () => createKeyValueCanvasPersistence,
|
|
35
46
|
createLocalStorageCanvasPersistence: () => createLocalStorageCanvasPersistence,
|
|
47
|
+
createRemoteCanvasStore: () => createRemoteCanvasStore,
|
|
48
|
+
defaultCanvasWidth: () => defaultCanvasWidth,
|
|
36
49
|
findNonJson: () => findNonJson,
|
|
50
|
+
fitCanvasWidth: () => fitCanvasWidth,
|
|
51
|
+
foldCanvasWindows: () => foldCanvasWindows,
|
|
37
52
|
isCanvasAction: () => isCanvasAction,
|
|
38
53
|
isCanvasJson: () => isCanvasJson,
|
|
54
|
+
listCanvasWindowIds: () => listCanvasWindowIds,
|
|
39
55
|
listPaneIds: () => listPaneIds,
|
|
56
|
+
matchesCanvasAccelerator: () => matchesCanvasAccelerator,
|
|
40
57
|
normalizeSizes: () => normalizeSizes,
|
|
41
58
|
parseCanvasItemId: () => parseCanvasItemId,
|
|
59
|
+
resolveCanvasLayoutRules: () => resolveCanvasLayoutRules,
|
|
60
|
+
runCanvasCommand: () => runCanvasCommand,
|
|
42
61
|
sanitizeCanvasSnapshot: () => sanitizeCanvasSnapshot,
|
|
43
62
|
selectCanvasActiveItem: () => selectCanvasActiveItem,
|
|
63
|
+
selectCanvasExtraWindow: () => selectCanvasExtraWindow,
|
|
44
64
|
selectCanvasFocusedPaneId: () => selectCanvasFocusedPaneId,
|
|
45
65
|
selectCanvasIsFullscreen: () => selectCanvasIsFullscreen,
|
|
46
66
|
selectCanvasIsHydrated: () => selectCanvasIsHydrated,
|
|
@@ -51,11 +71,17 @@ __export(src_exports, {
|
|
|
51
71
|
selectCanvasPane: () => selectCanvasPane,
|
|
52
72
|
selectCanvasPaneCount: () => selectCanvasPaneCount,
|
|
53
73
|
selectCanvasWidth: () => selectCanvasWidth,
|
|
74
|
+
selectCanvasWindowItemCount: () => selectCanvasWindowItemCount,
|
|
75
|
+
selectCanvasWindowOfItem: () => selectCanvasWindowOfItem,
|
|
76
|
+
selectCanvasWindows: () => selectCanvasWindows,
|
|
77
|
+
shareStructure: () => shareStructure,
|
|
78
|
+
shouldStackSplit: () => shouldStackSplit,
|
|
54
79
|
toPersistableSnapshot: () => toPersistableSnapshot
|
|
55
80
|
});
|
|
56
81
|
module.exports = __toCommonJS(src_exports);
|
|
57
82
|
|
|
58
83
|
// src/core/types.ts
|
|
84
|
+
var CANVAS_MAIN_WINDOW = "main";
|
|
59
85
|
var CANVAS_MIN_WIDTH = 360;
|
|
60
86
|
var CANVAS_DEFAULT_WIDTH = 640;
|
|
61
87
|
var CANVAS_MIN_SPLIT_FRACTION = 0.12;
|
|
@@ -107,12 +133,19 @@ function findNonJson(value, path = "data") {
|
|
|
107
133
|
function isCanvasJson(value) {
|
|
108
134
|
return findNonJson(value) === null;
|
|
109
135
|
}
|
|
136
|
+
function canvasWindowId(id) {
|
|
137
|
+
return id;
|
|
138
|
+
}
|
|
110
139
|
|
|
111
140
|
// src/core/layout.ts
|
|
112
141
|
function listPaneIds(node) {
|
|
113
142
|
if (node.type === "pane") return [node.paneId];
|
|
114
143
|
return node.children.flatMap(listPaneIds);
|
|
115
144
|
}
|
|
145
|
+
function containsPane(node, paneId) {
|
|
146
|
+
if (node.type === "pane") return node.paneId === paneId;
|
|
147
|
+
return node.children.some((child) => containsPane(child, paneId));
|
|
148
|
+
}
|
|
116
149
|
function evenSizes(count) {
|
|
117
150
|
return Array.from({ length: count }, () => 1 / count);
|
|
118
151
|
}
|
|
@@ -123,7 +156,8 @@ function normalizeSizes(sizes, count) {
|
|
|
123
156
|
const total = sizes.reduce((sum, s) => sum + s, 0);
|
|
124
157
|
return sizes.map((s) => s / total);
|
|
125
158
|
}
|
|
126
|
-
function splitPane(node, paneId, newPaneId, orientation, newSplitId) {
|
|
159
|
+
function splitPane(node, paneId, newPaneId, orientation, newSplitId, firstShare = 0.5) {
|
|
160
|
+
const first = Number.isFinite(firstShare) ? Math.min(1 - CANVAS_MIN_SPLIT_FRACTION, Math.max(CANVAS_MIN_SPLIT_FRACTION, firstShare)) : 0.5;
|
|
127
161
|
if (node.type === "pane") {
|
|
128
162
|
if (node.paneId !== paneId) return node;
|
|
129
163
|
return {
|
|
@@ -131,7 +165,7 @@ function splitPane(node, paneId, newPaneId, orientation, newSplitId) {
|
|
|
131
165
|
id: newSplitId,
|
|
132
166
|
orientation,
|
|
133
167
|
children: [node, { type: "pane", paneId: newPaneId }],
|
|
134
|
-
sizes: [
|
|
168
|
+
sizes: [first, 1 - first]
|
|
135
169
|
};
|
|
136
170
|
}
|
|
137
171
|
const index = node.children.findIndex(
|
|
@@ -142,13 +176,13 @@ function splitPane(node, paneId, newPaneId, orientation, newSplitId) {
|
|
|
142
176
|
const children = [...node.children];
|
|
143
177
|
children.splice(index + 1, 0, { type: "pane", paneId: newPaneId });
|
|
144
178
|
const sizes = [...node.sizes];
|
|
145
|
-
sizes.splice(index, 1, share
|
|
179
|
+
sizes.splice(index, 1, share * first, share * (1 - first));
|
|
146
180
|
return { ...node, children, sizes: normalizeSizes(sizes, children.length) };
|
|
147
181
|
}
|
|
148
182
|
return {
|
|
149
183
|
...node,
|
|
150
184
|
children: node.children.map(
|
|
151
|
-
(child) => splitPane(child, paneId, newPaneId, orientation, newSplitId)
|
|
185
|
+
(child) => splitPane(child, paneId, newPaneId, orientation, newSplitId, first)
|
|
152
186
|
)
|
|
153
187
|
};
|
|
154
188
|
}
|
|
@@ -167,6 +201,10 @@ function removePane(node, paneId) {
|
|
|
167
201
|
if (children.length === 1) return children[0] ?? null;
|
|
168
202
|
return { ...node, children, sizes: normalizeSizes(sizes, children.length) };
|
|
169
203
|
}
|
|
204
|
+
function containsSplit(node, splitId) {
|
|
205
|
+
if (node.type === "pane") return false;
|
|
206
|
+
return node.id === splitId || node.children.some((child) => containsSplit(child, splitId));
|
|
207
|
+
}
|
|
170
208
|
function resizeSplit(node, splitId, sizes) {
|
|
171
209
|
if (node.type === "pane") return node;
|
|
172
210
|
if (node.id === splitId) {
|
|
@@ -197,8 +235,12 @@ function isValidLayout(node, knownPanes, seen = /* @__PURE__ */ new Set()) {
|
|
|
197
235
|
|
|
198
236
|
// src/core/reducer.ts
|
|
199
237
|
var P = "matrxCanvas/";
|
|
238
|
+
var win = (windowId) => windowId && windowId !== CANVAS_MAIN_WINDOW ? { windowId } : {};
|
|
200
239
|
var canvasActions = {
|
|
201
|
-
open: (input) => ({
|
|
240
|
+
open: (input, options) => ({
|
|
241
|
+
type: `${P}open`,
|
|
242
|
+
payload: { ...input, now: Date.now(), ...options?.splitShare !== void 0 ? { splitShare: options.splitShare } : {} }
|
|
243
|
+
}),
|
|
202
244
|
update: (itemId, patch) => ({
|
|
203
245
|
type: `${P}update`,
|
|
204
246
|
payload: { itemId, ...patch, now: Date.now() }
|
|
@@ -213,19 +255,32 @@ var canvasActions = {
|
|
|
213
255
|
type: `${P}moveItem`,
|
|
214
256
|
payload: { itemId, toPaneId, index }
|
|
215
257
|
}),
|
|
216
|
-
|
|
258
|
+
/** `share` = the existing pane's fraction of the space (layout rules' stackSplit / sideSplit). */
|
|
259
|
+
splitPane: (paneId, orientation, moveItemId, share) => ({
|
|
217
260
|
type: `${P}splitPane`,
|
|
218
|
-
payload: { paneId, orientation, moveItemId }
|
|
261
|
+
payload: { paneId, orientation, moveItemId, ...share !== void 0 ? { share } : {} }
|
|
219
262
|
}),
|
|
220
263
|
closePane: (paneId) => ({ type: `${P}closePane`, payload: { paneId } }),
|
|
221
264
|
resizeSplit: (splitId, sizes) => ({
|
|
222
265
|
type: `${P}resizeSplit`,
|
|
223
266
|
payload: { splitId, sizes }
|
|
224
267
|
}),
|
|
225
|
-
setOpen: (open) => ({ type: `${P}setOpen`, payload: { open } }),
|
|
226
|
-
toggle: () =>
|
|
227
|
-
setFullscreen: (fullscreen) => ({
|
|
228
|
-
|
|
268
|
+
setOpen: (open, windowId) => ({ type: `${P}setOpen`, payload: { open, ...win(windowId) } }),
|
|
269
|
+
toggle: (windowId) => windowId && windowId !== CANVAS_MAIN_WINDOW ? { type: `${P}toggle`, payload: { windowId } } : { type: `${P}toggle` },
|
|
270
|
+
setFullscreen: (fullscreen, windowId) => ({
|
|
271
|
+
type: `${P}setFullscreen`,
|
|
272
|
+
payload: { fullscreen, ...win(windowId) }
|
|
273
|
+
}),
|
|
274
|
+
setWidth: (width, windowId) => ({ type: `${P}setWidth`, payload: { width, ...win(windowId) } }),
|
|
275
|
+
/** Moves a tab into its own pop-out window `windowId` (made by the host's popOut port). */
|
|
276
|
+
popOut: (itemId, windowId) => ({ type: `${P}popOut`, payload: { itemId, windowId } }),
|
|
277
|
+
/** Returns a popped-out tab to the window it came from. */
|
|
278
|
+
dockBack: (itemId) => ({ type: `${P}dockBack`, payload: { itemId } }),
|
|
279
|
+
/** A host window closed: its tabs dock home (default) or close with it. The main window never closes. */
|
|
280
|
+
closeWindow: (windowId, mode = "dock") => ({
|
|
281
|
+
type: `${P}closeWindow`,
|
|
282
|
+
payload: { windowId, mode }
|
|
283
|
+
}),
|
|
229
284
|
hydrate: (snapshot) => ({ type: `${P}hydrate`, payload: { snapshot } }),
|
|
230
285
|
reset: () => ({ type: `${P}reset` })
|
|
231
286
|
};
|
|
@@ -238,15 +293,80 @@ function createInitialCanvasState() {
|
|
|
238
293
|
version: 1,
|
|
239
294
|
isOpen: false,
|
|
240
295
|
isFullscreen: false,
|
|
241
|
-
width:
|
|
296
|
+
width: null,
|
|
242
297
|
layout: { type: "pane", paneId },
|
|
298
|
+
focusedPaneId: paneId,
|
|
299
|
+
windows: {},
|
|
243
300
|
panes: { [paneId]: { id: paneId, itemIds: [], activeItemId: null } },
|
|
244
301
|
items: {},
|
|
245
|
-
focusedPaneId: paneId,
|
|
246
302
|
seq: 1,
|
|
247
303
|
hydrated: false
|
|
248
304
|
};
|
|
249
305
|
}
|
|
306
|
+
function listCanvasWindowIds(state) {
|
|
307
|
+
return [CANVAS_MAIN_WINDOW, ...Object.keys(state.windows ?? {})];
|
|
308
|
+
}
|
|
309
|
+
function canvasWindowFrame(state, windowId = CANVAS_MAIN_WINDOW) {
|
|
310
|
+
return windowId === CANVAS_MAIN_WINDOW ? state : state.windows?.[windowId] ?? null;
|
|
311
|
+
}
|
|
312
|
+
function canvasWindowOfPane(state, paneId) {
|
|
313
|
+
for (const id of listCanvasWindowIds(state)) {
|
|
314
|
+
const frame2 = canvasWindowFrame(state, id);
|
|
315
|
+
if (frame2 && containsPane(frame2.layout, paneId)) return id;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
function canvasWindowOfItem(state, itemId) {
|
|
320
|
+
const pane = paneOf(state, itemId);
|
|
321
|
+
return pane ? canvasWindowOfPane(state, pane.id) : null;
|
|
322
|
+
}
|
|
323
|
+
function withFrame(state, windowId, patch) {
|
|
324
|
+
if (windowId === CANVAS_MAIN_WINDOW) return { ...state, ...patch };
|
|
325
|
+
const current = state.windows[windowId];
|
|
326
|
+
if (!current) return state;
|
|
327
|
+
return { ...state, windows: { ...state.windows, [windowId]: { ...current, ...patch } } };
|
|
328
|
+
}
|
|
329
|
+
function ensureWindow(state, windowId, role = "window", home = CANVAS_MAIN_WINDOW) {
|
|
330
|
+
if (windowId === CANVAS_MAIN_WINDOW || state.windows[windowId]) return state;
|
|
331
|
+
const seq = state.seq + 1;
|
|
332
|
+
const paneId = paneIdFromSeq(seq);
|
|
333
|
+
const frame2 = {
|
|
334
|
+
id: windowId,
|
|
335
|
+
role,
|
|
336
|
+
home,
|
|
337
|
+
isOpen: false,
|
|
338
|
+
isFullscreen: false,
|
|
339
|
+
width: null,
|
|
340
|
+
layout: { type: "pane", paneId },
|
|
341
|
+
focusedPaneId: paneId
|
|
342
|
+
};
|
|
343
|
+
return {
|
|
344
|
+
...state,
|
|
345
|
+
seq,
|
|
346
|
+
panes: { ...state.panes, [paneId]: { id: paneId, itemIds: [], activeItemId: null } },
|
|
347
|
+
windows: { ...state.windows, [windowId]: frame2 }
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function removeWindow(state, windowId) {
|
|
351
|
+
const frame2 = state.windows[windowId];
|
|
352
|
+
if (!frame2) return state;
|
|
353
|
+
const panes = { ...state.panes };
|
|
354
|
+
for (const paneId of listPaneIds(frame2.layout)) delete panes[paneId];
|
|
355
|
+
const windows = { ...state.windows };
|
|
356
|
+
delete windows[windowId];
|
|
357
|
+
return { ...state, panes, windows };
|
|
358
|
+
}
|
|
359
|
+
function windowItemIds(state, windowId) {
|
|
360
|
+
const frame2 = canvasWindowFrame(state, windowId);
|
|
361
|
+
if (!frame2) return [];
|
|
362
|
+
return listPaneIds(frame2.layout).flatMap((paneId) => state.panes[paneId]?.itemIds ?? []);
|
|
363
|
+
}
|
|
364
|
+
function dropEmptyPopOut(state, windowId) {
|
|
365
|
+
if (!windowId || windowId === CANVAS_MAIN_WINDOW) return state;
|
|
366
|
+
const frame2 = state.windows[windowId];
|
|
367
|
+
if (!frame2 || frame2.role !== "pop-out" || windowItemIds(state, windowId).length > 0) return state;
|
|
368
|
+
return removeWindow(state, windowId);
|
|
369
|
+
}
|
|
250
370
|
function paneOf(state, itemId) {
|
|
251
371
|
for (const pane of Object.values(state.panes)) {
|
|
252
372
|
if (pane.itemIds.includes(itemId)) return pane;
|
|
@@ -256,6 +376,10 @@ function paneOf(state, itemId) {
|
|
|
256
376
|
function withPane(state, pane) {
|
|
257
377
|
return { ...state, panes: { ...state.panes, [pane.id]: pane } };
|
|
258
378
|
}
|
|
379
|
+
function focus(state, paneId) {
|
|
380
|
+
const windowId = canvasWindowOfPane(state, paneId);
|
|
381
|
+
return windowId ? withFrame(state, windowId, { focusedPaneId: paneId }) : state;
|
|
382
|
+
}
|
|
259
383
|
function nextActive(itemIds, removed, current) {
|
|
260
384
|
if (current !== removed) return current;
|
|
261
385
|
const index = itemIds.indexOf(removed);
|
|
@@ -272,27 +396,29 @@ function detachItem(state, itemId) {
|
|
|
272
396
|
});
|
|
273
397
|
}
|
|
274
398
|
function dropPane(state, paneId) {
|
|
275
|
-
const
|
|
399
|
+
const windowId = canvasWindowOfPane(state, paneId);
|
|
400
|
+
const frame2 = windowId ? canvasWindowFrame(state, windowId) : null;
|
|
401
|
+
if (!windowId || !frame2) return state;
|
|
402
|
+
const layout = removePane(frame2.layout, paneId);
|
|
276
403
|
if (!layout) return state;
|
|
277
404
|
const panes = { ...state.panes };
|
|
278
405
|
delete panes[paneId];
|
|
279
|
-
const focusedPaneId =
|
|
280
|
-
return { ...state,
|
|
406
|
+
const focusedPaneId = frame2.focusedPaneId === paneId ? listPaneIds(layout)[0] ?? frame2.focusedPaneId : frame2.focusedPaneId;
|
|
407
|
+
return withFrame({ ...state, panes }, windowId, { layout, focusedPaneId });
|
|
281
408
|
}
|
|
282
|
-
function addSplit(state, paneId, orientation) {
|
|
409
|
+
function addSplit(state, paneId, orientation, share) {
|
|
410
|
+
const windowId = canvasWindowOfPane(state, paneId);
|
|
411
|
+
const frame2 = windowId ? canvasWindowFrame(state, windowId) : null;
|
|
412
|
+
if (!windowId || !frame2) return null;
|
|
283
413
|
const seq = state.seq + 1;
|
|
284
414
|
const newPaneId = paneIdFromSeq(seq);
|
|
285
|
-
const layout = splitPane(
|
|
286
|
-
|
|
287
|
-
newPaneId,
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
panes: { ...state.panes, [newPaneId]: { id: newPaneId, itemIds: [], activeItemId: null } },
|
|
293
|
-
focusedPaneId: newPaneId
|
|
294
|
-
}
|
|
295
|
-
};
|
|
415
|
+
const layout = splitPane(frame2.layout, paneId, newPaneId, orientation, splitIdFromSeq(seq), share ?? 0.5);
|
|
416
|
+
const next = withFrame(
|
|
417
|
+
{ ...state, seq, panes: { ...state.panes, [newPaneId]: { id: newPaneId, itemIds: [], activeItemId: null } } },
|
|
418
|
+
windowId,
|
|
419
|
+
{ layout, focusedPaneId: newPaneId }
|
|
420
|
+
);
|
|
421
|
+
return { newPaneId, state: next };
|
|
296
422
|
}
|
|
297
423
|
function insertInto(state, paneId, itemId, activate, index) {
|
|
298
424
|
const pane = state.panes[paneId];
|
|
@@ -310,46 +436,96 @@ function afterActive(itemIds, active) {
|
|
|
310
436
|
const index = active ? itemIds.indexOf(active) : -1;
|
|
311
437
|
return index < 0 ? itemIds.length : index + 1;
|
|
312
438
|
}
|
|
439
|
+
function sendHome(state, itemId, home, activate) {
|
|
440
|
+
const homeFrame = canvasWindowFrame(state, home) ?? state;
|
|
441
|
+
const target = canvasWindowFrame(state, home) ? home : CANVAS_MAIN_WINDOW;
|
|
442
|
+
let next = detachItem(state, itemId);
|
|
443
|
+
next = insertInto(next, homeFrame.focusedPaneId, itemId, activate);
|
|
444
|
+
return activate ? withFrame(next, target, { focusedPaneId: homeFrame.focusedPaneId }) : next;
|
|
445
|
+
}
|
|
313
446
|
function clampWidth(width) {
|
|
314
|
-
return Number.isFinite(width) ? Math.max(CANVAS_MIN_WIDTH, Math.round(width)) :
|
|
447
|
+
return Number.isFinite(width) ? Math.max(CANVAS_MIN_WIDTH, Math.round(width)) : null;
|
|
448
|
+
}
|
|
449
|
+
function foldCanvasWindows(state) {
|
|
450
|
+
const extra = Object.keys(state.windows ?? {});
|
|
451
|
+
if (extra.length === 0) return state.windows ? state : { ...state, windows: {} };
|
|
452
|
+
let next = state;
|
|
453
|
+
for (const windowId of extra) {
|
|
454
|
+
for (const itemId of windowItemIds(next, windowId)) next = sendHome(next, itemId, CANVAS_MAIN_WINDOW, false);
|
|
455
|
+
next = removeWindow(next, windowId);
|
|
456
|
+
}
|
|
457
|
+
return next;
|
|
315
458
|
}
|
|
316
459
|
function sanitizeCanvasSnapshot(raw) {
|
|
317
460
|
if (!raw || typeof raw !== "object") return null;
|
|
318
461
|
const s = raw;
|
|
319
|
-
if (s.version !== 1 || !s.panes || !s.items || !s.layout) return null;
|
|
462
|
+
if (s.version !== 1 || !s.panes || typeof s.panes !== "object" || !s.items || typeof s.items !== "object" || !s.layout) return null;
|
|
320
463
|
const paneIds = new Set(Object.keys(s.panes));
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
464
|
+
const seen = /* @__PURE__ */ new Set();
|
|
465
|
+
if (!isValidLayout(s.layout, paneIds, seen)) return null;
|
|
466
|
+
const windows = {};
|
|
467
|
+
if (s.windows && typeof s.windows === "object") {
|
|
468
|
+
for (const [id, value] of Object.entries(s.windows)) {
|
|
469
|
+
if (!value || typeof value !== "object" || id === CANVAS_MAIN_WINDOW) continue;
|
|
470
|
+
const w = value;
|
|
471
|
+
const claimed = new Set(seen);
|
|
472
|
+
if (!isValidLayout(w.layout, paneIds, claimed)) continue;
|
|
473
|
+
for (const pane of claimed) seen.add(pane);
|
|
474
|
+
const layoutPanes = listPaneIds(w.layout);
|
|
475
|
+
const focused2 = w.focusedPaneId && layoutPanes.includes(w.focusedPaneId) ? w.focusedPaneId : layoutPanes[0];
|
|
476
|
+
if (!focused2) continue;
|
|
477
|
+
windows[id] = {
|
|
478
|
+
id,
|
|
479
|
+
role: w.role === "pop-out" ? "pop-out" : "window",
|
|
480
|
+
home: typeof w.home === "string" ? w.home : CANVAS_MAIN_WINDOW,
|
|
481
|
+
isOpen: w.isOpen === true,
|
|
482
|
+
isFullscreen: false,
|
|
483
|
+
width: typeof w.width === "number" ? clampWidth(w.width) : null,
|
|
484
|
+
layout: normalizeLayout(w.layout),
|
|
485
|
+
focusedPaneId: focused2
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (seen.size !== paneIds.size) return null;
|
|
324
490
|
const items = {};
|
|
325
491
|
for (const [id, item] of Object.entries(s.items)) {
|
|
326
492
|
if (item && typeof item.kind === "string" && typeof item.key === "string" && canvasItemId(item.kind, item.key) === id) {
|
|
327
493
|
items[id] = item;
|
|
328
494
|
}
|
|
329
495
|
}
|
|
496
|
+
const placed = /* @__PURE__ */ new Set();
|
|
330
497
|
const panes = {};
|
|
331
498
|
for (const [id, pane] of Object.entries(s.panes)) {
|
|
332
|
-
const itemIds = (Array.isArray(pane.itemIds) ? pane.itemIds : []).filter((itemId) => itemId in items);
|
|
499
|
+
const itemIds = (Array.isArray(pane.itemIds) ? pane.itemIds : []).filter((itemId) => itemId in items && !placed.has(itemId));
|
|
500
|
+
for (const itemId of itemIds) placed.add(itemId);
|
|
333
501
|
const activeItemId = pane.activeItemId && itemIds.includes(pane.activeItemId) ? pane.activeItemId : itemIds[0] ?? null;
|
|
334
502
|
panes[id] = { id, itemIds, activeItemId };
|
|
335
503
|
}
|
|
336
|
-
const
|
|
504
|
+
const mainPanes = listPaneIds(s.layout);
|
|
505
|
+
const focused = s.focusedPaneId && mainPanes.includes(s.focusedPaneId) ? s.focusedPaneId : mainPanes[0];
|
|
337
506
|
if (!focused) return null;
|
|
338
507
|
const layout = normalizeLayout(s.layout);
|
|
339
|
-
const
|
|
340
|
-
|
|
508
|
+
const highest = Math.max(highestIdNumber(layout), ...Object.values(windows).map((w) => highestIdNumber(w.layout)));
|
|
509
|
+
const seq = Math.max(typeof s.seq === "number" && Number.isFinite(s.seq) ? s.seq : 0, highest);
|
|
510
|
+
let state = {
|
|
341
511
|
version: 1,
|
|
342
512
|
isOpen: s.isOpen === true,
|
|
343
513
|
// Full screen is a moment, not a preference: a reload never starts there.
|
|
344
514
|
isFullscreen: false,
|
|
345
|
-
width:
|
|
515
|
+
width: typeof s.width === "number" ? clampWidth(s.width) : null,
|
|
346
516
|
layout,
|
|
517
|
+
focusedPaneId: focused,
|
|
518
|
+
windows,
|
|
347
519
|
panes,
|
|
348
520
|
items,
|
|
349
|
-
focusedPaneId: focused,
|
|
350
521
|
seq,
|
|
351
522
|
hydrated: true
|
|
352
523
|
};
|
|
524
|
+
state = foldCanvasWindows(state);
|
|
525
|
+
for (const id of Object.keys(items)) {
|
|
526
|
+
if (!placed.has(id)) state = insertInto(state, state.focusedPaneId, id, false);
|
|
527
|
+
}
|
|
528
|
+
return state;
|
|
353
529
|
}
|
|
354
530
|
function normalizeLayout(node) {
|
|
355
531
|
if (node.type === "pane") return node;
|
|
@@ -362,12 +538,14 @@ function highestIdNumber(node) {
|
|
|
362
538
|
}
|
|
363
539
|
function canvasReducer(state = createInitialCanvasState(), action) {
|
|
364
540
|
if (!isCanvasAction(action)) return state;
|
|
541
|
+
if (!state.windows) state = { ...state, windows: {} };
|
|
365
542
|
switch (action.type) {
|
|
366
543
|
case `${P}open`: {
|
|
367
|
-
const { kind, key, title, data, target = "focused", reveal = true, activate = true, now } = action.payload;
|
|
544
|
+
const { kind, key, title, data, target = "focused", reveal = true, activate = true, now, splitShare } = action.payload;
|
|
368
545
|
const itemId = canvasItemId(kind, key);
|
|
369
546
|
const existing = state.items[itemId];
|
|
370
547
|
let next = state;
|
|
548
|
+
let revealIn = action.payload.windowId ?? CANVAS_MAIN_WINDOW;
|
|
371
549
|
if (existing) {
|
|
372
550
|
const item = {
|
|
373
551
|
...existing,
|
|
@@ -377,23 +555,33 @@ function canvasReducer(state = createInitialCanvasState(), action) {
|
|
|
377
555
|
};
|
|
378
556
|
next = { ...next, items: { ...next.items, [itemId]: item } };
|
|
379
557
|
const pane = paneOf(next, itemId);
|
|
380
|
-
if (pane
|
|
381
|
-
|
|
558
|
+
if (pane) {
|
|
559
|
+
revealIn = canvasWindowOfPane(next, pane.id) ?? CANVAS_MAIN_WINDOW;
|
|
560
|
+
if (activate) next = focus(withPane(next, { ...pane, activeItemId: itemId }), pane.id);
|
|
561
|
+
} else {
|
|
562
|
+
next = ensureWindow(next, revealIn);
|
|
563
|
+
const frame2 = canvasWindowFrame(next, revealIn) ?? next;
|
|
564
|
+
next = insertInto(next, frame2.focusedPaneId, itemId, true);
|
|
565
|
+
}
|
|
382
566
|
} else {
|
|
383
567
|
const item = { id: itemId, kind, key, title: title ?? null, data: data ?? null, openedAt: now, updatedAt: now };
|
|
384
|
-
next = { ...next, items: { ...next.items, [itemId]: item } };
|
|
385
|
-
|
|
568
|
+
next = ensureWindow({ ...next, items: { ...next.items, [itemId]: item } }, revealIn);
|
|
569
|
+
const frame2 = canvasWindowFrame(next, revealIn) ?? next;
|
|
570
|
+
let paneId = frame2.focusedPaneId;
|
|
386
571
|
if (typeof target === "object" && next.panes[target.paneId]) {
|
|
387
572
|
paneId = target.paneId;
|
|
573
|
+
revealIn = canvasWindowOfPane(next, paneId) ?? revealIn;
|
|
388
574
|
} else if (target === "split-right" || target === "split-down") {
|
|
389
|
-
const split = addSplit(next,
|
|
390
|
-
|
|
391
|
-
|
|
575
|
+
const split = addSplit(next, frame2.focusedPaneId, target === "split-right" ? "horizontal" : "vertical", splitShare);
|
|
576
|
+
if (split) {
|
|
577
|
+
next = split.state;
|
|
578
|
+
paneId = split.newPaneId;
|
|
579
|
+
}
|
|
392
580
|
}
|
|
393
581
|
next = insertInto(next, paneId, itemId, activate);
|
|
394
|
-
if (activate) next =
|
|
582
|
+
if (activate) next = focus(next, paneId);
|
|
395
583
|
}
|
|
396
|
-
return reveal ?
|
|
584
|
+
return reveal ? withFrame(next, revealIn, { isOpen: true }) : next;
|
|
397
585
|
}
|
|
398
586
|
case `${P}update`: {
|
|
399
587
|
const { itemId, data, title, now } = action.payload;
|
|
@@ -429,12 +617,13 @@ function canvasReducer(state = createInitialCanvasState(), action) {
|
|
|
429
617
|
const { itemId } = action.payload;
|
|
430
618
|
if (!state.items[itemId]) return state;
|
|
431
619
|
const pane = paneOf(state, itemId);
|
|
620
|
+
const windowId = pane ? canvasWindowOfPane(state, pane.id) : null;
|
|
432
621
|
let next = detachItem(state, itemId);
|
|
433
622
|
const items = { ...next.items };
|
|
434
623
|
delete items[itemId];
|
|
435
624
|
next = { ...next, items };
|
|
436
625
|
if (pane && next.panes[pane.id]?.itemIds.length === 0) next = dropPane(next, pane.id);
|
|
437
|
-
return next;
|
|
626
|
+
return dropEmptyPopOut(next, windowId);
|
|
438
627
|
}
|
|
439
628
|
case `${P}closeOthers`: {
|
|
440
629
|
const { itemId } = action.payload;
|
|
@@ -447,26 +636,31 @@ function canvasReducer(state = createInitialCanvasState(), action) {
|
|
|
447
636
|
case `${P}activate`: {
|
|
448
637
|
const pane = paneOf(state, action.payload.itemId);
|
|
449
638
|
if (!pane) return state;
|
|
450
|
-
return
|
|
639
|
+
return focus(withPane(state, { ...pane, activeItemId: action.payload.itemId }), pane.id);
|
|
640
|
+
}
|
|
641
|
+
case `${P}focusPane`: {
|
|
642
|
+
const windowId = canvasWindowOfPane(state, action.payload.paneId);
|
|
643
|
+
const frame2 = windowId ? canvasWindowFrame(state, windowId) : null;
|
|
644
|
+
if (!windowId || !frame2 || frame2.focusedPaneId === action.payload.paneId) return state;
|
|
645
|
+
return withFrame(state, windowId, { focusedPaneId: action.payload.paneId });
|
|
451
646
|
}
|
|
452
|
-
case `${P}focusPane`:
|
|
453
|
-
return state.panes[action.payload.paneId] ? { ...state, focusedPaneId: action.payload.paneId } : state;
|
|
454
647
|
case `${P}moveItem`: {
|
|
455
648
|
const { itemId, toPaneId, index } = action.payload;
|
|
456
649
|
const from = paneOf(state, itemId);
|
|
457
650
|
if (!from || !state.panes[toPaneId]) return state;
|
|
458
|
-
if (from.id === toPaneId && index === void 0) return state
|
|
651
|
+
if (from.id === toPaneId && index === void 0) return canvasReducer(state, canvasActions.focusPane(toPaneId));
|
|
652
|
+
const fromWindow = canvasWindowOfPane(state, from.id);
|
|
459
653
|
let next = from.id === toPaneId ? state : detachItem(state, itemId);
|
|
460
|
-
next = insertInto(next, toPaneId, itemId, true, index);
|
|
461
|
-
next = { ...next, focusedPaneId: toPaneId };
|
|
654
|
+
next = focus(insertInto(next, toPaneId, itemId, true, index), toPaneId);
|
|
462
655
|
if (from.id !== toPaneId && next.panes[from.id]?.itemIds.length === 0) next = dropPane(next, from.id);
|
|
463
|
-
return next;
|
|
656
|
+
return dropEmptyPopOut(next, fromWindow);
|
|
464
657
|
}
|
|
465
658
|
case `${P}splitPane`: {
|
|
466
|
-
const { paneId, orientation, moveItemId } = action.payload;
|
|
659
|
+
const { paneId, orientation, moveItemId, share } = action.payload;
|
|
467
660
|
const pane = state.panes[paneId];
|
|
468
661
|
if (!pane) return state;
|
|
469
|
-
const split = addSplit(state, paneId, orientation);
|
|
662
|
+
const split = addSplit(state, paneId, orientation, share);
|
|
663
|
+
if (!split) return state;
|
|
470
664
|
let next = split.state;
|
|
471
665
|
if (moveItemId && pane.itemIds.includes(moveItemId) && pane.itemIds.length > 1) {
|
|
472
666
|
next = detachItem(next, moveItemId);
|
|
@@ -476,23 +670,106 @@ function canvasReducer(state = createInitialCanvasState(), action) {
|
|
|
476
670
|
}
|
|
477
671
|
case `${P}closePane`: {
|
|
478
672
|
const pane = state.panes[action.payload.paneId];
|
|
479
|
-
|
|
480
|
-
const
|
|
481
|
-
if (
|
|
673
|
+
const windowId = pane ? canvasWindowOfPane(state, pane.id) : null;
|
|
674
|
+
const frame2 = windowId ? canvasWindowFrame(state, windowId) : null;
|
|
675
|
+
if (!pane || !windowId || !frame2) return state;
|
|
676
|
+
const onlyPane = listPaneIds(frame2.layout).length === 1;
|
|
677
|
+
if (onlyPane) {
|
|
678
|
+
if (windowId !== CANVAS_MAIN_WINDOW && state.windows[windowId]?.role === "pop-out") {
|
|
679
|
+
return canvasReducer(state, canvasActions.closeWindow(windowId, "dock"));
|
|
680
|
+
}
|
|
681
|
+
return withFrame(state, windowId, { isOpen: false, isFullscreen: false });
|
|
682
|
+
}
|
|
482
683
|
const items = { ...state.items };
|
|
483
684
|
for (const id of pane.itemIds) delete items[id];
|
|
484
685
|
return dropPane({ ...state, items }, pane.id);
|
|
485
686
|
}
|
|
486
|
-
case `${P}resizeSplit`:
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
687
|
+
case `${P}resizeSplit`: {
|
|
688
|
+
const { splitId, sizes } = action.payload;
|
|
689
|
+
for (const windowId of listCanvasWindowIds(state)) {
|
|
690
|
+
const frame2 = canvasWindowFrame(state, windowId);
|
|
691
|
+
if (frame2 && containsSplit(frame2.layout, splitId)) {
|
|
692
|
+
return withFrame(state, windowId, { layout: resizeSplit(frame2.layout, splitId, sizes) });
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return state;
|
|
696
|
+
}
|
|
697
|
+
case `${P}setOpen`: {
|
|
698
|
+
const { open, windowId = CANVAS_MAIN_WINDOW } = action.payload;
|
|
699
|
+
const next = open ? ensureWindow(state, windowId) : state;
|
|
700
|
+
const frame2 = canvasWindowFrame(next, windowId);
|
|
701
|
+
if (!frame2 || frame2.isOpen === open) return next;
|
|
702
|
+
return withFrame(next, windowId, { isOpen: open, isFullscreen: open ? frame2.isFullscreen : false });
|
|
703
|
+
}
|
|
704
|
+
case `${P}toggle`: {
|
|
705
|
+
const windowId = action.payload?.windowId ?? CANVAS_MAIN_WINDOW;
|
|
706
|
+
const next = ensureWindow(state, windowId);
|
|
707
|
+
const frame2 = canvasWindowFrame(next, windowId);
|
|
708
|
+
if (!frame2) return state;
|
|
709
|
+
return withFrame(next, windowId, { isOpen: !frame2.isOpen, isFullscreen: frame2.isOpen ? false : frame2.isFullscreen });
|
|
710
|
+
}
|
|
711
|
+
case `${P}setFullscreen`: {
|
|
712
|
+
const { fullscreen, windowId = CANVAS_MAIN_WINDOW } = action.payload;
|
|
713
|
+
const next = fullscreen ? ensureWindow(state, windowId) : state;
|
|
714
|
+
const frame2 = canvasWindowFrame(next, windowId);
|
|
715
|
+
if (!frame2) return state;
|
|
716
|
+
return withFrame(next, windowId, { isFullscreen: fullscreen, isOpen: fullscreen ? true : frame2.isOpen });
|
|
717
|
+
}
|
|
718
|
+
case `${P}setWidth`: {
|
|
719
|
+
const { width, windowId = CANVAS_MAIN_WINDOW } = action.payload;
|
|
720
|
+
const next = ensureWindow(state, windowId);
|
|
721
|
+
return withFrame(next, windowId, { width: clampWidth(width) });
|
|
722
|
+
}
|
|
723
|
+
case `${P}popOut`: {
|
|
724
|
+
const { itemId, windowId } = action.payload;
|
|
725
|
+
const from = paneOf(state, itemId);
|
|
726
|
+
if (!from || windowId === CANVAS_MAIN_WINDOW) return state;
|
|
727
|
+
const fromWindow = canvasWindowOfPane(state, from.id) ?? CANVAS_MAIN_WINDOW;
|
|
728
|
+
if (fromWindow === windowId) return state;
|
|
729
|
+
let next = ensureWindow(state, windowId, "pop-out", fromWindow);
|
|
730
|
+
const target = canvasWindowFrame(next, windowId);
|
|
731
|
+
if (!target) return state;
|
|
732
|
+
next = detachItem(next, itemId);
|
|
733
|
+
next = insertInto(next, target.focusedPaneId, itemId, true);
|
|
734
|
+
next = withFrame(next, windowId, { isOpen: true, focusedPaneId: target.focusedPaneId });
|
|
735
|
+
if (next.panes[from.id]?.itemIds.length === 0) next = dropPane(next, from.id);
|
|
736
|
+
next = dropEmptyPopOut(next, fromWindow);
|
|
737
|
+
if (canvasWindowFrame(next, fromWindow) && windowItemIds(next, fromWindow).length === 0) {
|
|
738
|
+
next = withFrame(next, fromWindow, { isOpen: false, isFullscreen: false });
|
|
739
|
+
}
|
|
740
|
+
return next;
|
|
741
|
+
}
|
|
742
|
+
case `${P}dockBack`: {
|
|
743
|
+
const { itemId } = action.payload;
|
|
744
|
+
const windowId = canvasWindowOfItem(state, itemId);
|
|
745
|
+
if (!windowId || windowId === CANVAS_MAIN_WINDOW) return state;
|
|
746
|
+
const frame2 = state.windows[windowId];
|
|
747
|
+
if (!frame2) return state;
|
|
748
|
+
const home = canvasWindowFrame(state, frame2.home) ? frame2.home : CANVAS_MAIN_WINDOW;
|
|
749
|
+
let next = sendHome(state, itemId, home, true);
|
|
750
|
+
next = withFrame(next, home, { isOpen: true });
|
|
751
|
+
const left = paneOf(state, itemId);
|
|
752
|
+
if (left && next.panes[left.id]?.itemIds.length === 0) next = dropPane(next, left.id);
|
|
753
|
+
return dropEmptyPopOut(next, windowId);
|
|
754
|
+
}
|
|
755
|
+
case `${P}closeWindow`: {
|
|
756
|
+
const { windowId, mode } = action.payload;
|
|
757
|
+
const frame2 = state.windows[windowId];
|
|
758
|
+
if (!frame2) return state;
|
|
759
|
+
const itemIds = windowItemIds(state, windowId);
|
|
760
|
+
let next = state;
|
|
761
|
+
if (mode === "close") {
|
|
762
|
+
const items = { ...next.items };
|
|
763
|
+
for (const id of itemIds) delete items[id];
|
|
764
|
+
next = { ...next, items };
|
|
765
|
+
} else if (itemIds.length > 0) {
|
|
766
|
+
const home = canvasWindowFrame(state, frame2.home) ? frame2.home : CANVAS_MAIN_WINDOW;
|
|
767
|
+
const active = state.panes[frame2.focusedPaneId]?.activeItemId ?? null;
|
|
768
|
+
for (const id of itemIds) next = sendHome(next, id, home, id === active);
|
|
769
|
+
next = withFrame(next, home, { isOpen: true });
|
|
770
|
+
}
|
|
771
|
+
return removeWindow(next, windowId);
|
|
772
|
+
}
|
|
496
773
|
case `${P}hydrate`: {
|
|
497
774
|
const snapshot = action.payload.snapshot ? sanitizeCanvasSnapshot(action.payload.snapshot) : null;
|
|
498
775
|
if (!snapshot) return { ...state, hydrated: true };
|
|
@@ -539,6 +816,64 @@ function bindCanvasToReduxStore(store, select) {
|
|
|
539
816
|
};
|
|
540
817
|
}
|
|
541
818
|
|
|
819
|
+
// src/core/remote.ts
|
|
820
|
+
function createRemoteCanvasStore(options) {
|
|
821
|
+
let state = options.initialSnapshot;
|
|
822
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
823
|
+
const report = options.onError ?? ((error, action) => console.error(`[@ai-matrx/canvas] remote dispatch of ${action.type} failed`, error));
|
|
824
|
+
const stop = options.subscribe((snapshot) => {
|
|
825
|
+
const next = shareStructure(state, snapshot);
|
|
826
|
+
if (next === state) return;
|
|
827
|
+
state = next;
|
|
828
|
+
for (const listener of [...listeners]) listener();
|
|
829
|
+
});
|
|
830
|
+
return {
|
|
831
|
+
authority: "remote",
|
|
832
|
+
getState: () => state,
|
|
833
|
+
dispatch(action) {
|
|
834
|
+
if (!isCanvasAction(action)) return;
|
|
835
|
+
try {
|
|
836
|
+
void Promise.resolve(options.dispatch(action)).catch((error) => report(error, action));
|
|
837
|
+
} catch (error) {
|
|
838
|
+
report(error, action);
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
subscribe(listener) {
|
|
842
|
+
listeners.add(listener);
|
|
843
|
+
return () => listeners.delete(listener);
|
|
844
|
+
},
|
|
845
|
+
dispose() {
|
|
846
|
+
stop();
|
|
847
|
+
listeners.clear();
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
function shareStructure(prev, next) {
|
|
852
|
+
if (Object.is(prev, next)) return prev;
|
|
853
|
+
if (typeof prev !== "object" || typeof next !== "object" || prev === null || next === null) return next;
|
|
854
|
+
if (Array.isArray(prev) !== Array.isArray(next)) return next;
|
|
855
|
+
if (Array.isArray(prev) && Array.isArray(next)) {
|
|
856
|
+
let same2 = prev.length === next.length;
|
|
857
|
+
const out2 = next.map((value, i) => {
|
|
858
|
+
const shared = i < prev.length ? shareStructure(prev[i], value) : value;
|
|
859
|
+
if (shared !== prev[i]) same2 = false;
|
|
860
|
+
return shared;
|
|
861
|
+
});
|
|
862
|
+
return same2 ? prev : out2;
|
|
863
|
+
}
|
|
864
|
+
const a = prev;
|
|
865
|
+
const b = next;
|
|
866
|
+
const keys = Object.keys(b);
|
|
867
|
+
let same = keys.length === Object.keys(a).length;
|
|
868
|
+
const out = {};
|
|
869
|
+
for (const key of keys) {
|
|
870
|
+
const shared = key in a ? shareStructure(a[key], b[key]) : b[key];
|
|
871
|
+
if (shared !== a[key] || !(key in a)) same = false;
|
|
872
|
+
out[key] = shared;
|
|
873
|
+
}
|
|
874
|
+
return same ? prev : out;
|
|
875
|
+
}
|
|
876
|
+
|
|
542
877
|
// src/core/persistence.ts
|
|
543
878
|
var CANVAS_STORAGE_KEY = "ai-matrx.canvas.v1";
|
|
544
879
|
function createLocalStorageCanvasPersistence(key = CANVAS_STORAGE_KEY, storage = typeof window === "undefined" ? null : safeLocalStorage()) {
|
|
@@ -555,6 +890,16 @@ function createLocalStorageCanvasPersistence(key = CANVAS_STORAGE_KEY, storage =
|
|
|
555
890
|
}
|
|
556
891
|
};
|
|
557
892
|
}
|
|
893
|
+
function createKeyValueCanvasPersistence(kv, key = CANVAS_STORAGE_KEY) {
|
|
894
|
+
return {
|
|
895
|
+
async load() {
|
|
896
|
+
const raw = await kv.get(key);
|
|
897
|
+
if (raw === void 0 || raw === null) return null;
|
|
898
|
+
return typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
899
|
+
},
|
|
900
|
+
save: (snapshot) => kv.set(key, JSON.parse(JSON.stringify(snapshot)))
|
|
901
|
+
};
|
|
902
|
+
}
|
|
558
903
|
function safeLocalStorage() {
|
|
559
904
|
try {
|
|
560
905
|
const probe = "__mxc_probe__";
|
|
@@ -566,6 +911,7 @@ function safeLocalStorage() {
|
|
|
566
911
|
}
|
|
567
912
|
}
|
|
568
913
|
function toPersistableSnapshot(state, isRestorable) {
|
|
914
|
+
state = foldCanvasWindows(state);
|
|
569
915
|
const keep = new Set(
|
|
570
916
|
Object.values(state.items).filter((item) => isRestorable(item.kind)).map((item) => item.id)
|
|
571
917
|
);
|
|
@@ -592,22 +938,164 @@ function toPersistableSnapshot(state, isRestorable) {
|
|
|
592
938
|
return { ...state, items, panes, layout, focusedPaneId };
|
|
593
939
|
}
|
|
594
940
|
|
|
941
|
+
// src/core/rules.ts
|
|
942
|
+
var CANVAS_WEB_LAYOUT_RULES = {
|
|
943
|
+
defaultWidth: CANVAS_DEFAULT_WIDTH,
|
|
944
|
+
minWidth: CANVAS_MIN_WIDTH,
|
|
945
|
+
centreMinWidth: 420,
|
|
946
|
+
stackSplit: 0.5,
|
|
947
|
+
sideSplit: 0.5,
|
|
948
|
+
paneMinWidth: 0
|
|
949
|
+
};
|
|
950
|
+
var CANVAS_DESKTOP_LAYOUT_RULES = {
|
|
951
|
+
defaultWidth: { fraction: 0.36 },
|
|
952
|
+
minWidth: CANVAS_MIN_WIDTH,
|
|
953
|
+
centreMinWidth: 520,
|
|
954
|
+
stackSplit: 0.7,
|
|
955
|
+
sideSplit: 0.5,
|
|
956
|
+
paneMinWidth: 360
|
|
957
|
+
};
|
|
958
|
+
var clampFraction = (value, fallback) => Number.isFinite(value) ? Math.min(0.88, Math.max(0.12, value)) : fallback;
|
|
959
|
+
function resolveCanvasLayoutRules(rules) {
|
|
960
|
+
const merged = { ...CANVAS_WEB_LAYOUT_RULES, ...rules ?? {} };
|
|
961
|
+
const positive = (value, fallback) => Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
962
|
+
return {
|
|
963
|
+
defaultWidth: typeof merged.defaultWidth === "number" ? positive(merged.defaultWidth, CANVAS_DEFAULT_WIDTH) : { fraction: Math.min(1, Math.max(0.05, Number.isFinite(merged.defaultWidth.fraction) ? merged.defaultWidth.fraction : 0.36)) },
|
|
964
|
+
minWidth: positive(merged.minWidth, CANVAS_MIN_WIDTH),
|
|
965
|
+
centreMinWidth: positive(merged.centreMinWidth, CANVAS_WEB_LAYOUT_RULES.centreMinWidth),
|
|
966
|
+
stackSplit: clampFraction(merged.stackSplit, 0.5),
|
|
967
|
+
sideSplit: clampFraction(merged.sideSplit, 0.5),
|
|
968
|
+
paneMinWidth: positive(merged.paneMinWidth, 0)
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
function defaultCanvasWidth(rules, region) {
|
|
972
|
+
if (typeof rules.defaultWidth === "number") return rules.defaultWidth;
|
|
973
|
+
return region > 0 ? Math.round(region * rules.defaultWidth.fraction) : CANVAS_DEFAULT_WIDTH;
|
|
974
|
+
}
|
|
975
|
+
function fitCanvasWidth(stored, region, rules) {
|
|
976
|
+
const wanted = stored ?? defaultCanvasWidth(rules, region);
|
|
977
|
+
if (region <= 0) return Math.max(rules.minWidth, wanted);
|
|
978
|
+
return Math.max(rules.minWidth, Math.min(wanted, region - rules.centreMinWidth));
|
|
979
|
+
}
|
|
980
|
+
function clampDraggedCanvasWidth(next, region, rules) {
|
|
981
|
+
const max = Math.max(rules.minWidth, region - rules.centreMinWidth);
|
|
982
|
+
return Math.min(max, Math.max(rules.minWidth, Math.round(next)));
|
|
983
|
+
}
|
|
984
|
+
function shouldStackSplit(width, children, rules) {
|
|
985
|
+
return rules.paneMinWidth > 0 && width > 0 && width < rules.paneMinWidth * children;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// src/core/commands.ts
|
|
989
|
+
var CANVAS_SHORTCUTS = [
|
|
990
|
+
{ id: "canvas.toggle", accelerator: "CmdOrCtrl+\\", when: "presented", label: "Show or hide canvas", web: true },
|
|
991
|
+
{ id: "canvas.exitFullscreen", accelerator: "Escape", when: "fullscreen", label: "Restore canvas", web: true },
|
|
992
|
+
{ id: "canvas.toggleFullscreen", accelerator: "CmdOrCtrl+Shift+Enter", when: "canvas-focus", label: "Expand canvas", web: true },
|
|
993
|
+
{ id: "canvas.splitRight", accelerator: "CmdOrCtrl+Alt+\\", when: "canvas-focus", label: "Split right", web: true },
|
|
994
|
+
{ id: "canvas.splitDown", accelerator: "CmdOrCtrl+Alt+Shift+\\", when: "canvas-focus", label: "Split down", web: true },
|
|
995
|
+
{ id: "canvas.closeTab", accelerator: "CmdOrCtrl+W", when: "canvas-focus", label: "Close tab", web: false },
|
|
996
|
+
{ id: "canvas.nextTab", accelerator: "Ctrl+Tab", when: "canvas-focus", label: "Next tab", web: false },
|
|
997
|
+
{ id: "canvas.previousTab", accelerator: "Ctrl+Shift+Tab", when: "canvas-focus", label: "Previous tab", web: false }
|
|
998
|
+
];
|
|
999
|
+
function canvasShortcutHint(id, isMac) {
|
|
1000
|
+
const shortcut = CANVAS_SHORTCUTS.find((entry) => entry.id === id);
|
|
1001
|
+
if (!shortcut) return null;
|
|
1002
|
+
return shortcut.accelerator.split("+").map((part) => {
|
|
1003
|
+
if (part === "CmdOrCtrl") return isMac ? "\u2318" : "Ctrl+";
|
|
1004
|
+
if (part === "Ctrl") return isMac ? "\u2303" : "Ctrl+";
|
|
1005
|
+
if (part === "Alt") return isMac ? "\u2325" : "Alt+";
|
|
1006
|
+
if (part === "Shift") return isMac ? "\u21E7" : "Shift+";
|
|
1007
|
+
if (part === "Escape") return "Esc";
|
|
1008
|
+
return part;
|
|
1009
|
+
}).join("");
|
|
1010
|
+
}
|
|
1011
|
+
function runCanvasCommand(controller, id, windowId = controller.windowId) {
|
|
1012
|
+
const state = controller.getState();
|
|
1013
|
+
const frame2 = canvasWindowFrame(state, windowId);
|
|
1014
|
+
const pane = frame2 ? state.panes[frame2.focusedPaneId] : void 0;
|
|
1015
|
+
switch (id) {
|
|
1016
|
+
case "canvas.toggle":
|
|
1017
|
+
controller.toggle(windowId);
|
|
1018
|
+
return true;
|
|
1019
|
+
case "canvas.toggleFullscreen":
|
|
1020
|
+
controller.setFullscreen(!(frame2?.isFullscreen ?? false), windowId);
|
|
1021
|
+
return true;
|
|
1022
|
+
case "canvas.exitFullscreen":
|
|
1023
|
+
if (!frame2?.isFullscreen) return false;
|
|
1024
|
+
controller.setFullscreen(false, windowId);
|
|
1025
|
+
return true;
|
|
1026
|
+
case "canvas.closeTab":
|
|
1027
|
+
if (!frame2?.isOpen || !pane?.activeItemId) return false;
|
|
1028
|
+
controller.close(pane.activeItemId);
|
|
1029
|
+
return true;
|
|
1030
|
+
case "canvas.nextTab":
|
|
1031
|
+
case "canvas.previousTab": {
|
|
1032
|
+
if (!frame2?.isOpen || !pane?.activeItemId || pane.itemIds.length < 2) return false;
|
|
1033
|
+
const at = pane.itemIds.indexOf(pane.activeItemId);
|
|
1034
|
+
const step = id === "canvas.nextTab" ? 1 : -1;
|
|
1035
|
+
const next = pane.itemIds[(at + step + pane.itemIds.length) % pane.itemIds.length];
|
|
1036
|
+
if (next) controller.activate(next);
|
|
1037
|
+
return true;
|
|
1038
|
+
}
|
|
1039
|
+
case "canvas.splitRight":
|
|
1040
|
+
case "canvas.splitDown":
|
|
1041
|
+
if (!frame2?.isOpen || !pane) return false;
|
|
1042
|
+
controller.splitPane(pane.id, id === "canvas.splitRight" ? "horizontal" : "vertical", pane.itemIds.length > 1 ? pane.activeItemId ?? void 0 : void 0);
|
|
1043
|
+
return true;
|
|
1044
|
+
default:
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
var CODE_FOR = { "\\": "Backslash", Tab: "Tab", Escape: "Escape", Enter: "Enter" };
|
|
1049
|
+
function matchesCanvasAccelerator(input, accelerator, isMac) {
|
|
1050
|
+
const parts = accelerator.split("+");
|
|
1051
|
+
const key = parts.pop() ?? "";
|
|
1052
|
+
const want = { meta: false, ctrl: false, alt: false, shift: false };
|
|
1053
|
+
for (const part of parts) {
|
|
1054
|
+
if (part === "CmdOrCtrl") want[isMac ? "meta" : "ctrl"] = true;
|
|
1055
|
+
else if (part === "Cmd" || part === "Meta") want.meta = true;
|
|
1056
|
+
else if (part === "Ctrl") want.ctrl = true;
|
|
1057
|
+
else if (part === "Alt") want.alt = true;
|
|
1058
|
+
else if (part === "Shift") want.shift = true;
|
|
1059
|
+
}
|
|
1060
|
+
if (input.metaKey !== want.meta || input.ctrlKey !== want.ctrl || input.altKey !== want.alt || input.shiftKey !== want.shift) return false;
|
|
1061
|
+
const code = CODE_FOR[key] ?? (/^[A-Z]$/i.test(key) ? `Key${key.toUpperCase()}` : void 0);
|
|
1062
|
+
if (code && input.code === code) return true;
|
|
1063
|
+
return input.key.toLowerCase() === key.toLowerCase();
|
|
1064
|
+
}
|
|
1065
|
+
|
|
595
1066
|
// src/core/controller.ts
|
|
596
1067
|
var consoleCanvasErrorSink = (report) => {
|
|
597
1068
|
console.error(`[@ai-matrx/canvas] ${report.code}: ${report.message}`, report.detail ?? "");
|
|
598
1069
|
};
|
|
599
1070
|
function createCanvasController(options) {
|
|
1071
|
+
const shared = {
|
|
1072
|
+
options,
|
|
1073
|
+
onError: options.onError ?? consoleCanvasErrorSink,
|
|
1074
|
+
rules: resolveCanvasLayoutRules(options.layoutRules),
|
|
1075
|
+
presentations: /* @__PURE__ */ new Map(),
|
|
1076
|
+
presentationListeners: /* @__PURE__ */ new Set(),
|
|
1077
|
+
views: /* @__PURE__ */ new Map(),
|
|
1078
|
+
started: /* @__PURE__ */ new Map(),
|
|
1079
|
+
sessionStop: null
|
|
1080
|
+
};
|
|
1081
|
+
return view(shared, canvasWindowId(options.windowId ?? CANVAS_MAIN_WINDOW));
|
|
1082
|
+
}
|
|
1083
|
+
function view(shared, windowId) {
|
|
1084
|
+
const cached = shared.views.get(windowId);
|
|
1085
|
+
if (cached) return cached;
|
|
1086
|
+
const { options, onError, rules } = shared;
|
|
600
1087
|
const { store, persistence = null } = options;
|
|
601
|
-
const onError = options.onError ?? consoleCanvasErrorSink;
|
|
602
1088
|
const isRestorable = options.isRestorable ?? (() => true);
|
|
603
1089
|
const dispatch = (action) => store.dispatch(action);
|
|
604
|
-
let presentations = 0;
|
|
605
|
-
const presentationListeners = /* @__PURE__ */ new Set();
|
|
606
1090
|
const notifyPresentation = () => {
|
|
607
|
-
for (const listener of [...presentationListeners]) listener();
|
|
1091
|
+
for (const listener of [...shared.presentationListeners]) listener();
|
|
608
1092
|
};
|
|
1093
|
+
const shareFor = (orientation) => orientation === "vertical" ? rules.stackSplit : rules.sideSplit;
|
|
609
1094
|
const controller = {
|
|
610
1095
|
store,
|
|
1096
|
+
windowId,
|
|
1097
|
+
rules,
|
|
1098
|
+
forWindow: (id) => view(shared, canvasWindowId(id)),
|
|
611
1099
|
getState: () => store.getState(),
|
|
612
1100
|
open(input) {
|
|
613
1101
|
if (options.isKnownKind && !options.isKnownKind(input.kind)) {
|
|
@@ -625,7 +1113,10 @@ function createCanvasController(options) {
|
|
|
625
1113
|
return null;
|
|
626
1114
|
}
|
|
627
1115
|
}
|
|
628
|
-
|
|
1116
|
+
const target = input.target ?? "focused";
|
|
1117
|
+
const splitShare = target === "split-down" ? rules.stackSplit : target === "split-right" ? rules.sideSplit : void 0;
|
|
1118
|
+
const resolved = input.windowId || windowId === CANVAS_MAIN_WINDOW ? input : { ...input, windowId };
|
|
1119
|
+
dispatch(canvasActions.open(resolved, splitShare === void 0 ? void 0 : { splitShare }));
|
|
629
1120
|
return canvasItemId(input.kind, input.key);
|
|
630
1121
|
},
|
|
631
1122
|
update(itemId, patch) {
|
|
@@ -651,42 +1142,76 @@ function createCanvasController(options) {
|
|
|
651
1142
|
activate: (itemId) => dispatch(canvasActions.activate(itemId)),
|
|
652
1143
|
focusPane: (paneId) => dispatch(canvasActions.focusPane(paneId)),
|
|
653
1144
|
moveItem: (itemId, toPaneId, index) => dispatch(canvasActions.moveItem(itemId, toPaneId, index)),
|
|
654
|
-
splitPane: (paneId, orientation, moveItemId) => dispatch(canvasActions.splitPane(paneId, orientation, moveItemId)),
|
|
1145
|
+
splitPane: (paneId, orientation, moveItemId) => dispatch(canvasActions.splitPane(paneId, orientation, moveItemId, shareFor(orientation))),
|
|
655
1146
|
closePane: (paneId) => dispatch(canvasActions.closePane(paneId)),
|
|
656
1147
|
resizeSplit: (splitId, sizes) => dispatch(canvasActions.resizeSplit(splitId, sizes)),
|
|
657
|
-
show: () => dispatch(canvasActions.setOpen(true)),
|
|
658
|
-
hide: () => dispatch(canvasActions.setOpen(false)),
|
|
659
|
-
toggle: () => dispatch(canvasActions.toggle()),
|
|
660
|
-
setFullscreen: (fullscreen) => dispatch(canvasActions.setFullscreen(fullscreen)),
|
|
661
|
-
setWidth: (width) => dispatch(canvasActions.setWidth(width)),
|
|
1148
|
+
show: (w = windowId) => dispatch(canvasActions.setOpen(true, w)),
|
|
1149
|
+
hide: (w = windowId) => dispatch(canvasActions.setOpen(false, w)),
|
|
1150
|
+
toggle: (w = windowId) => dispatch(canvasActions.toggle(w)),
|
|
1151
|
+
setFullscreen: (fullscreen, w = windowId) => dispatch(canvasActions.setFullscreen(fullscreen, w)),
|
|
1152
|
+
setWidth: (width, w = windowId) => dispatch(canvasActions.setWidth(width, w)),
|
|
1153
|
+
async popOut(itemId) {
|
|
1154
|
+
const item = store.getState().items[itemId];
|
|
1155
|
+
if (!item) return null;
|
|
1156
|
+
const port = options.windowPorts?.().popOut;
|
|
1157
|
+
if (!port) {
|
|
1158
|
+
onError({ code: "pop-out-unavailable", message: "This app cannot pop canvas tabs out into their own window.", detail: { itemId } });
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
let result;
|
|
1162
|
+
try {
|
|
1163
|
+
result = await port(item);
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
onError({ code: "pop-out-failed", message: "The window for this tab could not be opened.", detail: error });
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
if (!result) {
|
|
1169
|
+
onError({ code: "pop-out-refused", message: "The app declined to pop this tab out.", detail: { itemId } });
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
const target = canvasWindowId(result.windowId);
|
|
1173
|
+
dispatch(canvasActions.popOut(itemId, target));
|
|
1174
|
+
return target;
|
|
1175
|
+
},
|
|
1176
|
+
dockBack(itemId) {
|
|
1177
|
+
const state = store.getState();
|
|
1178
|
+
const from = canvasWindowOfItem(state, itemId);
|
|
1179
|
+
if (!from || from === CANVAS_MAIN_WINDOW || state.windows[from]?.role !== "pop-out") return false;
|
|
1180
|
+
dispatch(canvasActions.dockBack(itemId));
|
|
1181
|
+
options.windowPorts?.().dockBack?.(itemId, from);
|
|
1182
|
+
return true;
|
|
1183
|
+
},
|
|
1184
|
+
closeWindow: (w, mode = "dock") => dispatch(canvasActions.closeWindow(w, mode)),
|
|
662
1185
|
has: (kind, key) => canvasItemId(kind, key) in store.getState().items,
|
|
663
|
-
isPresented: () => presentations > 0,
|
|
664
|
-
registerPresentation() {
|
|
665
|
-
presentations
|
|
1186
|
+
isPresented: (w = windowId) => (shared.presentations.get(w) ?? 0) > 0,
|
|
1187
|
+
registerPresentation(w = windowId) {
|
|
1188
|
+
shared.presentations.set(w, (shared.presentations.get(w) ?? 0) + 1);
|
|
666
1189
|
notifyPresentation();
|
|
667
1190
|
let done = false;
|
|
668
1191
|
return () => {
|
|
669
1192
|
if (done) return;
|
|
670
1193
|
done = true;
|
|
671
|
-
presentations
|
|
1194
|
+
shared.presentations.set(w, Math.max(0, (shared.presentations.get(w) ?? 1) - 1));
|
|
672
1195
|
notifyPresentation();
|
|
673
1196
|
};
|
|
674
1197
|
},
|
|
675
1198
|
subscribePresentation(listener) {
|
|
676
|
-
presentationListeners.add(listener);
|
|
677
|
-
return () => presentationListeners.delete(listener);
|
|
1199
|
+
shared.presentationListeners.add(listener);
|
|
1200
|
+
return () => shared.presentationListeners.delete(listener);
|
|
678
1201
|
},
|
|
679
1202
|
start() {
|
|
680
1203
|
let disposed = false;
|
|
681
1204
|
let timer = null;
|
|
682
1205
|
let lastSaved = null;
|
|
1206
|
+
const owns = store.authority !== "remote";
|
|
1207
|
+
const memory = owns ? persistence : null;
|
|
683
1208
|
const save = () => {
|
|
684
1209
|
timer = null;
|
|
685
1210
|
const state = store.getState();
|
|
686
|
-
if (!
|
|
1211
|
+
if (!memory || !state.hydrated || state === lastSaved) return;
|
|
687
1212
|
lastSaved = state;
|
|
688
1213
|
try {
|
|
689
|
-
void Promise.resolve(
|
|
1214
|
+
void Promise.resolve(memory.save(toPersistableSnapshot(state, isRestorable))).catch(
|
|
690
1215
|
(error) => onError({ code: "persistence-save", message: "Saving the canvas layout failed.", detail: error })
|
|
691
1216
|
);
|
|
692
1217
|
} catch (error) {
|
|
@@ -695,18 +1220,18 @@ function createCanvasController(options) {
|
|
|
695
1220
|
};
|
|
696
1221
|
let seen = store.getState();
|
|
697
1222
|
const unsubscribe = store.subscribe(() => {
|
|
698
|
-
if (!
|
|
1223
|
+
if (!memory || disposed) return;
|
|
699
1224
|
const current = store.getState();
|
|
700
1225
|
if (current === seen) return;
|
|
701
1226
|
seen = current;
|
|
702
1227
|
if (timer) clearTimeout(timer);
|
|
703
1228
|
timer = setTimeout(save, options.saveDelayMs ?? 250);
|
|
704
1229
|
});
|
|
705
|
-
if (store.getState().hydrated) {
|
|
706
|
-
} else if (!
|
|
1230
|
+
if (!owns || store.getState().hydrated) {
|
|
1231
|
+
} else if (!memory) {
|
|
707
1232
|
dispatch(canvasActions.hydrate(null));
|
|
708
1233
|
} else {
|
|
709
|
-
Promise.resolve().then(() =>
|
|
1234
|
+
Promise.resolve().then(() => memory.load()).then(
|
|
710
1235
|
(snapshot) => {
|
|
711
1236
|
if (disposed) return;
|
|
712
1237
|
const openedThisSession = store.getState().isOpen;
|
|
@@ -725,9 +1250,11 @@ function createCanvasController(options) {
|
|
|
725
1250
|
}
|
|
726
1251
|
);
|
|
727
1252
|
}
|
|
1253
|
+
const stopSessions = startSessions(shared, windowId);
|
|
728
1254
|
return () => {
|
|
729
1255
|
disposed = true;
|
|
730
1256
|
unsubscribe();
|
|
1257
|
+
stopSessions();
|
|
731
1258
|
if (timer) {
|
|
732
1259
|
clearTimeout(timer);
|
|
733
1260
|
save();
|
|
@@ -735,24 +1262,136 @@ function createCanvasController(options) {
|
|
|
735
1262
|
};
|
|
736
1263
|
}
|
|
737
1264
|
};
|
|
1265
|
+
shared.views.set(windowId, controller);
|
|
738
1266
|
return controller;
|
|
739
1267
|
}
|
|
1268
|
+
function startSessions(shared, windowId) {
|
|
1269
|
+
const { options, onError } = shared;
|
|
1270
|
+
const sessions = options.sessions;
|
|
1271
|
+
if (!sessions) return () => void 0;
|
|
1272
|
+
shared.started.set(windowId, (shared.started.get(windowId) ?? 0) + 1);
|
|
1273
|
+
let stopped = false;
|
|
1274
|
+
if (!shared.sessionStop) {
|
|
1275
|
+
const store = options.store;
|
|
1276
|
+
const local = store.authority !== "remote";
|
|
1277
|
+
let tracked = null;
|
|
1278
|
+
const call = (fn) => {
|
|
1279
|
+
try {
|
|
1280
|
+
fn();
|
|
1281
|
+
} catch (error) {
|
|
1282
|
+
onError({ code: "session-hook", message: "A canvas session hook threw.", detail: error });
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1285
|
+
const read = (fn) => {
|
|
1286
|
+
try {
|
|
1287
|
+
return fn();
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
onError({ code: "session-hook", message: "A canvas kind's sessionKey threw.", detail: error });
|
|
1290
|
+
return null;
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
let prevItems = /* @__PURE__ */ new Set();
|
|
1294
|
+
const snapshot = (state) => {
|
|
1295
|
+
const out = /* @__PURE__ */ new Map();
|
|
1296
|
+
for (const w of listCanvasWindowIds(state)) {
|
|
1297
|
+
if (!local && !shared.started.has(w)) continue;
|
|
1298
|
+
const frame2 = canvasWindowFrame(state, w);
|
|
1299
|
+
if (!frame2) continue;
|
|
1300
|
+
for (const paneId of listPaneIds(frame2.layout)) {
|
|
1301
|
+
for (const itemId of state.panes[paneId]?.itemIds ?? []) {
|
|
1302
|
+
const item = state.items[itemId];
|
|
1303
|
+
const hooks = item ? sessions(item.kind) : void 0;
|
|
1304
|
+
if (!item || !hooks) continue;
|
|
1305
|
+
const key = read(() => hooks.sessionKey(item));
|
|
1306
|
+
if (key) out.set(itemId, { item, key, windowId: w });
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
return out;
|
|
1311
|
+
};
|
|
1312
|
+
const sync = () => {
|
|
1313
|
+
const state = options.store.getState();
|
|
1314
|
+
if (!state.hydrated) return;
|
|
1315
|
+
const next = snapshot(state);
|
|
1316
|
+
const first = tracked === null;
|
|
1317
|
+
const prev = tracked ?? /* @__PURE__ */ new Map();
|
|
1318
|
+
for (const [id, entry] of prev) {
|
|
1319
|
+
const now = next.get(id);
|
|
1320
|
+
if (now && now.key === entry.key) continue;
|
|
1321
|
+
const hooks = sessions(entry.item.kind);
|
|
1322
|
+
const reason = state.items[id] && !now ? "leave" : "close";
|
|
1323
|
+
call(() => hooks?.onDetach?.({ item: entry.item, sessionKey: entry.key, windowId: entry.windowId, reason }));
|
|
1324
|
+
}
|
|
1325
|
+
for (const [id, entry] of next) {
|
|
1326
|
+
const before = prev.get(id);
|
|
1327
|
+
if (before && before.key === entry.key) continue;
|
|
1328
|
+
const hooks = sessions(entry.item.kind);
|
|
1329
|
+
const reason = first ? "restore" : prevItems.has(id) ? "arrive" : "open";
|
|
1330
|
+
call(() => hooks?.onAttach?.({ item: entry.item, sessionKey: entry.key, windowId: entry.windowId, reason }));
|
|
1331
|
+
}
|
|
1332
|
+
tracked = next;
|
|
1333
|
+
prevItems = new Set(Object.keys(state.items));
|
|
1334
|
+
};
|
|
1335
|
+
let lastState = null;
|
|
1336
|
+
const unsubscribe = store.subscribe(() => {
|
|
1337
|
+
const state = store.getState();
|
|
1338
|
+
if (state === lastState) return;
|
|
1339
|
+
lastState = state;
|
|
1340
|
+
sync();
|
|
1341
|
+
});
|
|
1342
|
+
sync();
|
|
1343
|
+
shared.sessionStop = () => {
|
|
1344
|
+
unsubscribe();
|
|
1345
|
+
for (const entry of tracked?.values() ?? []) {
|
|
1346
|
+
const hooks = sessions(entry.item.kind);
|
|
1347
|
+
call(() => hooks?.onDetach?.({ item: entry.item, sessionKey: entry.key, windowId: entry.windowId, reason: "unmount" }));
|
|
1348
|
+
}
|
|
1349
|
+
tracked = null;
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
return () => {
|
|
1353
|
+
if (stopped) return;
|
|
1354
|
+
stopped = true;
|
|
1355
|
+
const left = (shared.started.get(windowId) ?? 1) - 1;
|
|
1356
|
+
if (left > 0) shared.started.set(windowId, left);
|
|
1357
|
+
else shared.started.delete(windowId);
|
|
1358
|
+
if (shared.started.size === 0 && shared.sessionStop) {
|
|
1359
|
+
const stop = shared.sessionStop;
|
|
1360
|
+
shared.sessionStop = null;
|
|
1361
|
+
stop();
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
740
1365
|
|
|
741
1366
|
// src/core/selectors.ts
|
|
742
|
-
var
|
|
743
|
-
var
|
|
744
|
-
var
|
|
745
|
-
var
|
|
746
|
-
var
|
|
1367
|
+
var frame = (s, w) => canvasWindowFrame(s, w ?? CANVAS_MAIN_WINDOW);
|
|
1368
|
+
var selectCanvasIsOpen = (s, windowId) => frame(s, windowId)?.isOpen ?? false;
|
|
1369
|
+
var selectCanvasIsFullscreen = (s, windowId) => frame(s, windowId)?.isFullscreen ?? false;
|
|
1370
|
+
var selectCanvasWidth = (s, windowId) => frame(s, windowId)?.width ?? null;
|
|
1371
|
+
var selectCanvasLayout = (s, windowId) => frame(s, windowId)?.layout ?? null;
|
|
1372
|
+
var selectCanvasFocusedPaneId = (s, windowId) => frame(s, windowId)?.focusedPaneId ?? null;
|
|
747
1373
|
var selectCanvasIsHydrated = (s) => s.hydrated;
|
|
748
1374
|
var selectCanvasItemCount = (s) => Object.keys(s.items).length;
|
|
749
1375
|
var selectCanvasPane = (s, paneId) => s.panes[paneId];
|
|
750
1376
|
var selectCanvasItem = (s, itemId) => s.items[itemId];
|
|
751
|
-
function selectCanvasPaneCount(s) {
|
|
752
|
-
|
|
1377
|
+
function selectCanvasPaneCount(s, windowId) {
|
|
1378
|
+
const layout = frame(s, windowId)?.layout;
|
|
1379
|
+
return layout ? listPaneIds(layout).length : 0;
|
|
753
1380
|
}
|
|
754
|
-
function
|
|
755
|
-
const
|
|
1381
|
+
function selectCanvasWindowItemCount(s, windowId) {
|
|
1382
|
+
const layout = frame(s, windowId)?.layout;
|
|
1383
|
+
if (!layout) return 0;
|
|
1384
|
+
return listPaneIds(layout).reduce((sum, paneId) => sum + (s.panes[paneId]?.itemIds.length ?? 0), 0);
|
|
1385
|
+
}
|
|
1386
|
+
function selectCanvasActiveItem(s, windowId) {
|
|
1387
|
+
const f = frame(s, windowId);
|
|
1388
|
+
const pane = f ? s.panes[f.focusedPaneId] : void 0;
|
|
756
1389
|
return pane?.activeItemId ? s.items[pane.activeItemId] ?? null : null;
|
|
757
1390
|
}
|
|
1391
|
+
var NO_WINDOWS = Object.freeze({});
|
|
1392
|
+
function selectCanvasWindows(s) {
|
|
1393
|
+
return s.windows ?? NO_WINDOWS;
|
|
1394
|
+
}
|
|
1395
|
+
var selectCanvasExtraWindow = (s, windowId) => s.windows?.[windowId];
|
|
1396
|
+
var selectCanvasWindowOfItem = canvasWindowOfItem;
|
|
758
1397
|
//# sourceMappingURL=index.cjs.map
|