@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/react.js
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
import {
|
|
6
6
|
createContext,
|
|
7
7
|
useContext,
|
|
8
|
-
useEffect,
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
useEffect as useEffect2,
|
|
9
|
+
useRef as useRef2,
|
|
10
|
+
useState as useState2,
|
|
11
|
+
useSyncExternalStore as useSyncExternalStore2
|
|
11
12
|
} from "react";
|
|
12
13
|
|
|
13
14
|
// src/core/types.ts
|
|
15
|
+
var CANVAS_MAIN_WINDOW = "main";
|
|
14
16
|
var CANVAS_MIN_WIDTH = 360;
|
|
15
17
|
var CANVAS_DEFAULT_WIDTH = 640;
|
|
16
18
|
var CANVAS_MIN_SPLIT_FRACTION = 0.12;
|
|
@@ -55,12 +57,19 @@ function findNonJson(value, path = "data") {
|
|
|
55
57
|
return path;
|
|
56
58
|
}
|
|
57
59
|
}
|
|
60
|
+
function canvasWindowId(id) {
|
|
61
|
+
return id;
|
|
62
|
+
}
|
|
58
63
|
|
|
59
64
|
// src/core/layout.ts
|
|
60
65
|
function listPaneIds(node) {
|
|
61
66
|
if (node.type === "pane") return [node.paneId];
|
|
62
67
|
return node.children.flatMap(listPaneIds);
|
|
63
68
|
}
|
|
69
|
+
function containsPane(node, paneId) {
|
|
70
|
+
if (node.type === "pane") return node.paneId === paneId;
|
|
71
|
+
return node.children.some((child) => containsPane(child, paneId));
|
|
72
|
+
}
|
|
64
73
|
function evenSizes(count) {
|
|
65
74
|
return Array.from({ length: count }, () => 1 / count);
|
|
66
75
|
}
|
|
@@ -71,7 +80,8 @@ function normalizeSizes(sizes, count) {
|
|
|
71
80
|
const total = sizes.reduce((sum, s) => sum + s, 0);
|
|
72
81
|
return sizes.map((s) => s / total);
|
|
73
82
|
}
|
|
74
|
-
function splitPane(node, paneId, newPaneId, orientation, newSplitId) {
|
|
83
|
+
function splitPane(node, paneId, newPaneId, orientation, newSplitId, firstShare = 0.5) {
|
|
84
|
+
const first = Number.isFinite(firstShare) ? Math.min(1 - CANVAS_MIN_SPLIT_FRACTION, Math.max(CANVAS_MIN_SPLIT_FRACTION, firstShare)) : 0.5;
|
|
75
85
|
if (node.type === "pane") {
|
|
76
86
|
if (node.paneId !== paneId) return node;
|
|
77
87
|
return {
|
|
@@ -79,7 +89,7 @@ function splitPane(node, paneId, newPaneId, orientation, newSplitId) {
|
|
|
79
89
|
id: newSplitId,
|
|
80
90
|
orientation,
|
|
81
91
|
children: [node, { type: "pane", paneId: newPaneId }],
|
|
82
|
-
sizes: [
|
|
92
|
+
sizes: [first, 1 - first]
|
|
83
93
|
};
|
|
84
94
|
}
|
|
85
95
|
const index = node.children.findIndex(
|
|
@@ -90,13 +100,13 @@ function splitPane(node, paneId, newPaneId, orientation, newSplitId) {
|
|
|
90
100
|
const children = [...node.children];
|
|
91
101
|
children.splice(index + 1, 0, { type: "pane", paneId: newPaneId });
|
|
92
102
|
const sizes = [...node.sizes];
|
|
93
|
-
sizes.splice(index, 1, share
|
|
103
|
+
sizes.splice(index, 1, share * first, share * (1 - first));
|
|
94
104
|
return { ...node, children, sizes: normalizeSizes(sizes, children.length) };
|
|
95
105
|
}
|
|
96
106
|
return {
|
|
97
107
|
...node,
|
|
98
108
|
children: node.children.map(
|
|
99
|
-
(child) => splitPane(child, paneId, newPaneId, orientation, newSplitId)
|
|
109
|
+
(child) => splitPane(child, paneId, newPaneId, orientation, newSplitId, first)
|
|
100
110
|
)
|
|
101
111
|
};
|
|
102
112
|
}
|
|
@@ -115,6 +125,10 @@ function removePane(node, paneId) {
|
|
|
115
125
|
if (children.length === 1) return children[0] ?? null;
|
|
116
126
|
return { ...node, children, sizes: normalizeSizes(sizes, children.length) };
|
|
117
127
|
}
|
|
128
|
+
function containsSplit(node, splitId) {
|
|
129
|
+
if (node.type === "pane") return false;
|
|
130
|
+
return node.id === splitId || node.children.some((child) => containsSplit(child, splitId));
|
|
131
|
+
}
|
|
118
132
|
function resizeSplit(node, splitId, sizes) {
|
|
119
133
|
if (node.type === "pane") return node;
|
|
120
134
|
if (node.id === splitId) {
|
|
@@ -145,8 +159,12 @@ function isValidLayout(node, knownPanes, seen = /* @__PURE__ */ new Set()) {
|
|
|
145
159
|
|
|
146
160
|
// src/core/reducer.ts
|
|
147
161
|
var P = "matrxCanvas/";
|
|
162
|
+
var win = (windowId) => windowId && windowId !== CANVAS_MAIN_WINDOW ? { windowId } : {};
|
|
148
163
|
var canvasActions = {
|
|
149
|
-
open: (input) => ({
|
|
164
|
+
open: (input, options) => ({
|
|
165
|
+
type: `${P}open`,
|
|
166
|
+
payload: { ...input, now: Date.now(), ...options?.splitShare !== void 0 ? { splitShare: options.splitShare } : {} }
|
|
167
|
+
}),
|
|
150
168
|
update: (itemId, patch) => ({
|
|
151
169
|
type: `${P}update`,
|
|
152
170
|
payload: { itemId, ...patch, now: Date.now() }
|
|
@@ -161,19 +179,32 @@ var canvasActions = {
|
|
|
161
179
|
type: `${P}moveItem`,
|
|
162
180
|
payload: { itemId, toPaneId, index }
|
|
163
181
|
}),
|
|
164
|
-
|
|
182
|
+
/** `share` = the existing pane's fraction of the space (layout rules' stackSplit / sideSplit). */
|
|
183
|
+
splitPane: (paneId, orientation, moveItemId, share) => ({
|
|
165
184
|
type: `${P}splitPane`,
|
|
166
|
-
payload: { paneId, orientation, moveItemId }
|
|
185
|
+
payload: { paneId, orientation, moveItemId, ...share !== void 0 ? { share } : {} }
|
|
167
186
|
}),
|
|
168
187
|
closePane: (paneId) => ({ type: `${P}closePane`, payload: { paneId } }),
|
|
169
188
|
resizeSplit: (splitId, sizes) => ({
|
|
170
189
|
type: `${P}resizeSplit`,
|
|
171
190
|
payload: { splitId, sizes }
|
|
172
191
|
}),
|
|
173
|
-
setOpen: (open) => ({ type: `${P}setOpen`, payload: { open } }),
|
|
174
|
-
toggle: () =>
|
|
175
|
-
setFullscreen: (fullscreen) => ({
|
|
176
|
-
|
|
192
|
+
setOpen: (open, windowId) => ({ type: `${P}setOpen`, payload: { open, ...win(windowId) } }),
|
|
193
|
+
toggle: (windowId) => windowId && windowId !== CANVAS_MAIN_WINDOW ? { type: `${P}toggle`, payload: { windowId } } : { type: `${P}toggle` },
|
|
194
|
+
setFullscreen: (fullscreen, windowId) => ({
|
|
195
|
+
type: `${P}setFullscreen`,
|
|
196
|
+
payload: { fullscreen, ...win(windowId) }
|
|
197
|
+
}),
|
|
198
|
+
setWidth: (width, windowId) => ({ type: `${P}setWidth`, payload: { width, ...win(windowId) } }),
|
|
199
|
+
/** Moves a tab into its own pop-out window `windowId` (made by the host's popOut port). */
|
|
200
|
+
popOut: (itemId, windowId) => ({ type: `${P}popOut`, payload: { itemId, windowId } }),
|
|
201
|
+
/** Returns a popped-out tab to the window it came from. */
|
|
202
|
+
dockBack: (itemId) => ({ type: `${P}dockBack`, payload: { itemId } }),
|
|
203
|
+
/** A host window closed: its tabs dock home (default) or close with it. The main window never closes. */
|
|
204
|
+
closeWindow: (windowId, mode = "dock") => ({
|
|
205
|
+
type: `${P}closeWindow`,
|
|
206
|
+
payload: { windowId, mode }
|
|
207
|
+
}),
|
|
177
208
|
hydrate: (snapshot) => ({ type: `${P}hydrate`, payload: { snapshot } }),
|
|
178
209
|
reset: () => ({ type: `${P}reset` })
|
|
179
210
|
};
|
|
@@ -186,15 +217,80 @@ function createInitialCanvasState() {
|
|
|
186
217
|
version: 1,
|
|
187
218
|
isOpen: false,
|
|
188
219
|
isFullscreen: false,
|
|
189
|
-
width:
|
|
220
|
+
width: null,
|
|
190
221
|
layout: { type: "pane", paneId },
|
|
222
|
+
focusedPaneId: paneId,
|
|
223
|
+
windows: {},
|
|
191
224
|
panes: { [paneId]: { id: paneId, itemIds: [], activeItemId: null } },
|
|
192
225
|
items: {},
|
|
193
|
-
focusedPaneId: paneId,
|
|
194
226
|
seq: 1,
|
|
195
227
|
hydrated: false
|
|
196
228
|
};
|
|
197
229
|
}
|
|
230
|
+
function listCanvasWindowIds(state) {
|
|
231
|
+
return [CANVAS_MAIN_WINDOW, ...Object.keys(state.windows ?? {})];
|
|
232
|
+
}
|
|
233
|
+
function canvasWindowFrame(state, windowId = CANVAS_MAIN_WINDOW) {
|
|
234
|
+
return windowId === CANVAS_MAIN_WINDOW ? state : state.windows?.[windowId] ?? null;
|
|
235
|
+
}
|
|
236
|
+
function canvasWindowOfPane(state, paneId) {
|
|
237
|
+
for (const id of listCanvasWindowIds(state)) {
|
|
238
|
+
const frame2 = canvasWindowFrame(state, id);
|
|
239
|
+
if (frame2 && containsPane(frame2.layout, paneId)) return id;
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
function canvasWindowOfItem(state, itemId) {
|
|
244
|
+
const pane = paneOf(state, itemId);
|
|
245
|
+
return pane ? canvasWindowOfPane(state, pane.id) : null;
|
|
246
|
+
}
|
|
247
|
+
function withFrame(state, windowId, patch) {
|
|
248
|
+
if (windowId === CANVAS_MAIN_WINDOW) return { ...state, ...patch };
|
|
249
|
+
const current = state.windows[windowId];
|
|
250
|
+
if (!current) return state;
|
|
251
|
+
return { ...state, windows: { ...state.windows, [windowId]: { ...current, ...patch } } };
|
|
252
|
+
}
|
|
253
|
+
function ensureWindow(state, windowId, role = "window", home = CANVAS_MAIN_WINDOW) {
|
|
254
|
+
if (windowId === CANVAS_MAIN_WINDOW || state.windows[windowId]) return state;
|
|
255
|
+
const seq = state.seq + 1;
|
|
256
|
+
const paneId = paneIdFromSeq(seq);
|
|
257
|
+
const frame2 = {
|
|
258
|
+
id: windowId,
|
|
259
|
+
role,
|
|
260
|
+
home,
|
|
261
|
+
isOpen: false,
|
|
262
|
+
isFullscreen: false,
|
|
263
|
+
width: null,
|
|
264
|
+
layout: { type: "pane", paneId },
|
|
265
|
+
focusedPaneId: paneId
|
|
266
|
+
};
|
|
267
|
+
return {
|
|
268
|
+
...state,
|
|
269
|
+
seq,
|
|
270
|
+
panes: { ...state.panes, [paneId]: { id: paneId, itemIds: [], activeItemId: null } },
|
|
271
|
+
windows: { ...state.windows, [windowId]: frame2 }
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function removeWindow(state, windowId) {
|
|
275
|
+
const frame2 = state.windows[windowId];
|
|
276
|
+
if (!frame2) return state;
|
|
277
|
+
const panes = { ...state.panes };
|
|
278
|
+
for (const paneId of listPaneIds(frame2.layout)) delete panes[paneId];
|
|
279
|
+
const windows = { ...state.windows };
|
|
280
|
+
delete windows[windowId];
|
|
281
|
+
return { ...state, panes, windows };
|
|
282
|
+
}
|
|
283
|
+
function windowItemIds(state, windowId) {
|
|
284
|
+
const frame2 = canvasWindowFrame(state, windowId);
|
|
285
|
+
if (!frame2) return [];
|
|
286
|
+
return listPaneIds(frame2.layout).flatMap((paneId) => state.panes[paneId]?.itemIds ?? []);
|
|
287
|
+
}
|
|
288
|
+
function dropEmptyPopOut(state, windowId) {
|
|
289
|
+
if (!windowId || windowId === CANVAS_MAIN_WINDOW) return state;
|
|
290
|
+
const frame2 = state.windows[windowId];
|
|
291
|
+
if (!frame2 || frame2.role !== "pop-out" || windowItemIds(state, windowId).length > 0) return state;
|
|
292
|
+
return removeWindow(state, windowId);
|
|
293
|
+
}
|
|
198
294
|
function paneOf(state, itemId) {
|
|
199
295
|
for (const pane of Object.values(state.panes)) {
|
|
200
296
|
if (pane.itemIds.includes(itemId)) return pane;
|
|
@@ -204,6 +300,10 @@ function paneOf(state, itemId) {
|
|
|
204
300
|
function withPane(state, pane) {
|
|
205
301
|
return { ...state, panes: { ...state.panes, [pane.id]: pane } };
|
|
206
302
|
}
|
|
303
|
+
function focus(state, paneId) {
|
|
304
|
+
const windowId = canvasWindowOfPane(state, paneId);
|
|
305
|
+
return windowId ? withFrame(state, windowId, { focusedPaneId: paneId }) : state;
|
|
306
|
+
}
|
|
207
307
|
function nextActive(itemIds, removed, current) {
|
|
208
308
|
if (current !== removed) return current;
|
|
209
309
|
const index = itemIds.indexOf(removed);
|
|
@@ -220,27 +320,29 @@ function detachItem(state, itemId) {
|
|
|
220
320
|
});
|
|
221
321
|
}
|
|
222
322
|
function dropPane(state, paneId) {
|
|
223
|
-
const
|
|
323
|
+
const windowId = canvasWindowOfPane(state, paneId);
|
|
324
|
+
const frame2 = windowId ? canvasWindowFrame(state, windowId) : null;
|
|
325
|
+
if (!windowId || !frame2) return state;
|
|
326
|
+
const layout = removePane(frame2.layout, paneId);
|
|
224
327
|
if (!layout) return state;
|
|
225
328
|
const panes = { ...state.panes };
|
|
226
329
|
delete panes[paneId];
|
|
227
|
-
const focusedPaneId =
|
|
228
|
-
return { ...state,
|
|
330
|
+
const focusedPaneId = frame2.focusedPaneId === paneId ? listPaneIds(layout)[0] ?? frame2.focusedPaneId : frame2.focusedPaneId;
|
|
331
|
+
return withFrame({ ...state, panes }, windowId, { layout, focusedPaneId });
|
|
229
332
|
}
|
|
230
|
-
function addSplit(state, paneId, orientation) {
|
|
333
|
+
function addSplit(state, paneId, orientation, share) {
|
|
334
|
+
const windowId = canvasWindowOfPane(state, paneId);
|
|
335
|
+
const frame2 = windowId ? canvasWindowFrame(state, windowId) : null;
|
|
336
|
+
if (!windowId || !frame2) return null;
|
|
231
337
|
const seq = state.seq + 1;
|
|
232
338
|
const newPaneId = paneIdFromSeq(seq);
|
|
233
|
-
const layout = splitPane(
|
|
234
|
-
|
|
235
|
-
newPaneId,
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
panes: { ...state.panes, [newPaneId]: { id: newPaneId, itemIds: [], activeItemId: null } },
|
|
241
|
-
focusedPaneId: newPaneId
|
|
242
|
-
}
|
|
243
|
-
};
|
|
339
|
+
const layout = splitPane(frame2.layout, paneId, newPaneId, orientation, splitIdFromSeq(seq), share ?? 0.5);
|
|
340
|
+
const next = withFrame(
|
|
341
|
+
{ ...state, seq, panes: { ...state.panes, [newPaneId]: { id: newPaneId, itemIds: [], activeItemId: null } } },
|
|
342
|
+
windowId,
|
|
343
|
+
{ layout, focusedPaneId: newPaneId }
|
|
344
|
+
);
|
|
345
|
+
return { newPaneId, state: next };
|
|
244
346
|
}
|
|
245
347
|
function insertInto(state, paneId, itemId, activate, index) {
|
|
246
348
|
const pane = state.panes[paneId];
|
|
@@ -258,46 +360,96 @@ function afterActive(itemIds, active) {
|
|
|
258
360
|
const index = active ? itemIds.indexOf(active) : -1;
|
|
259
361
|
return index < 0 ? itemIds.length : index + 1;
|
|
260
362
|
}
|
|
363
|
+
function sendHome(state, itemId, home, activate) {
|
|
364
|
+
const homeFrame = canvasWindowFrame(state, home) ?? state;
|
|
365
|
+
const target = canvasWindowFrame(state, home) ? home : CANVAS_MAIN_WINDOW;
|
|
366
|
+
let next = detachItem(state, itemId);
|
|
367
|
+
next = insertInto(next, homeFrame.focusedPaneId, itemId, activate);
|
|
368
|
+
return activate ? withFrame(next, target, { focusedPaneId: homeFrame.focusedPaneId }) : next;
|
|
369
|
+
}
|
|
261
370
|
function clampWidth(width) {
|
|
262
|
-
return Number.isFinite(width) ? Math.max(CANVAS_MIN_WIDTH, Math.round(width)) :
|
|
371
|
+
return Number.isFinite(width) ? Math.max(CANVAS_MIN_WIDTH, Math.round(width)) : null;
|
|
372
|
+
}
|
|
373
|
+
function foldCanvasWindows(state) {
|
|
374
|
+
const extra = Object.keys(state.windows ?? {});
|
|
375
|
+
if (extra.length === 0) return state.windows ? state : { ...state, windows: {} };
|
|
376
|
+
let next = state;
|
|
377
|
+
for (const windowId of extra) {
|
|
378
|
+
for (const itemId of windowItemIds(next, windowId)) next = sendHome(next, itemId, CANVAS_MAIN_WINDOW, false);
|
|
379
|
+
next = removeWindow(next, windowId);
|
|
380
|
+
}
|
|
381
|
+
return next;
|
|
263
382
|
}
|
|
264
383
|
function sanitizeCanvasSnapshot(raw) {
|
|
265
384
|
if (!raw || typeof raw !== "object") return null;
|
|
266
385
|
const s = raw;
|
|
267
|
-
if (s.version !== 1 || !s.panes || !s.items || !s.layout) return null;
|
|
386
|
+
if (s.version !== 1 || !s.panes || typeof s.panes !== "object" || !s.items || typeof s.items !== "object" || !s.layout) return null;
|
|
268
387
|
const paneIds = new Set(Object.keys(s.panes));
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
388
|
+
const seen = /* @__PURE__ */ new Set();
|
|
389
|
+
if (!isValidLayout(s.layout, paneIds, seen)) return null;
|
|
390
|
+
const windows = {};
|
|
391
|
+
if (s.windows && typeof s.windows === "object") {
|
|
392
|
+
for (const [id, value] of Object.entries(s.windows)) {
|
|
393
|
+
if (!value || typeof value !== "object" || id === CANVAS_MAIN_WINDOW) continue;
|
|
394
|
+
const w = value;
|
|
395
|
+
const claimed = new Set(seen);
|
|
396
|
+
if (!isValidLayout(w.layout, paneIds, claimed)) continue;
|
|
397
|
+
for (const pane of claimed) seen.add(pane);
|
|
398
|
+
const layoutPanes = listPaneIds(w.layout);
|
|
399
|
+
const focused2 = w.focusedPaneId && layoutPanes.includes(w.focusedPaneId) ? w.focusedPaneId : layoutPanes[0];
|
|
400
|
+
if (!focused2) continue;
|
|
401
|
+
windows[id] = {
|
|
402
|
+
id,
|
|
403
|
+
role: w.role === "pop-out" ? "pop-out" : "window",
|
|
404
|
+
home: typeof w.home === "string" ? w.home : CANVAS_MAIN_WINDOW,
|
|
405
|
+
isOpen: w.isOpen === true,
|
|
406
|
+
isFullscreen: false,
|
|
407
|
+
width: typeof w.width === "number" ? clampWidth(w.width) : null,
|
|
408
|
+
layout: normalizeLayout(w.layout),
|
|
409
|
+
focusedPaneId: focused2
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (seen.size !== paneIds.size) return null;
|
|
272
414
|
const items = {};
|
|
273
415
|
for (const [id, item] of Object.entries(s.items)) {
|
|
274
416
|
if (item && typeof item.kind === "string" && typeof item.key === "string" && canvasItemId(item.kind, item.key) === id) {
|
|
275
417
|
items[id] = item;
|
|
276
418
|
}
|
|
277
419
|
}
|
|
420
|
+
const placed = /* @__PURE__ */ new Set();
|
|
278
421
|
const panes = {};
|
|
279
422
|
for (const [id, pane] of Object.entries(s.panes)) {
|
|
280
|
-
const itemIds = (Array.isArray(pane.itemIds) ? pane.itemIds : []).filter((itemId) => itemId in items);
|
|
423
|
+
const itemIds = (Array.isArray(pane.itemIds) ? pane.itemIds : []).filter((itemId) => itemId in items && !placed.has(itemId));
|
|
424
|
+
for (const itemId of itemIds) placed.add(itemId);
|
|
281
425
|
const activeItemId = pane.activeItemId && itemIds.includes(pane.activeItemId) ? pane.activeItemId : itemIds[0] ?? null;
|
|
282
426
|
panes[id] = { id, itemIds, activeItemId };
|
|
283
427
|
}
|
|
284
|
-
const
|
|
428
|
+
const mainPanes = listPaneIds(s.layout);
|
|
429
|
+
const focused = s.focusedPaneId && mainPanes.includes(s.focusedPaneId) ? s.focusedPaneId : mainPanes[0];
|
|
285
430
|
if (!focused) return null;
|
|
286
431
|
const layout = normalizeLayout(s.layout);
|
|
287
|
-
const
|
|
288
|
-
|
|
432
|
+
const highest = Math.max(highestIdNumber(layout), ...Object.values(windows).map((w) => highestIdNumber(w.layout)));
|
|
433
|
+
const seq = Math.max(typeof s.seq === "number" && Number.isFinite(s.seq) ? s.seq : 0, highest);
|
|
434
|
+
let state = {
|
|
289
435
|
version: 1,
|
|
290
436
|
isOpen: s.isOpen === true,
|
|
291
437
|
// Full screen is a moment, not a preference: a reload never starts there.
|
|
292
438
|
isFullscreen: false,
|
|
293
|
-
width:
|
|
439
|
+
width: typeof s.width === "number" ? clampWidth(s.width) : null,
|
|
294
440
|
layout,
|
|
441
|
+
focusedPaneId: focused,
|
|
442
|
+
windows,
|
|
295
443
|
panes,
|
|
296
444
|
items,
|
|
297
|
-
focusedPaneId: focused,
|
|
298
445
|
seq,
|
|
299
446
|
hydrated: true
|
|
300
447
|
};
|
|
448
|
+
state = foldCanvasWindows(state);
|
|
449
|
+
for (const id of Object.keys(items)) {
|
|
450
|
+
if (!placed.has(id)) state = insertInto(state, state.focusedPaneId, id, false);
|
|
451
|
+
}
|
|
452
|
+
return state;
|
|
301
453
|
}
|
|
302
454
|
function normalizeLayout(node) {
|
|
303
455
|
if (node.type === "pane") return node;
|
|
@@ -310,12 +462,14 @@ function highestIdNumber(node) {
|
|
|
310
462
|
}
|
|
311
463
|
function canvasReducer(state = createInitialCanvasState(), action) {
|
|
312
464
|
if (!isCanvasAction(action)) return state;
|
|
465
|
+
if (!state.windows) state = { ...state, windows: {} };
|
|
313
466
|
switch (action.type) {
|
|
314
467
|
case `${P}open`: {
|
|
315
|
-
const { kind, key, title, data, target = "focused", reveal = true, activate = true, now } = action.payload;
|
|
468
|
+
const { kind, key, title, data, target = "focused", reveal = true, activate = true, now, splitShare } = action.payload;
|
|
316
469
|
const itemId = canvasItemId(kind, key);
|
|
317
470
|
const existing = state.items[itemId];
|
|
318
471
|
let next = state;
|
|
472
|
+
let revealIn = action.payload.windowId ?? CANVAS_MAIN_WINDOW;
|
|
319
473
|
if (existing) {
|
|
320
474
|
const item = {
|
|
321
475
|
...existing,
|
|
@@ -325,23 +479,33 @@ function canvasReducer(state = createInitialCanvasState(), action) {
|
|
|
325
479
|
};
|
|
326
480
|
next = { ...next, items: { ...next.items, [itemId]: item } };
|
|
327
481
|
const pane = paneOf(next, itemId);
|
|
328
|
-
if (pane
|
|
329
|
-
|
|
482
|
+
if (pane) {
|
|
483
|
+
revealIn = canvasWindowOfPane(next, pane.id) ?? CANVAS_MAIN_WINDOW;
|
|
484
|
+
if (activate) next = focus(withPane(next, { ...pane, activeItemId: itemId }), pane.id);
|
|
485
|
+
} else {
|
|
486
|
+
next = ensureWindow(next, revealIn);
|
|
487
|
+
const frame2 = canvasWindowFrame(next, revealIn) ?? next;
|
|
488
|
+
next = insertInto(next, frame2.focusedPaneId, itemId, true);
|
|
489
|
+
}
|
|
330
490
|
} else {
|
|
331
491
|
const item = { id: itemId, kind, key, title: title ?? null, data: data ?? null, openedAt: now, updatedAt: now };
|
|
332
|
-
next = { ...next, items: { ...next.items, [itemId]: item } };
|
|
333
|
-
|
|
492
|
+
next = ensureWindow({ ...next, items: { ...next.items, [itemId]: item } }, revealIn);
|
|
493
|
+
const frame2 = canvasWindowFrame(next, revealIn) ?? next;
|
|
494
|
+
let paneId = frame2.focusedPaneId;
|
|
334
495
|
if (typeof target === "object" && next.panes[target.paneId]) {
|
|
335
496
|
paneId = target.paneId;
|
|
497
|
+
revealIn = canvasWindowOfPane(next, paneId) ?? revealIn;
|
|
336
498
|
} else if (target === "split-right" || target === "split-down") {
|
|
337
|
-
const split = addSplit(next,
|
|
338
|
-
|
|
339
|
-
|
|
499
|
+
const split = addSplit(next, frame2.focusedPaneId, target === "split-right" ? "horizontal" : "vertical", splitShare);
|
|
500
|
+
if (split) {
|
|
501
|
+
next = split.state;
|
|
502
|
+
paneId = split.newPaneId;
|
|
503
|
+
}
|
|
340
504
|
}
|
|
341
505
|
next = insertInto(next, paneId, itemId, activate);
|
|
342
|
-
if (activate) next =
|
|
506
|
+
if (activate) next = focus(next, paneId);
|
|
343
507
|
}
|
|
344
|
-
return reveal ?
|
|
508
|
+
return reveal ? withFrame(next, revealIn, { isOpen: true }) : next;
|
|
345
509
|
}
|
|
346
510
|
case `${P}update`: {
|
|
347
511
|
const { itemId, data, title, now } = action.payload;
|
|
@@ -377,12 +541,13 @@ function canvasReducer(state = createInitialCanvasState(), action) {
|
|
|
377
541
|
const { itemId } = action.payload;
|
|
378
542
|
if (!state.items[itemId]) return state;
|
|
379
543
|
const pane = paneOf(state, itemId);
|
|
544
|
+
const windowId = pane ? canvasWindowOfPane(state, pane.id) : null;
|
|
380
545
|
let next = detachItem(state, itemId);
|
|
381
546
|
const items = { ...next.items };
|
|
382
547
|
delete items[itemId];
|
|
383
548
|
next = { ...next, items };
|
|
384
549
|
if (pane && next.panes[pane.id]?.itemIds.length === 0) next = dropPane(next, pane.id);
|
|
385
|
-
return next;
|
|
550
|
+
return dropEmptyPopOut(next, windowId);
|
|
386
551
|
}
|
|
387
552
|
case `${P}closeOthers`: {
|
|
388
553
|
const { itemId } = action.payload;
|
|
@@ -395,26 +560,31 @@ function canvasReducer(state = createInitialCanvasState(), action) {
|
|
|
395
560
|
case `${P}activate`: {
|
|
396
561
|
const pane = paneOf(state, action.payload.itemId);
|
|
397
562
|
if (!pane) return state;
|
|
398
|
-
return
|
|
563
|
+
return focus(withPane(state, { ...pane, activeItemId: action.payload.itemId }), pane.id);
|
|
564
|
+
}
|
|
565
|
+
case `${P}focusPane`: {
|
|
566
|
+
const windowId = canvasWindowOfPane(state, action.payload.paneId);
|
|
567
|
+
const frame2 = windowId ? canvasWindowFrame(state, windowId) : null;
|
|
568
|
+
if (!windowId || !frame2 || frame2.focusedPaneId === action.payload.paneId) return state;
|
|
569
|
+
return withFrame(state, windowId, { focusedPaneId: action.payload.paneId });
|
|
399
570
|
}
|
|
400
|
-
case `${P}focusPane`:
|
|
401
|
-
return state.panes[action.payload.paneId] ? { ...state, focusedPaneId: action.payload.paneId } : state;
|
|
402
571
|
case `${P}moveItem`: {
|
|
403
572
|
const { itemId, toPaneId, index } = action.payload;
|
|
404
573
|
const from = paneOf(state, itemId);
|
|
405
574
|
if (!from || !state.panes[toPaneId]) return state;
|
|
406
|
-
if (from.id === toPaneId && index === void 0) return state
|
|
575
|
+
if (from.id === toPaneId && index === void 0) return canvasReducer(state, canvasActions.focusPane(toPaneId));
|
|
576
|
+
const fromWindow = canvasWindowOfPane(state, from.id);
|
|
407
577
|
let next = from.id === toPaneId ? state : detachItem(state, itemId);
|
|
408
|
-
next = insertInto(next, toPaneId, itemId, true, index);
|
|
409
|
-
next = { ...next, focusedPaneId: toPaneId };
|
|
578
|
+
next = focus(insertInto(next, toPaneId, itemId, true, index), toPaneId);
|
|
410
579
|
if (from.id !== toPaneId && next.panes[from.id]?.itemIds.length === 0) next = dropPane(next, from.id);
|
|
411
|
-
return next;
|
|
580
|
+
return dropEmptyPopOut(next, fromWindow);
|
|
412
581
|
}
|
|
413
582
|
case `${P}splitPane`: {
|
|
414
|
-
const { paneId, orientation, moveItemId } = action.payload;
|
|
583
|
+
const { paneId, orientation, moveItemId, share } = action.payload;
|
|
415
584
|
const pane = state.panes[paneId];
|
|
416
585
|
if (!pane) return state;
|
|
417
|
-
const split = addSplit(state, paneId, orientation);
|
|
586
|
+
const split = addSplit(state, paneId, orientation, share);
|
|
587
|
+
if (!split) return state;
|
|
418
588
|
let next = split.state;
|
|
419
589
|
if (moveItemId && pane.itemIds.includes(moveItemId) && pane.itemIds.length > 1) {
|
|
420
590
|
next = detachItem(next, moveItemId);
|
|
@@ -424,23 +594,106 @@ function canvasReducer(state = createInitialCanvasState(), action) {
|
|
|
424
594
|
}
|
|
425
595
|
case `${P}closePane`: {
|
|
426
596
|
const pane = state.panes[action.payload.paneId];
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
if (
|
|
597
|
+
const windowId = pane ? canvasWindowOfPane(state, pane.id) : null;
|
|
598
|
+
const frame2 = windowId ? canvasWindowFrame(state, windowId) : null;
|
|
599
|
+
if (!pane || !windowId || !frame2) return state;
|
|
600
|
+
const onlyPane = listPaneIds(frame2.layout).length === 1;
|
|
601
|
+
if (onlyPane) {
|
|
602
|
+
if (windowId !== CANVAS_MAIN_WINDOW && state.windows[windowId]?.role === "pop-out") {
|
|
603
|
+
return canvasReducer(state, canvasActions.closeWindow(windowId, "dock"));
|
|
604
|
+
}
|
|
605
|
+
return withFrame(state, windowId, { isOpen: false, isFullscreen: false });
|
|
606
|
+
}
|
|
430
607
|
const items = { ...state.items };
|
|
431
608
|
for (const id of pane.itemIds) delete items[id];
|
|
432
609
|
return dropPane({ ...state, items }, pane.id);
|
|
433
610
|
}
|
|
434
|
-
case `${P}resizeSplit`:
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
611
|
+
case `${P}resizeSplit`: {
|
|
612
|
+
const { splitId, sizes } = action.payload;
|
|
613
|
+
for (const windowId of listCanvasWindowIds(state)) {
|
|
614
|
+
const frame2 = canvasWindowFrame(state, windowId);
|
|
615
|
+
if (frame2 && containsSplit(frame2.layout, splitId)) {
|
|
616
|
+
return withFrame(state, windowId, { layout: resizeSplit(frame2.layout, splitId, sizes) });
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return state;
|
|
620
|
+
}
|
|
621
|
+
case `${P}setOpen`: {
|
|
622
|
+
const { open, windowId = CANVAS_MAIN_WINDOW } = action.payload;
|
|
623
|
+
const next = open ? ensureWindow(state, windowId) : state;
|
|
624
|
+
const frame2 = canvasWindowFrame(next, windowId);
|
|
625
|
+
if (!frame2 || frame2.isOpen === open) return next;
|
|
626
|
+
return withFrame(next, windowId, { isOpen: open, isFullscreen: open ? frame2.isFullscreen : false });
|
|
627
|
+
}
|
|
628
|
+
case `${P}toggle`: {
|
|
629
|
+
const windowId = action.payload?.windowId ?? CANVAS_MAIN_WINDOW;
|
|
630
|
+
const next = ensureWindow(state, windowId);
|
|
631
|
+
const frame2 = canvasWindowFrame(next, windowId);
|
|
632
|
+
if (!frame2) return state;
|
|
633
|
+
return withFrame(next, windowId, { isOpen: !frame2.isOpen, isFullscreen: frame2.isOpen ? false : frame2.isFullscreen });
|
|
634
|
+
}
|
|
635
|
+
case `${P}setFullscreen`: {
|
|
636
|
+
const { fullscreen, windowId = CANVAS_MAIN_WINDOW } = action.payload;
|
|
637
|
+
const next = fullscreen ? ensureWindow(state, windowId) : state;
|
|
638
|
+
const frame2 = canvasWindowFrame(next, windowId);
|
|
639
|
+
if (!frame2) return state;
|
|
640
|
+
return withFrame(next, windowId, { isFullscreen: fullscreen, isOpen: fullscreen ? true : frame2.isOpen });
|
|
641
|
+
}
|
|
642
|
+
case `${P}setWidth`: {
|
|
643
|
+
const { width, windowId = CANVAS_MAIN_WINDOW } = action.payload;
|
|
644
|
+
const next = ensureWindow(state, windowId);
|
|
645
|
+
return withFrame(next, windowId, { width: clampWidth(width) });
|
|
646
|
+
}
|
|
647
|
+
case `${P}popOut`: {
|
|
648
|
+
const { itemId, windowId } = action.payload;
|
|
649
|
+
const from = paneOf(state, itemId);
|
|
650
|
+
if (!from || windowId === CANVAS_MAIN_WINDOW) return state;
|
|
651
|
+
const fromWindow = canvasWindowOfPane(state, from.id) ?? CANVAS_MAIN_WINDOW;
|
|
652
|
+
if (fromWindow === windowId) return state;
|
|
653
|
+
let next = ensureWindow(state, windowId, "pop-out", fromWindow);
|
|
654
|
+
const target = canvasWindowFrame(next, windowId);
|
|
655
|
+
if (!target) return state;
|
|
656
|
+
next = detachItem(next, itemId);
|
|
657
|
+
next = insertInto(next, target.focusedPaneId, itemId, true);
|
|
658
|
+
next = withFrame(next, windowId, { isOpen: true, focusedPaneId: target.focusedPaneId });
|
|
659
|
+
if (next.panes[from.id]?.itemIds.length === 0) next = dropPane(next, from.id);
|
|
660
|
+
next = dropEmptyPopOut(next, fromWindow);
|
|
661
|
+
if (canvasWindowFrame(next, fromWindow) && windowItemIds(next, fromWindow).length === 0) {
|
|
662
|
+
next = withFrame(next, fromWindow, { isOpen: false, isFullscreen: false });
|
|
663
|
+
}
|
|
664
|
+
return next;
|
|
665
|
+
}
|
|
666
|
+
case `${P}dockBack`: {
|
|
667
|
+
const { itemId } = action.payload;
|
|
668
|
+
const windowId = canvasWindowOfItem(state, itemId);
|
|
669
|
+
if (!windowId || windowId === CANVAS_MAIN_WINDOW) return state;
|
|
670
|
+
const frame2 = state.windows[windowId];
|
|
671
|
+
if (!frame2) return state;
|
|
672
|
+
const home = canvasWindowFrame(state, frame2.home) ? frame2.home : CANVAS_MAIN_WINDOW;
|
|
673
|
+
let next = sendHome(state, itemId, home, true);
|
|
674
|
+
next = withFrame(next, home, { isOpen: true });
|
|
675
|
+
const left = paneOf(state, itemId);
|
|
676
|
+
if (left && next.panes[left.id]?.itemIds.length === 0) next = dropPane(next, left.id);
|
|
677
|
+
return dropEmptyPopOut(next, windowId);
|
|
678
|
+
}
|
|
679
|
+
case `${P}closeWindow`: {
|
|
680
|
+
const { windowId, mode } = action.payload;
|
|
681
|
+
const frame2 = state.windows[windowId];
|
|
682
|
+
if (!frame2) return state;
|
|
683
|
+
const itemIds = windowItemIds(state, windowId);
|
|
684
|
+
let next = state;
|
|
685
|
+
if (mode === "close") {
|
|
686
|
+
const items = { ...next.items };
|
|
687
|
+
for (const id of itemIds) delete items[id];
|
|
688
|
+
next = { ...next, items };
|
|
689
|
+
} else if (itemIds.length > 0) {
|
|
690
|
+
const home = canvasWindowFrame(state, frame2.home) ? frame2.home : CANVAS_MAIN_WINDOW;
|
|
691
|
+
const active = state.panes[frame2.focusedPaneId]?.activeItemId ?? null;
|
|
692
|
+
for (const id of itemIds) next = sendHome(next, id, home, id === active);
|
|
693
|
+
next = withFrame(next, home, { isOpen: true });
|
|
694
|
+
}
|
|
695
|
+
return removeWindow(next, windowId);
|
|
696
|
+
}
|
|
444
697
|
case `${P}hydrate`: {
|
|
445
698
|
const snapshot = action.payload.snapshot ? sanitizeCanvasSnapshot(action.payload.snapshot) : null;
|
|
446
699
|
if (!snapshot) return { ...state, hydrated: true };
|
|
@@ -505,6 +758,7 @@ function safeLocalStorage() {
|
|
|
505
758
|
}
|
|
506
759
|
}
|
|
507
760
|
function toPersistableSnapshot(state, isRestorable) {
|
|
761
|
+
state = foldCanvasWindows(state);
|
|
508
762
|
const keep = new Set(
|
|
509
763
|
Object.values(state.items).filter((item) => isRestorable(item.kind)).map((item) => item.id)
|
|
510
764
|
);
|
|
@@ -531,22 +785,156 @@ function toPersistableSnapshot(state, isRestorable) {
|
|
|
531
785
|
return { ...state, items, panes, layout, focusedPaneId };
|
|
532
786
|
}
|
|
533
787
|
|
|
788
|
+
// src/core/rules.ts
|
|
789
|
+
var CANVAS_WEB_LAYOUT_RULES = {
|
|
790
|
+
defaultWidth: CANVAS_DEFAULT_WIDTH,
|
|
791
|
+
minWidth: CANVAS_MIN_WIDTH,
|
|
792
|
+
centreMinWidth: 420,
|
|
793
|
+
stackSplit: 0.5,
|
|
794
|
+
sideSplit: 0.5,
|
|
795
|
+
paneMinWidth: 0
|
|
796
|
+
};
|
|
797
|
+
var clampFraction = (value, fallback) => Number.isFinite(value) ? Math.min(0.88, Math.max(0.12, value)) : fallback;
|
|
798
|
+
function resolveCanvasLayoutRules(rules) {
|
|
799
|
+
const merged = { ...CANVAS_WEB_LAYOUT_RULES, ...rules ?? {} };
|
|
800
|
+
const positive = (value, fallback) => Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
801
|
+
return {
|
|
802
|
+
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)) },
|
|
803
|
+
minWidth: positive(merged.minWidth, CANVAS_MIN_WIDTH),
|
|
804
|
+
centreMinWidth: positive(merged.centreMinWidth, CANVAS_WEB_LAYOUT_RULES.centreMinWidth),
|
|
805
|
+
stackSplit: clampFraction(merged.stackSplit, 0.5),
|
|
806
|
+
sideSplit: clampFraction(merged.sideSplit, 0.5),
|
|
807
|
+
paneMinWidth: positive(merged.paneMinWidth, 0)
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
function defaultCanvasWidth(rules, region) {
|
|
811
|
+
if (typeof rules.defaultWidth === "number") return rules.defaultWidth;
|
|
812
|
+
return region > 0 ? Math.round(region * rules.defaultWidth.fraction) : CANVAS_DEFAULT_WIDTH;
|
|
813
|
+
}
|
|
814
|
+
function fitCanvasWidth(stored, region, rules) {
|
|
815
|
+
const wanted = stored ?? defaultCanvasWidth(rules, region);
|
|
816
|
+
if (region <= 0) return Math.max(rules.minWidth, wanted);
|
|
817
|
+
return Math.max(rules.minWidth, Math.min(wanted, region - rules.centreMinWidth));
|
|
818
|
+
}
|
|
819
|
+
function clampDraggedCanvasWidth(next, region, rules) {
|
|
820
|
+
const max = Math.max(rules.minWidth, region - rules.centreMinWidth);
|
|
821
|
+
return Math.min(max, Math.max(rules.minWidth, Math.round(next)));
|
|
822
|
+
}
|
|
823
|
+
function shouldStackSplit(width, children, rules) {
|
|
824
|
+
return rules.paneMinWidth > 0 && width > 0 && width < rules.paneMinWidth * children;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/core/commands.ts
|
|
828
|
+
var CANVAS_SHORTCUTS = [
|
|
829
|
+
{ id: "canvas.toggle", accelerator: "CmdOrCtrl+\\", when: "presented", label: "Show or hide canvas", web: true },
|
|
830
|
+
{ id: "canvas.exitFullscreen", accelerator: "Escape", when: "fullscreen", label: "Restore canvas", web: true },
|
|
831
|
+
{ id: "canvas.toggleFullscreen", accelerator: "CmdOrCtrl+Shift+Enter", when: "canvas-focus", label: "Expand canvas", web: true },
|
|
832
|
+
{ id: "canvas.splitRight", accelerator: "CmdOrCtrl+Alt+\\", when: "canvas-focus", label: "Split right", web: true },
|
|
833
|
+
{ id: "canvas.splitDown", accelerator: "CmdOrCtrl+Alt+Shift+\\", when: "canvas-focus", label: "Split down", web: true },
|
|
834
|
+
{ id: "canvas.closeTab", accelerator: "CmdOrCtrl+W", when: "canvas-focus", label: "Close tab", web: false },
|
|
835
|
+
{ id: "canvas.nextTab", accelerator: "Ctrl+Tab", when: "canvas-focus", label: "Next tab", web: false },
|
|
836
|
+
{ id: "canvas.previousTab", accelerator: "Ctrl+Shift+Tab", when: "canvas-focus", label: "Previous tab", web: false }
|
|
837
|
+
];
|
|
838
|
+
function canvasShortcutHint(id, isMac3) {
|
|
839
|
+
const shortcut = CANVAS_SHORTCUTS.find((entry) => entry.id === id);
|
|
840
|
+
if (!shortcut) return null;
|
|
841
|
+
return shortcut.accelerator.split("+").map((part) => {
|
|
842
|
+
if (part === "CmdOrCtrl") return isMac3 ? "\u2318" : "Ctrl+";
|
|
843
|
+
if (part === "Ctrl") return isMac3 ? "\u2303" : "Ctrl+";
|
|
844
|
+
if (part === "Alt") return isMac3 ? "\u2325" : "Alt+";
|
|
845
|
+
if (part === "Shift") return isMac3 ? "\u21E7" : "Shift+";
|
|
846
|
+
if (part === "Escape") return "Esc";
|
|
847
|
+
return part;
|
|
848
|
+
}).join("");
|
|
849
|
+
}
|
|
850
|
+
function runCanvasCommand(controller, id, windowId = controller.windowId) {
|
|
851
|
+
const state = controller.getState();
|
|
852
|
+
const frame2 = canvasWindowFrame(state, windowId);
|
|
853
|
+
const pane = frame2 ? state.panes[frame2.focusedPaneId] : void 0;
|
|
854
|
+
switch (id) {
|
|
855
|
+
case "canvas.toggle":
|
|
856
|
+
controller.toggle(windowId);
|
|
857
|
+
return true;
|
|
858
|
+
case "canvas.toggleFullscreen":
|
|
859
|
+
controller.setFullscreen(!(frame2?.isFullscreen ?? false), windowId);
|
|
860
|
+
return true;
|
|
861
|
+
case "canvas.exitFullscreen":
|
|
862
|
+
if (!frame2?.isFullscreen) return false;
|
|
863
|
+
controller.setFullscreen(false, windowId);
|
|
864
|
+
return true;
|
|
865
|
+
case "canvas.closeTab":
|
|
866
|
+
if (!frame2?.isOpen || !pane?.activeItemId) return false;
|
|
867
|
+
controller.close(pane.activeItemId);
|
|
868
|
+
return true;
|
|
869
|
+
case "canvas.nextTab":
|
|
870
|
+
case "canvas.previousTab": {
|
|
871
|
+
if (!frame2?.isOpen || !pane?.activeItemId || pane.itemIds.length < 2) return false;
|
|
872
|
+
const at = pane.itemIds.indexOf(pane.activeItemId);
|
|
873
|
+
const step = id === "canvas.nextTab" ? 1 : -1;
|
|
874
|
+
const next = pane.itemIds[(at + step + pane.itemIds.length) % pane.itemIds.length];
|
|
875
|
+
if (next) controller.activate(next);
|
|
876
|
+
return true;
|
|
877
|
+
}
|
|
878
|
+
case "canvas.splitRight":
|
|
879
|
+
case "canvas.splitDown":
|
|
880
|
+
if (!frame2?.isOpen || !pane) return false;
|
|
881
|
+
controller.splitPane(pane.id, id === "canvas.splitRight" ? "horizontal" : "vertical", pane.itemIds.length > 1 ? pane.activeItemId ?? void 0 : void 0);
|
|
882
|
+
return true;
|
|
883
|
+
default:
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
var CODE_FOR = { "\\": "Backslash", Tab: "Tab", Escape: "Escape", Enter: "Enter" };
|
|
888
|
+
function matchesCanvasAccelerator(input, accelerator, isMac3) {
|
|
889
|
+
const parts = accelerator.split("+");
|
|
890
|
+
const key = parts.pop() ?? "";
|
|
891
|
+
const want = { meta: false, ctrl: false, alt: false, shift: false };
|
|
892
|
+
for (const part of parts) {
|
|
893
|
+
if (part === "CmdOrCtrl") want[isMac3 ? "meta" : "ctrl"] = true;
|
|
894
|
+
else if (part === "Cmd" || part === "Meta") want.meta = true;
|
|
895
|
+
else if (part === "Ctrl") want.ctrl = true;
|
|
896
|
+
else if (part === "Alt") want.alt = true;
|
|
897
|
+
else if (part === "Shift") want.shift = true;
|
|
898
|
+
}
|
|
899
|
+
if (input.metaKey !== want.meta || input.ctrlKey !== want.ctrl || input.altKey !== want.alt || input.shiftKey !== want.shift) return false;
|
|
900
|
+
const code = CODE_FOR[key] ?? (/^[A-Z]$/i.test(key) ? `Key${key.toUpperCase()}` : void 0);
|
|
901
|
+
if (code && input.code === code) return true;
|
|
902
|
+
return input.key.toLowerCase() === key.toLowerCase();
|
|
903
|
+
}
|
|
904
|
+
|
|
534
905
|
// src/core/controller.ts
|
|
535
906
|
var consoleCanvasErrorSink = (report) => {
|
|
536
907
|
console.error(`[@ai-matrx/canvas] ${report.code}: ${report.message}`, report.detail ?? "");
|
|
537
908
|
};
|
|
538
909
|
function createCanvasController(options) {
|
|
910
|
+
const shared = {
|
|
911
|
+
options,
|
|
912
|
+
onError: options.onError ?? consoleCanvasErrorSink,
|
|
913
|
+
rules: resolveCanvasLayoutRules(options.layoutRules),
|
|
914
|
+
presentations: /* @__PURE__ */ new Map(),
|
|
915
|
+
presentationListeners: /* @__PURE__ */ new Set(),
|
|
916
|
+
views: /* @__PURE__ */ new Map(),
|
|
917
|
+
started: /* @__PURE__ */ new Map(),
|
|
918
|
+
sessionStop: null
|
|
919
|
+
};
|
|
920
|
+
return view(shared, canvasWindowId(options.windowId ?? CANVAS_MAIN_WINDOW));
|
|
921
|
+
}
|
|
922
|
+
function view(shared, windowId) {
|
|
923
|
+
const cached = shared.views.get(windowId);
|
|
924
|
+
if (cached) return cached;
|
|
925
|
+
const { options, onError, rules } = shared;
|
|
539
926
|
const { store, persistence = null } = options;
|
|
540
|
-
const onError = options.onError ?? consoleCanvasErrorSink;
|
|
541
927
|
const isRestorable = options.isRestorable ?? (() => true);
|
|
542
928
|
const dispatch = (action) => store.dispatch(action);
|
|
543
|
-
let presentations = 0;
|
|
544
|
-
const presentationListeners = /* @__PURE__ */ new Set();
|
|
545
929
|
const notifyPresentation = () => {
|
|
546
|
-
for (const listener of [...presentationListeners]) listener();
|
|
930
|
+
for (const listener of [...shared.presentationListeners]) listener();
|
|
547
931
|
};
|
|
932
|
+
const shareFor = (orientation) => orientation === "vertical" ? rules.stackSplit : rules.sideSplit;
|
|
548
933
|
const controller = {
|
|
549
934
|
store,
|
|
935
|
+
windowId,
|
|
936
|
+
rules,
|
|
937
|
+
forWindow: (id) => view(shared, canvasWindowId(id)),
|
|
550
938
|
getState: () => store.getState(),
|
|
551
939
|
open(input) {
|
|
552
940
|
if (options.isKnownKind && !options.isKnownKind(input.kind)) {
|
|
@@ -564,7 +952,10 @@ function createCanvasController(options) {
|
|
|
564
952
|
return null;
|
|
565
953
|
}
|
|
566
954
|
}
|
|
567
|
-
|
|
955
|
+
const target = input.target ?? "focused";
|
|
956
|
+
const splitShare = target === "split-down" ? rules.stackSplit : target === "split-right" ? rules.sideSplit : void 0;
|
|
957
|
+
const resolved = input.windowId || windowId === CANVAS_MAIN_WINDOW ? input : { ...input, windowId };
|
|
958
|
+
dispatch(canvasActions.open(resolved, splitShare === void 0 ? void 0 : { splitShare }));
|
|
568
959
|
return canvasItemId(input.kind, input.key);
|
|
569
960
|
},
|
|
570
961
|
update(itemId, patch) {
|
|
@@ -590,42 +981,76 @@ function createCanvasController(options) {
|
|
|
590
981
|
activate: (itemId) => dispatch(canvasActions.activate(itemId)),
|
|
591
982
|
focusPane: (paneId) => dispatch(canvasActions.focusPane(paneId)),
|
|
592
983
|
moveItem: (itemId, toPaneId, index) => dispatch(canvasActions.moveItem(itemId, toPaneId, index)),
|
|
593
|
-
splitPane: (paneId, orientation, moveItemId) => dispatch(canvasActions.splitPane(paneId, orientation, moveItemId)),
|
|
984
|
+
splitPane: (paneId, orientation, moveItemId) => dispatch(canvasActions.splitPane(paneId, orientation, moveItemId, shareFor(orientation))),
|
|
594
985
|
closePane: (paneId) => dispatch(canvasActions.closePane(paneId)),
|
|
595
986
|
resizeSplit: (splitId, sizes) => dispatch(canvasActions.resizeSplit(splitId, sizes)),
|
|
596
|
-
show: () => dispatch(canvasActions.setOpen(true)),
|
|
597
|
-
hide: () => dispatch(canvasActions.setOpen(false)),
|
|
598
|
-
toggle: () => dispatch(canvasActions.toggle()),
|
|
599
|
-
setFullscreen: (fullscreen) => dispatch(canvasActions.setFullscreen(fullscreen)),
|
|
600
|
-
setWidth: (width) => dispatch(canvasActions.setWidth(width)),
|
|
987
|
+
show: (w = windowId) => dispatch(canvasActions.setOpen(true, w)),
|
|
988
|
+
hide: (w = windowId) => dispatch(canvasActions.setOpen(false, w)),
|
|
989
|
+
toggle: (w = windowId) => dispatch(canvasActions.toggle(w)),
|
|
990
|
+
setFullscreen: (fullscreen, w = windowId) => dispatch(canvasActions.setFullscreen(fullscreen, w)),
|
|
991
|
+
setWidth: (width, w = windowId) => dispatch(canvasActions.setWidth(width, w)),
|
|
992
|
+
async popOut(itemId) {
|
|
993
|
+
const item = store.getState().items[itemId];
|
|
994
|
+
if (!item) return null;
|
|
995
|
+
const port = options.windowPorts?.().popOut;
|
|
996
|
+
if (!port) {
|
|
997
|
+
onError({ code: "pop-out-unavailable", message: "This app cannot pop canvas tabs out into their own window.", detail: { itemId } });
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
let result;
|
|
1001
|
+
try {
|
|
1002
|
+
result = await port(item);
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
onError({ code: "pop-out-failed", message: "The window for this tab could not be opened.", detail: error });
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
if (!result) {
|
|
1008
|
+
onError({ code: "pop-out-refused", message: "The app declined to pop this tab out.", detail: { itemId } });
|
|
1009
|
+
return null;
|
|
1010
|
+
}
|
|
1011
|
+
const target = canvasWindowId(result.windowId);
|
|
1012
|
+
dispatch(canvasActions.popOut(itemId, target));
|
|
1013
|
+
return target;
|
|
1014
|
+
},
|
|
1015
|
+
dockBack(itemId) {
|
|
1016
|
+
const state = store.getState();
|
|
1017
|
+
const from = canvasWindowOfItem(state, itemId);
|
|
1018
|
+
if (!from || from === CANVAS_MAIN_WINDOW || state.windows[from]?.role !== "pop-out") return false;
|
|
1019
|
+
dispatch(canvasActions.dockBack(itemId));
|
|
1020
|
+
options.windowPorts?.().dockBack?.(itemId, from);
|
|
1021
|
+
return true;
|
|
1022
|
+
},
|
|
1023
|
+
closeWindow: (w, mode = "dock") => dispatch(canvasActions.closeWindow(w, mode)),
|
|
601
1024
|
has: (kind, key) => canvasItemId(kind, key) in store.getState().items,
|
|
602
|
-
isPresented: () => presentations > 0,
|
|
603
|
-
registerPresentation() {
|
|
604
|
-
presentations
|
|
1025
|
+
isPresented: (w = windowId) => (shared.presentations.get(w) ?? 0) > 0,
|
|
1026
|
+
registerPresentation(w = windowId) {
|
|
1027
|
+
shared.presentations.set(w, (shared.presentations.get(w) ?? 0) + 1);
|
|
605
1028
|
notifyPresentation();
|
|
606
1029
|
let done = false;
|
|
607
1030
|
return () => {
|
|
608
1031
|
if (done) return;
|
|
609
1032
|
done = true;
|
|
610
|
-
presentations
|
|
1033
|
+
shared.presentations.set(w, Math.max(0, (shared.presentations.get(w) ?? 1) - 1));
|
|
611
1034
|
notifyPresentation();
|
|
612
1035
|
};
|
|
613
1036
|
},
|
|
614
1037
|
subscribePresentation(listener) {
|
|
615
|
-
presentationListeners.add(listener);
|
|
616
|
-
return () => presentationListeners.delete(listener);
|
|
1038
|
+
shared.presentationListeners.add(listener);
|
|
1039
|
+
return () => shared.presentationListeners.delete(listener);
|
|
617
1040
|
},
|
|
618
1041
|
start() {
|
|
619
1042
|
let disposed = false;
|
|
620
1043
|
let timer = null;
|
|
621
1044
|
let lastSaved = null;
|
|
1045
|
+
const owns = store.authority !== "remote";
|
|
1046
|
+
const memory = owns ? persistence : null;
|
|
622
1047
|
const save = () => {
|
|
623
1048
|
timer = null;
|
|
624
1049
|
const state = store.getState();
|
|
625
|
-
if (!
|
|
1050
|
+
if (!memory || !state.hydrated || state === lastSaved) return;
|
|
626
1051
|
lastSaved = state;
|
|
627
1052
|
try {
|
|
628
|
-
void Promise.resolve(
|
|
1053
|
+
void Promise.resolve(memory.save(toPersistableSnapshot(state, isRestorable))).catch(
|
|
629
1054
|
(error) => onError({ code: "persistence-save", message: "Saving the canvas layout failed.", detail: error })
|
|
630
1055
|
);
|
|
631
1056
|
} catch (error) {
|
|
@@ -634,18 +1059,18 @@ function createCanvasController(options) {
|
|
|
634
1059
|
};
|
|
635
1060
|
let seen = store.getState();
|
|
636
1061
|
const unsubscribe = store.subscribe(() => {
|
|
637
|
-
if (!
|
|
1062
|
+
if (!memory || disposed) return;
|
|
638
1063
|
const current = store.getState();
|
|
639
1064
|
if (current === seen) return;
|
|
640
1065
|
seen = current;
|
|
641
1066
|
if (timer) clearTimeout(timer);
|
|
642
1067
|
timer = setTimeout(save, options.saveDelayMs ?? 250);
|
|
643
1068
|
});
|
|
644
|
-
if (store.getState().hydrated) {
|
|
645
|
-
} else if (!
|
|
1069
|
+
if (!owns || store.getState().hydrated) {
|
|
1070
|
+
} else if (!memory) {
|
|
646
1071
|
dispatch(canvasActions.hydrate(null));
|
|
647
1072
|
} else {
|
|
648
|
-
Promise.resolve().then(() =>
|
|
1073
|
+
Promise.resolve().then(() => memory.load()).then(
|
|
649
1074
|
(snapshot) => {
|
|
650
1075
|
if (disposed) return;
|
|
651
1076
|
const openedThisSession = store.getState().isOpen;
|
|
@@ -664,9 +1089,11 @@ function createCanvasController(options) {
|
|
|
664
1089
|
}
|
|
665
1090
|
);
|
|
666
1091
|
}
|
|
1092
|
+
const stopSessions = startSessions(shared, windowId);
|
|
667
1093
|
return () => {
|
|
668
1094
|
disposed = true;
|
|
669
1095
|
unsubscribe();
|
|
1096
|
+
stopSessions();
|
|
670
1097
|
if (timer) {
|
|
671
1098
|
clearTimeout(timer);
|
|
672
1099
|
save();
|
|
@@ -674,26 +1101,245 @@ function createCanvasController(options) {
|
|
|
674
1101
|
};
|
|
675
1102
|
}
|
|
676
1103
|
};
|
|
1104
|
+
shared.views.set(windowId, controller);
|
|
677
1105
|
return controller;
|
|
678
1106
|
}
|
|
1107
|
+
function startSessions(shared, windowId) {
|
|
1108
|
+
const { options, onError } = shared;
|
|
1109
|
+
const sessions = options.sessions;
|
|
1110
|
+
if (!sessions) return () => void 0;
|
|
1111
|
+
shared.started.set(windowId, (shared.started.get(windowId) ?? 0) + 1);
|
|
1112
|
+
let stopped = false;
|
|
1113
|
+
if (!shared.sessionStop) {
|
|
1114
|
+
const store = options.store;
|
|
1115
|
+
const local = store.authority !== "remote";
|
|
1116
|
+
let tracked = null;
|
|
1117
|
+
const call = (fn) => {
|
|
1118
|
+
try {
|
|
1119
|
+
fn();
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
onError({ code: "session-hook", message: "A canvas session hook threw.", detail: error });
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
const read = (fn) => {
|
|
1125
|
+
try {
|
|
1126
|
+
return fn();
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
onError({ code: "session-hook", message: "A canvas kind's sessionKey threw.", detail: error });
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
let prevItems = /* @__PURE__ */ new Set();
|
|
1133
|
+
const snapshot = (state) => {
|
|
1134
|
+
const out = /* @__PURE__ */ new Map();
|
|
1135
|
+
for (const w of listCanvasWindowIds(state)) {
|
|
1136
|
+
if (!local && !shared.started.has(w)) continue;
|
|
1137
|
+
const frame2 = canvasWindowFrame(state, w);
|
|
1138
|
+
if (!frame2) continue;
|
|
1139
|
+
for (const paneId of listPaneIds(frame2.layout)) {
|
|
1140
|
+
for (const itemId of state.panes[paneId]?.itemIds ?? []) {
|
|
1141
|
+
const item = state.items[itemId];
|
|
1142
|
+
const hooks = item ? sessions(item.kind) : void 0;
|
|
1143
|
+
if (!item || !hooks) continue;
|
|
1144
|
+
const key = read(() => hooks.sessionKey(item));
|
|
1145
|
+
if (key) out.set(itemId, { item, key, windowId: w });
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return out;
|
|
1150
|
+
};
|
|
1151
|
+
const sync = () => {
|
|
1152
|
+
const state = options.store.getState();
|
|
1153
|
+
if (!state.hydrated) return;
|
|
1154
|
+
const next = snapshot(state);
|
|
1155
|
+
const first = tracked === null;
|
|
1156
|
+
const prev = tracked ?? /* @__PURE__ */ new Map();
|
|
1157
|
+
for (const [id, entry] of prev) {
|
|
1158
|
+
const now = next.get(id);
|
|
1159
|
+
if (now && now.key === entry.key) continue;
|
|
1160
|
+
const hooks = sessions(entry.item.kind);
|
|
1161
|
+
const reason = state.items[id] && !now ? "leave" : "close";
|
|
1162
|
+
call(() => hooks?.onDetach?.({ item: entry.item, sessionKey: entry.key, windowId: entry.windowId, reason }));
|
|
1163
|
+
}
|
|
1164
|
+
for (const [id, entry] of next) {
|
|
1165
|
+
const before = prev.get(id);
|
|
1166
|
+
if (before && before.key === entry.key) continue;
|
|
1167
|
+
const hooks = sessions(entry.item.kind);
|
|
1168
|
+
const reason = first ? "restore" : prevItems.has(id) ? "arrive" : "open";
|
|
1169
|
+
call(() => hooks?.onAttach?.({ item: entry.item, sessionKey: entry.key, windowId: entry.windowId, reason }));
|
|
1170
|
+
}
|
|
1171
|
+
tracked = next;
|
|
1172
|
+
prevItems = new Set(Object.keys(state.items));
|
|
1173
|
+
};
|
|
1174
|
+
let lastState = null;
|
|
1175
|
+
const unsubscribe = store.subscribe(() => {
|
|
1176
|
+
const state = store.getState();
|
|
1177
|
+
if (state === lastState) return;
|
|
1178
|
+
lastState = state;
|
|
1179
|
+
sync();
|
|
1180
|
+
});
|
|
1181
|
+
sync();
|
|
1182
|
+
shared.sessionStop = () => {
|
|
1183
|
+
unsubscribe();
|
|
1184
|
+
for (const entry of tracked?.values() ?? []) {
|
|
1185
|
+
const hooks = sessions(entry.item.kind);
|
|
1186
|
+
call(() => hooks?.onDetach?.({ item: entry.item, sessionKey: entry.key, windowId: entry.windowId, reason: "unmount" }));
|
|
1187
|
+
}
|
|
1188
|
+
tracked = null;
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
return () => {
|
|
1192
|
+
if (stopped) return;
|
|
1193
|
+
stopped = true;
|
|
1194
|
+
const left = (shared.started.get(windowId) ?? 1) - 1;
|
|
1195
|
+
if (left > 0) shared.started.set(windowId, left);
|
|
1196
|
+
else shared.started.delete(windowId);
|
|
1197
|
+
if (shared.started.size === 0 && shared.sessionStop) {
|
|
1198
|
+
const stop = shared.sessionStop;
|
|
1199
|
+
shared.sessionStop = null;
|
|
1200
|
+
stop();
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
679
1204
|
|
|
680
1205
|
// src/core/selectors.ts
|
|
681
|
-
var
|
|
682
|
-
var
|
|
683
|
-
var
|
|
684
|
-
var
|
|
685
|
-
var
|
|
686
|
-
|
|
687
|
-
|
|
1206
|
+
var frame = (s, w) => canvasWindowFrame(s, w ?? CANVAS_MAIN_WINDOW);
|
|
1207
|
+
var selectCanvasIsOpen = (s, windowId) => frame(s, windowId)?.isOpen ?? false;
|
|
1208
|
+
var selectCanvasIsFullscreen = (s, windowId) => frame(s, windowId)?.isFullscreen ?? false;
|
|
1209
|
+
var selectCanvasWidth = (s, windowId) => frame(s, windowId)?.width ?? null;
|
|
1210
|
+
var selectCanvasLayout = (s, windowId) => frame(s, windowId)?.layout ?? null;
|
|
1211
|
+
var selectCanvasFocusedPaneId = (s, windowId) => frame(s, windowId)?.focusedPaneId ?? null;
|
|
1212
|
+
function selectCanvasPaneCount(s, windowId) {
|
|
1213
|
+
const layout = frame(s, windowId)?.layout;
|
|
1214
|
+
return layout ? listPaneIds(layout).length : 0;
|
|
1215
|
+
}
|
|
1216
|
+
function selectCanvasWindowItemCount(s, windowId) {
|
|
1217
|
+
const layout = frame(s, windowId)?.layout;
|
|
1218
|
+
if (!layout) return 0;
|
|
1219
|
+
return listPaneIds(layout).reduce((sum, paneId) => sum + (s.panes[paneId]?.itemIds.length ?? 0), 0);
|
|
1220
|
+
}
|
|
1221
|
+
var NO_WINDOWS = Object.freeze({});
|
|
1222
|
+
|
|
1223
|
+
// src/react/layer.tsx
|
|
1224
|
+
import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
1225
|
+
import { createPortal } from "react-dom";
|
|
1226
|
+
import { jsx } from "react/jsx-runtime";
|
|
1227
|
+
function createCanvasLayer(getPorts) {
|
|
1228
|
+
let elements = [];
|
|
1229
|
+
let nextId = 1;
|
|
1230
|
+
const domListeners = /* @__PURE__ */ new Set();
|
|
1231
|
+
const emitDom = () => {
|
|
1232
|
+
for (const listener of [...domListeners]) listener();
|
|
1233
|
+
};
|
|
1234
|
+
let blocking = 0;
|
|
1235
|
+
const measures = /* @__PURE__ */ new Set();
|
|
1236
|
+
const last = /* @__PURE__ */ new Map();
|
|
1237
|
+
const remeasure = () => {
|
|
1238
|
+
for (const measure of [...measures]) measure();
|
|
1239
|
+
};
|
|
1240
|
+
return {
|
|
1241
|
+
show(element) {
|
|
1242
|
+
const host = getPorts().overlay;
|
|
1243
|
+
if (host) return host.show(element);
|
|
1244
|
+
const id = nextId++;
|
|
1245
|
+
elements = [...elements, [id, element]];
|
|
1246
|
+
emitDom();
|
|
1247
|
+
let done = false;
|
|
1248
|
+
return () => {
|
|
1249
|
+
if (done) return;
|
|
1250
|
+
done = true;
|
|
1251
|
+
elements = elements.filter(([key]) => key !== id);
|
|
1252
|
+
emitDom();
|
|
1253
|
+
};
|
|
1254
|
+
},
|
|
1255
|
+
beginBlocking() {
|
|
1256
|
+
blocking += 1;
|
|
1257
|
+
remeasure();
|
|
1258
|
+
let done = false;
|
|
1259
|
+
return () => {
|
|
1260
|
+
if (done) return;
|
|
1261
|
+
done = true;
|
|
1262
|
+
blocking -= 1;
|
|
1263
|
+
remeasure();
|
|
1264
|
+
};
|
|
1265
|
+
},
|
|
1266
|
+
isBlocked: () => blocking > 0,
|
|
1267
|
+
dom: {
|
|
1268
|
+
list: () => elements,
|
|
1269
|
+
subscribe(listener) {
|
|
1270
|
+
domListeners.add(listener);
|
|
1271
|
+
return () => domListeners.delete(listener);
|
|
1272
|
+
}
|
|
1273
|
+
},
|
|
1274
|
+
native: {
|
|
1275
|
+
register(measure) {
|
|
1276
|
+
measures.add(measure);
|
|
1277
|
+
return () => measures.delete(measure);
|
|
1278
|
+
},
|
|
1279
|
+
report(itemId, rect, info) {
|
|
1280
|
+
const key = rect ? `${rect.x},${rect.y},${rect.width},${rect.height},${rect.devicePixelRatio},${info.visible},${info.windowId}` : `null,${info.windowId}`;
|
|
1281
|
+
if (last.get(itemId) === key) return;
|
|
1282
|
+
if (rect) last.set(itemId, key);
|
|
1283
|
+
else last.delete(itemId);
|
|
1284
|
+
getPorts().onNativeBodyFrame?.(itemId, rect, info);
|
|
1285
|
+
},
|
|
1286
|
+
remeasure
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
function CanvasDomOverlay({ layer }) {
|
|
1291
|
+
const elements = useSyncExternalStore(layer.dom.subscribe, layer.dom.list, layer.dom.list);
|
|
1292
|
+
const [mounted, setMounted] = useState(false);
|
|
1293
|
+
useEffect(() => setMounted(true), []);
|
|
1294
|
+
if (!mounted || elements.length === 0 || typeof document === "undefined") return null;
|
|
1295
|
+
return createPortal(
|
|
1296
|
+
/* @__PURE__ */ jsx("div", { className: "mxc-overlay", "aria-hidden": "true", children: elements.map(([id, element]) => /* @__PURE__ */ jsx(FloatingView, { element }, id)) }),
|
|
1297
|
+
document.body
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
function FloatingView({ element }) {
|
|
1301
|
+
if (element.type === "drop-indicator") {
|
|
1302
|
+
const { x, y, width, height } = element.rect;
|
|
1303
|
+
return /* @__PURE__ */ jsx("div", { className: "mxc-drop-indicator", style: { left: x, top: y, width, height } });
|
|
1304
|
+
}
|
|
1305
|
+
if (element.type === "tooltip") return /* @__PURE__ */ jsx(Tooltip, { element });
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
1308
|
+
function Tooltip({ element }) {
|
|
1309
|
+
const ref = useRef(null);
|
|
1310
|
+
const { anchor, side, text } = element;
|
|
1311
|
+
const top = side === "bottom" ? anchor.y + anchor.height + 6 : anchor.y - 6;
|
|
1312
|
+
const centre = anchor.x + anchor.width / 2;
|
|
1313
|
+
useLayoutEffect(() => {
|
|
1314
|
+
const el = ref.current;
|
|
1315
|
+
if (!el) return;
|
|
1316
|
+
const width = el.offsetWidth;
|
|
1317
|
+
const left = Math.min(Math.max(4, centre - width / 2), Math.max(4, window.innerWidth - width - 4));
|
|
1318
|
+
el.style.left = `${left}px`;
|
|
1319
|
+
el.style.visibility = "visible";
|
|
1320
|
+
}, [centre, text]);
|
|
1321
|
+
return /* @__PURE__ */ jsx("div", { ref, className: "mxc-tooltip", "data-side": side, role: "tooltip", style: { top, left: centre, visibility: "hidden" }, children: text });
|
|
1322
|
+
}
|
|
1323
|
+
function toCanvasRect(rect) {
|
|
1324
|
+
return { x: rect.left, y: rect.top, width: rect.width, height: rect.height };
|
|
688
1325
|
}
|
|
689
1326
|
|
|
690
1327
|
// src/react/registry.ts
|
|
691
1328
|
function defineCanvasKind(kind) {
|
|
692
|
-
if (!kind.component && !kind.load) {
|
|
693
|
-
throw new Error(`[@ai-matrx/canvas] kind "${kind.id}" needs a component
|
|
1329
|
+
if (!kind.component && !kind.load && kind.body !== "native" && !kind.unavailable) {
|
|
1330
|
+
throw new Error(`[@ai-matrx/canvas] kind "${kind.id}" needs a component, a load(), a native body or an unavailable() state.`);
|
|
694
1331
|
}
|
|
695
1332
|
return kind;
|
|
696
1333
|
}
|
|
1334
|
+
function canvasSessionKey(kind, item) {
|
|
1335
|
+
if (kind.sessionKey) return kind.sessionKey(item.data, item);
|
|
1336
|
+
const data = item.data;
|
|
1337
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
1338
|
+
const key = data.sessionKey;
|
|
1339
|
+
return typeof key === "string" && key ? key : null;
|
|
1340
|
+
}
|
|
1341
|
+
return null;
|
|
1342
|
+
}
|
|
697
1343
|
var REGISTRY = /* @__PURE__ */ Symbol.for("ai-matrx.canvas.kinds");
|
|
698
1344
|
function registry() {
|
|
699
1345
|
const g = globalThis;
|
|
@@ -735,76 +1381,125 @@ function getCanvasKindsVersion() {
|
|
|
735
1381
|
}
|
|
736
1382
|
|
|
737
1383
|
// src/react/provider.tsx
|
|
738
|
-
import { jsx } from "react/jsx-runtime";
|
|
1384
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
739
1385
|
var PHONE_MAX_WIDTH = 767;
|
|
740
1386
|
var isPhoneViewport = () => typeof window !== "undefined" && window.innerWidth <= PHONE_MAX_WIDTH;
|
|
1387
|
+
var isMacPlatform = () => typeof navigator !== "undefined" && /mac|iphone|ipad/i.test(navigator.userAgentData?.platform ?? navigator.platform ?? "");
|
|
741
1388
|
function escapeBelongsElsewhere(event) {
|
|
742
1389
|
if (event.defaultPrevented) return true;
|
|
743
1390
|
const active = typeof document === "undefined" ? null : document.activeElement;
|
|
744
1391
|
return !!active?.closest?.('[role="menu"], [role="dialog"], [role="alertdialog"], [role="listbox"]');
|
|
745
1392
|
}
|
|
746
1393
|
var CanvasContext = createContext(null);
|
|
1394
|
+
function sessionHooksFor(kindId) {
|
|
1395
|
+
const kind = getCanvasKind(kindId);
|
|
1396
|
+
if (!kind || kind.restore !== "session") return void 0;
|
|
1397
|
+
return {
|
|
1398
|
+
sessionKey: (item) => canvasSessionKey(kind, item),
|
|
1399
|
+
onAttach: kind.onAttach,
|
|
1400
|
+
onDetach: kind.onDetach
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
747
1403
|
function CanvasProvider({
|
|
748
1404
|
children,
|
|
749
1405
|
store,
|
|
750
1406
|
persistence,
|
|
751
1407
|
onError,
|
|
752
1408
|
hotkeys = true,
|
|
753
|
-
|
|
754
|
-
|
|
1409
|
+
windowId: windowIdProp,
|
|
1410
|
+
layoutRules,
|
|
1411
|
+
regionWidth = null,
|
|
1412
|
+
tabStrip = "auto",
|
|
1413
|
+
...hostPorts
|
|
755
1414
|
}) {
|
|
756
|
-
const
|
|
757
|
-
|
|
1415
|
+
const portsRef = useRef2(hostPorts);
|
|
1416
|
+
portsRef.current = hostPorts;
|
|
1417
|
+
const [{ controller, layer }] = useState2(() => ({
|
|
1418
|
+
controller: createCanvasController({
|
|
758
1419
|
store: store ?? createCanvasStore(),
|
|
759
1420
|
persistence: persistence === void 0 ? createLocalStorageCanvasPersistence() : persistence,
|
|
760
1421
|
isRestorable: (kind) => getCanvasKind(kind)?.restore !== false,
|
|
761
1422
|
mayRestoreOpen: () => !isPhoneViewport(),
|
|
762
|
-
onError
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
1423
|
+
onError,
|
|
1424
|
+
windowId: windowIdProp ?? CANVAS_MAIN_WINDOW,
|
|
1425
|
+
layoutRules,
|
|
1426
|
+
windowPorts: () => portsRef.current,
|
|
1427
|
+
sessions: sessionHooksFor
|
|
1428
|
+
}),
|
|
1429
|
+
layer: createCanvasLayer(() => portsRef.current)
|
|
1430
|
+
}));
|
|
1431
|
+
const windowId = controller.windowId;
|
|
1432
|
+
useEffect2(() => controller.start(), [controller]);
|
|
1433
|
+
useEffect2(() => {
|
|
767
1434
|
if (!hotkeys) return;
|
|
1435
|
+
const isMac3 = isMacPlatform();
|
|
1436
|
+
const applies = (when, event) => {
|
|
1437
|
+
if (!controller.isPresented()) return false;
|
|
1438
|
+
if (when === "presented") return true;
|
|
1439
|
+
if (when === "fullscreen") return !!canvasWindowFrame(controller.getState(), windowId)?.isFullscreen && !escapeBelongsElsewhere(event);
|
|
1440
|
+
const column = document.activeElement?.closest?.("[data-mxc-window]");
|
|
1441
|
+
return column?.getAttribute("data-mxc-window") === windowId;
|
|
1442
|
+
};
|
|
768
1443
|
const onKey = (event) => {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
event
|
|
772
|
-
controller.
|
|
773
|
-
|
|
774
|
-
controller.setFullscreen(false);
|
|
1444
|
+
for (const shortcut of CANVAS_SHORTCUTS) {
|
|
1445
|
+
if (!shortcut.web || !matchesCanvasAccelerator(event, shortcut.accelerator, isMac3)) continue;
|
|
1446
|
+
if (!applies(shortcut.when, event)) return;
|
|
1447
|
+
if (runCanvasCommand(controller, shortcut.id)) event.preventDefault();
|
|
1448
|
+
return;
|
|
775
1449
|
}
|
|
776
1450
|
};
|
|
777
1451
|
window.addEventListener("keydown", onKey);
|
|
778
1452
|
return () => window.removeEventListener("keydown", onKey);
|
|
779
|
-
}, [controller, hotkeys]);
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1453
|
+
}, [controller, hotkeys, windowId]);
|
|
1454
|
+
const value = { controller, windowId, ports: hostPorts, layer, regionWidth, tabStrip };
|
|
1455
|
+
return /* @__PURE__ */ jsxs(CanvasContext.Provider, { value, children: [
|
|
1456
|
+
children,
|
|
1457
|
+
hostPorts.overlay ? null : /* @__PURE__ */ jsx2(CanvasDomOverlay, { layer })
|
|
1458
|
+
] });
|
|
1459
|
+
}
|
|
1460
|
+
function CanvasWindowScope({ windowId, children }) {
|
|
1461
|
+
const parent = useCanvasContext();
|
|
1462
|
+
const id = canvasWindowId(windowId);
|
|
1463
|
+
const value = { ...parent, windowId: id, controller: parent.controller.forWindow(id) };
|
|
1464
|
+
return /* @__PURE__ */ jsx2(CanvasContext.Provider, { value, children });
|
|
785
1465
|
}
|
|
786
1466
|
function useCanvasContext() {
|
|
787
1467
|
const value = useContext(CanvasContext);
|
|
788
1468
|
if (!value) throw new Error("[@ai-matrx/canvas] useCanvas* must be used inside <CanvasProvider>.");
|
|
789
1469
|
return value;
|
|
790
1470
|
}
|
|
1471
|
+
function useCanvasInternals() {
|
|
1472
|
+
return useCanvasContext();
|
|
1473
|
+
}
|
|
1474
|
+
function useOptionalCanvasInternals() {
|
|
1475
|
+
return useContext(CanvasContext);
|
|
1476
|
+
}
|
|
791
1477
|
function useCanvas() {
|
|
792
1478
|
return useCanvasContext().controller;
|
|
793
1479
|
}
|
|
794
1480
|
function useOptionalCanvas() {
|
|
795
1481
|
return useContext(CanvasContext)?.controller ?? null;
|
|
796
1482
|
}
|
|
1483
|
+
function useCanvasWindowId() {
|
|
1484
|
+
return useContext(CanvasContext)?.windowId ?? CANVAS_MAIN_WINDOW;
|
|
1485
|
+
}
|
|
797
1486
|
function useCanvasHostPorts() {
|
|
798
1487
|
return useCanvasContext().ports;
|
|
799
1488
|
}
|
|
1489
|
+
function useCanvasNativeBodies() {
|
|
1490
|
+
return useCanvasContext().layer.native;
|
|
1491
|
+
}
|
|
800
1492
|
function useRegisterCanvasPresence() {
|
|
801
|
-
const
|
|
802
|
-
|
|
1493
|
+
const context = useContext(CanvasContext);
|
|
1494
|
+
const controller = context?.controller;
|
|
1495
|
+
const windowId = context?.windowId;
|
|
1496
|
+
useEffect2(() => controller?.registerPresentation(windowId), [controller, windowId]);
|
|
803
1497
|
}
|
|
804
1498
|
var noPresence = () => () => void 0;
|
|
805
1499
|
function useCanvasIsPresented() {
|
|
806
|
-
const
|
|
807
|
-
|
|
1500
|
+
const context = useContext(CanvasContext);
|
|
1501
|
+
const controller = context?.controller;
|
|
1502
|
+
return useSyncExternalStore2(
|
|
808
1503
|
controller ? controller.subscribePresentation : noPresence,
|
|
809
1504
|
() => controller?.isPresented() ?? false,
|
|
810
1505
|
() => false
|
|
@@ -812,7 +1507,7 @@ function useCanvasIsPresented() {
|
|
|
812
1507
|
}
|
|
813
1508
|
function useCanvasState(selector) {
|
|
814
1509
|
const { store } = useCanvasContext().controller;
|
|
815
|
-
return
|
|
1510
|
+
return useSyncExternalStore2(
|
|
816
1511
|
store.subscribe,
|
|
817
1512
|
() => selector(store.getState()),
|
|
818
1513
|
() => selector(store.getState())
|
|
@@ -822,37 +1517,50 @@ var NO_SUBSCRIPTION = () => () => void 0;
|
|
|
822
1517
|
function useOptionalCanvasState(selector, fallback) {
|
|
823
1518
|
const store = useContext(CanvasContext)?.controller.store;
|
|
824
1519
|
const read = () => store ? selector(store.getState()) : fallback;
|
|
825
|
-
return
|
|
1520
|
+
return useSyncExternalStore2(store ? store.subscribe : NO_SUBSCRIPTION, read, read);
|
|
826
1521
|
}
|
|
827
1522
|
function useCanvasKinds() {
|
|
828
|
-
|
|
1523
|
+
useSyncExternalStore2(subscribeCanvasKinds, getCanvasKindsVersion, getCanvasKindsVersion);
|
|
829
1524
|
return listCanvasKinds();
|
|
830
1525
|
}
|
|
831
1526
|
function useCanvasKind(id) {
|
|
832
|
-
|
|
1527
|
+
useSyncExternalStore2(subscribeCanvasKinds, getCanvasKindsVersion, getCanvasKindsVersion);
|
|
833
1528
|
return getCanvasKind(id);
|
|
834
1529
|
}
|
|
835
1530
|
|
|
836
1531
|
// src/react/CanvasColumn.tsx
|
|
837
|
-
import {
|
|
838
|
-
|
|
1532
|
+
import {
|
|
1533
|
+
useEffect as useEffect5,
|
|
1534
|
+
useLayoutEffect as useLayoutEffect3,
|
|
1535
|
+
useRef as useRef5,
|
|
1536
|
+
useState as useState4,
|
|
1537
|
+
useSyncExternalStore as useSyncExternalStore3
|
|
1538
|
+
} from "react";
|
|
1539
|
+
import { TapTargetButtonTransparent } from "@ai-matrx/tap-target";
|
|
839
1540
|
|
|
840
1541
|
// src/react/CanvasPane.tsx
|
|
841
1542
|
import {
|
|
842
1543
|
Component,
|
|
843
1544
|
Suspense,
|
|
844
1545
|
lazy,
|
|
845
|
-
useEffect as
|
|
846
|
-
|
|
847
|
-
|
|
1546
|
+
useEffect as useEffect4,
|
|
1547
|
+
useLayoutEffect as useLayoutEffect2,
|
|
1548
|
+
useRef as useRef4
|
|
848
1549
|
} from "react";
|
|
849
|
-
import { TapTargetButtonTransparent } from "@ai-matrx/tap-target";
|
|
850
1550
|
import * as Menu from "@radix-ui/react-dropdown-menu";
|
|
851
1551
|
|
|
1552
|
+
// src/react/chrome.tsx
|
|
1553
|
+
import {
|
|
1554
|
+
forwardRef,
|
|
1555
|
+
useEffect as useEffect3,
|
|
1556
|
+
useRef as useRef3,
|
|
1557
|
+
useState as useState3
|
|
1558
|
+
} from "react";
|
|
1559
|
+
|
|
852
1560
|
// src/react/icons.tsx
|
|
853
|
-
import { jsx as
|
|
1561
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
854
1562
|
function Glyph({ children, ...props }) {
|
|
855
|
-
return /* @__PURE__ */
|
|
1563
|
+
return /* @__PURE__ */ jsx3(
|
|
856
1564
|
"svg",
|
|
857
1565
|
{
|
|
858
1566
|
xmlns: "http://www.w3.org/2000/svg",
|
|
@@ -870,61 +1578,335 @@ function Glyph({ children, ...props }) {
|
|
|
870
1578
|
}
|
|
871
1579
|
);
|
|
872
1580
|
}
|
|
873
|
-
var CloseIcon = (p) => /* @__PURE__ */
|
|
874
|
-
/* @__PURE__ */
|
|
875
|
-
/* @__PURE__ */
|
|
1581
|
+
var CloseIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
|
|
1582
|
+
/* @__PURE__ */ jsx3("path", { d: "M18 6 6 18" }),
|
|
1583
|
+
/* @__PURE__ */ jsx3("path", { d: "m6 6 12 12" })
|
|
1584
|
+
] });
|
|
1585
|
+
var MaximizeIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
|
|
1586
|
+
/* @__PURE__ */ jsx3("path", { d: "M15 3h6v6" }),
|
|
1587
|
+
/* @__PURE__ */ jsx3("path", { d: "m21 3-7 7" }),
|
|
1588
|
+
/* @__PURE__ */ jsx3("path", { d: "m3 21 7-7" }),
|
|
1589
|
+
/* @__PURE__ */ jsx3("path", { d: "M9 21H3v-6" })
|
|
1590
|
+
] });
|
|
1591
|
+
var MinimizeIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
|
|
1592
|
+
/* @__PURE__ */ jsx3("path", { d: "m14 10 7-7" }),
|
|
1593
|
+
/* @__PURE__ */ jsx3("path", { d: "M20 10h-6V4" }),
|
|
1594
|
+
/* @__PURE__ */ jsx3("path", { d: "m3 21 7-7" }),
|
|
1595
|
+
/* @__PURE__ */ jsx3("path", { d: "M4 14h6v6" })
|
|
1596
|
+
] });
|
|
1597
|
+
var MoreIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
|
|
1598
|
+
/* @__PURE__ */ jsx3("circle", { cx: "12", cy: "12", r: "1" }),
|
|
1599
|
+
/* @__PURE__ */ jsx3("circle", { cx: "12", cy: "5", r: "1" }),
|
|
1600
|
+
/* @__PURE__ */ jsx3("circle", { cx: "12", cy: "19", r: "1" })
|
|
1601
|
+
] });
|
|
1602
|
+
var SplitRightIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
|
|
1603
|
+
/* @__PURE__ */ jsx3("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
|
|
1604
|
+
/* @__PURE__ */ jsx3("path", { d: "M12 3v18" })
|
|
876
1605
|
] });
|
|
877
|
-
var
|
|
878
|
-
/* @__PURE__ */
|
|
879
|
-
/* @__PURE__ */
|
|
880
|
-
/* @__PURE__ */ jsx2("path", { d: "m3 21 7-7" }),
|
|
881
|
-
/* @__PURE__ */ jsx2("path", { d: "M9 21H3v-6" })
|
|
1606
|
+
var SplitDownIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
|
|
1607
|
+
/* @__PURE__ */ jsx3("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
|
|
1608
|
+
/* @__PURE__ */ jsx3("path", { d: "M3 12h18" })
|
|
882
1609
|
] });
|
|
883
|
-
var
|
|
884
|
-
/* @__PURE__ */
|
|
885
|
-
/* @__PURE__ */
|
|
886
|
-
/* @__PURE__ */ jsx2("path", { d: "m3 21 7-7" }),
|
|
887
|
-
/* @__PURE__ */ jsx2("path", { d: "M4 14h6v6" })
|
|
1610
|
+
var PanelRightIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
|
|
1611
|
+
/* @__PURE__ */ jsx3("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
|
|
1612
|
+
/* @__PURE__ */ jsx3("path", { d: "M15 3v18" })
|
|
888
1613
|
] });
|
|
889
|
-
var
|
|
890
|
-
/* @__PURE__ */
|
|
891
|
-
/* @__PURE__ */
|
|
892
|
-
/* @__PURE__ */
|
|
1614
|
+
var PopOutIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
|
|
1615
|
+
/* @__PURE__ */ jsx3("path", { d: "M15 3h6v6" }),
|
|
1616
|
+
/* @__PURE__ */ jsx3("path", { d: "M10 14 21 3" }),
|
|
1617
|
+
/* @__PURE__ */ jsx3("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" })
|
|
893
1618
|
] });
|
|
894
|
-
var
|
|
895
|
-
/* @__PURE__ */
|
|
896
|
-
/* @__PURE__ */
|
|
1619
|
+
var AlertIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
|
|
1620
|
+
/* @__PURE__ */ jsx3("circle", { cx: "12", cy: "12", r: "10" }),
|
|
1621
|
+
/* @__PURE__ */ jsx3("path", { d: "M12 8v4" }),
|
|
1622
|
+
/* @__PURE__ */ jsx3("path", { d: "M12 16h.01" })
|
|
897
1623
|
] });
|
|
898
|
-
var
|
|
899
|
-
/* @__PURE__ */
|
|
900
|
-
/* @__PURE__ */
|
|
1624
|
+
var ArrowLeftIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
|
|
1625
|
+
/* @__PURE__ */ jsx3("path", { d: "m12 19-7-7 7-7" }),
|
|
1626
|
+
/* @__PURE__ */ jsx3("path", { d: "M19 12H5" })
|
|
901
1627
|
] });
|
|
902
|
-
var
|
|
903
|
-
/* @__PURE__ */
|
|
904
|
-
/* @__PURE__ */
|
|
1628
|
+
var ArrowRightIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
|
|
1629
|
+
/* @__PURE__ */ jsx3("path", { d: "M5 12h14" }),
|
|
1630
|
+
/* @__PURE__ */ jsx3("path", { d: "m12 5 7 7-7 7" })
|
|
905
1631
|
] });
|
|
906
|
-
var
|
|
907
|
-
/* @__PURE__ */
|
|
908
|
-
/* @__PURE__ */
|
|
909
|
-
/* @__PURE__ */ jsx2("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" })
|
|
1632
|
+
var RotateCwIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
|
|
1633
|
+
/* @__PURE__ */ jsx3("path", { d: "M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" }),
|
|
1634
|
+
/* @__PURE__ */ jsx3("path", { d: "M21 3v5h-5" })
|
|
910
1635
|
] });
|
|
911
|
-
var
|
|
912
|
-
/* @__PURE__ */
|
|
913
|
-
/* @__PURE__ */
|
|
914
|
-
/* @__PURE__ */
|
|
1636
|
+
var GlobeIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
|
|
1637
|
+
/* @__PURE__ */ jsx3("circle", { cx: "12", cy: "12", r: "10" }),
|
|
1638
|
+
/* @__PURE__ */ jsx3("path", { d: "M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" }),
|
|
1639
|
+
/* @__PURE__ */ jsx3("path", { d: "M2 12h20" })
|
|
915
1640
|
] });
|
|
1641
|
+
var PlusIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
|
|
1642
|
+
/* @__PURE__ */ jsx3("path", { d: "M5 12h14" }),
|
|
1643
|
+
/* @__PURE__ */ jsx3("path", { d: "M12 5v14" })
|
|
1644
|
+
] });
|
|
1645
|
+
var DockIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
|
|
1646
|
+
/* @__PURE__ */ jsx3("path", { d: "M21 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6" }),
|
|
1647
|
+
/* @__PURE__ */ jsx3("path", { d: "M21 3 12 12" }),
|
|
1648
|
+
/* @__PURE__ */ jsx3("path", { d: "M12 6v6h6" })
|
|
1649
|
+
] });
|
|
1650
|
+
|
|
1651
|
+
// src/react/chrome.tsx
|
|
1652
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1653
|
+
var cx = (...names) => names.filter(Boolean).join(" ");
|
|
1654
|
+
function useTooltip(text) {
|
|
1655
|
+
const internals = useOptionalCanvasInternals();
|
|
1656
|
+
const timer = useRef3(null);
|
|
1657
|
+
const hide = useRef3(null);
|
|
1658
|
+
const close = () => {
|
|
1659
|
+
if (timer.current) clearTimeout(timer.current);
|
|
1660
|
+
timer.current = null;
|
|
1661
|
+
hide.current?.();
|
|
1662
|
+
hide.current = null;
|
|
1663
|
+
};
|
|
1664
|
+
useEffect3(() => close, []);
|
|
1665
|
+
if (!text) return { close, handlers: {} };
|
|
1666
|
+
if (!internals) return { close, handlers: { title: text } };
|
|
1667
|
+
const open = (element, delay) => {
|
|
1668
|
+
close();
|
|
1669
|
+
timer.current = setTimeout(() => {
|
|
1670
|
+
timer.current = null;
|
|
1671
|
+
if (!element.isConnected) return;
|
|
1672
|
+
hide.current = internals.layer.show({ type: "tooltip", text, anchor: toCanvasRect(element.getBoundingClientRect()), side: "bottom" });
|
|
1673
|
+
}, delay);
|
|
1674
|
+
};
|
|
1675
|
+
return {
|
|
1676
|
+
close,
|
|
1677
|
+
handlers: {
|
|
1678
|
+
onPointerEnter: (event) => {
|
|
1679
|
+
if (event.pointerType === "mouse") open(event.currentTarget, 450);
|
|
1680
|
+
},
|
|
1681
|
+
onPointerLeave: close,
|
|
1682
|
+
onPointerDown: close,
|
|
1683
|
+
onFocus: (event) => {
|
|
1684
|
+
if (event.currentTarget.matches(":focus-visible")) open(event.currentTarget, 0);
|
|
1685
|
+
},
|
|
1686
|
+
onBlur: close
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
var CanvasHeaderButton = forwardRef(function CanvasHeaderButton2({ label, shortcut, available = true, size = "sm", pressed, className, children, onClick, onPointerEnter, onPointerLeave, onPointerDown, onFocus, onBlur, ...rest }, ref) {
|
|
1691
|
+
const tooltip = useTooltip(shortcut ? `${label} (${shortcut})` : label);
|
|
1692
|
+
const own = tooltip.handlers;
|
|
1693
|
+
return /* @__PURE__ */ jsx4(
|
|
1694
|
+
"button",
|
|
1695
|
+
{
|
|
1696
|
+
ref,
|
|
1697
|
+
type: "button",
|
|
1698
|
+
"aria-label": label,
|
|
1699
|
+
"aria-pressed": pressed,
|
|
1700
|
+
"data-slot": "canvas-panel-action",
|
|
1701
|
+
"data-size": size,
|
|
1702
|
+
"data-available": available ? void 0 : "false",
|
|
1703
|
+
className: cx("mxc-icon-button", className),
|
|
1704
|
+
...rest,
|
|
1705
|
+
...own.title ? { title: own.title } : {},
|
|
1706
|
+
"aria-disabled": available ? rest["aria-disabled"] : true,
|
|
1707
|
+
onClick: (event) => {
|
|
1708
|
+
tooltip.close();
|
|
1709
|
+
if (available) onClick?.(event);
|
|
1710
|
+
},
|
|
1711
|
+
onPointerEnter: (event) => {
|
|
1712
|
+
onPointerEnter?.(event);
|
|
1713
|
+
own.onPointerEnter?.(event);
|
|
1714
|
+
},
|
|
1715
|
+
onPointerLeave: (event) => {
|
|
1716
|
+
onPointerLeave?.(event);
|
|
1717
|
+
own.onPointerLeave?.();
|
|
1718
|
+
},
|
|
1719
|
+
onPointerDown: (event) => {
|
|
1720
|
+
onPointerDown?.(event);
|
|
1721
|
+
own.onPointerDown?.();
|
|
1722
|
+
},
|
|
1723
|
+
onFocus: (event) => {
|
|
1724
|
+
onFocus?.(event);
|
|
1725
|
+
own.onFocus?.(event);
|
|
1726
|
+
},
|
|
1727
|
+
onBlur: (event) => {
|
|
1728
|
+
onBlur?.(event);
|
|
1729
|
+
own.onBlur?.();
|
|
1730
|
+
},
|
|
1731
|
+
children
|
|
1732
|
+
}
|
|
1733
|
+
);
|
|
1734
|
+
});
|
|
1735
|
+
var CanvasMenuTrigger = forwardRef(
|
|
1736
|
+
function CanvasMenuTrigger2({ label = "More", ...props }, ref) {
|
|
1737
|
+
return /* @__PURE__ */ jsx4(CanvasHeaderButton, { ref, label, ...props, children: /* @__PURE__ */ jsx4(MoreIcon, {}) });
|
|
1738
|
+
}
|
|
1739
|
+
);
|
|
1740
|
+
var CanvasPanel = forwardRef(function CanvasPanel2({
|
|
1741
|
+
panelId,
|
|
1742
|
+
title,
|
|
1743
|
+
leading,
|
|
1744
|
+
header,
|
|
1745
|
+
actions,
|
|
1746
|
+
renderMenu,
|
|
1747
|
+
expanded = false,
|
|
1748
|
+
onToggleExpand,
|
|
1749
|
+
expandShortcut,
|
|
1750
|
+
onHide,
|
|
1751
|
+
onClose,
|
|
1752
|
+
closeLabel = "Close",
|
|
1753
|
+
focused,
|
|
1754
|
+
bodyProps,
|
|
1755
|
+
className,
|
|
1756
|
+
children,
|
|
1757
|
+
...props
|
|
1758
|
+
}, ref) {
|
|
1759
|
+
return /* @__PURE__ */ jsxs3(
|
|
1760
|
+
"section",
|
|
1761
|
+
{
|
|
1762
|
+
ref,
|
|
1763
|
+
"data-slot": "canvas-panel",
|
|
1764
|
+
"data-panel": panelId,
|
|
1765
|
+
"data-expanded": expanded ? "" : void 0,
|
|
1766
|
+
"data-focused": focused ? "" : void 0,
|
|
1767
|
+
"aria-label": typeof title === "string" ? title : void 0,
|
|
1768
|
+
className: cx("mxc-panel", className),
|
|
1769
|
+
...props,
|
|
1770
|
+
children: [
|
|
1771
|
+
/* @__PURE__ */ jsxs3("header", { "data-slot": "canvas-panel-header", className: "mxc-pane-header", children: [
|
|
1772
|
+
header ?? /* @__PURE__ */ jsx4("span", { className: "mxc-panel-title", children: title }),
|
|
1773
|
+
/* @__PURE__ */ jsx4("div", { className: "mxc-pane-leading", children: leading }),
|
|
1774
|
+
/* @__PURE__ */ jsxs3("div", { className: "mxc-pane-actions", children: [
|
|
1775
|
+
actions,
|
|
1776
|
+
renderMenu ? renderMenu(/* @__PURE__ */ jsx4(CanvasMenuTrigger, {})) : null,
|
|
1777
|
+
onToggleExpand ? /* @__PURE__ */ jsx4(CanvasHeaderButton, { className: "mxc-desktop-only", label: expanded ? "Restore" : "Expand", shortcut: expandShortcut, onClick: onToggleExpand, children: expanded ? /* @__PURE__ */ jsx4(MinimizeIcon, {}) : /* @__PURE__ */ jsx4(MaximizeIcon, {}) }) : null,
|
|
1778
|
+
onHide ? /* @__PURE__ */ jsx4(CanvasHeaderButton, { className: "mxc-phone-only", label: "Hide canvas", onClick: onHide, children: /* @__PURE__ */ jsx4(PanelRightIcon, {}) }) : null,
|
|
1779
|
+
onClose ? /* @__PURE__ */ jsx4(CanvasHeaderButton, { label: closeLabel, onClick: onClose, children: /* @__PURE__ */ jsx4(CloseIcon, {}) }) : null
|
|
1780
|
+
] })
|
|
1781
|
+
] }),
|
|
1782
|
+
/* @__PURE__ */ jsx4("div", { "data-slot": "canvas-panel-body", ...bodyProps, className: cx("mxc-pane-body", bodyProps?.className), children })
|
|
1783
|
+
]
|
|
1784
|
+
}
|
|
1785
|
+
);
|
|
1786
|
+
});
|
|
1787
|
+
function CanvasPanelEmpty({ children, className }) {
|
|
1788
|
+
return /* @__PURE__ */ jsx4("div", { "data-slot": "canvas-panel-empty", className: cx("mxc-empty-line", className), children });
|
|
1789
|
+
}
|
|
1790
|
+
var CanvasTab = forwardRef(function CanvasTab2({ title, icon, note, active, onSelect, onClose, className, onKeyDown, onClick, ...rest }, ref) {
|
|
1791
|
+
const shown = title || "New tab";
|
|
1792
|
+
return /* @__PURE__ */ jsxs3(
|
|
1793
|
+
"div",
|
|
1794
|
+
{
|
|
1795
|
+
ref,
|
|
1796
|
+
role: "tab",
|
|
1797
|
+
"aria-selected": active,
|
|
1798
|
+
tabIndex: active ? 0 : -1,
|
|
1799
|
+
"data-slot": "browser-tab",
|
|
1800
|
+
"data-active": active ? "" : void 0,
|
|
1801
|
+
title: shown,
|
|
1802
|
+
className: cx("mxc-tab", className),
|
|
1803
|
+
...rest,
|
|
1804
|
+
onClick: (event) => {
|
|
1805
|
+
onClick?.(event);
|
|
1806
|
+
onSelect();
|
|
1807
|
+
},
|
|
1808
|
+
onKeyDown: (event) => {
|
|
1809
|
+
onKeyDown?.(event);
|
|
1810
|
+
if (event.defaultPrevented) return;
|
|
1811
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1812
|
+
event.preventDefault();
|
|
1813
|
+
onSelect();
|
|
1814
|
+
}
|
|
1815
|
+
},
|
|
1816
|
+
children: [
|
|
1817
|
+
/* @__PURE__ */ jsx4("span", { className: "mxc-tab-icon", children: icon }),
|
|
1818
|
+
/* @__PURE__ */ jsx4("span", { className: "mxc-tab-title", children: shown }),
|
|
1819
|
+
note ? /* @__PURE__ */ jsx4("span", { className: "mxc-tab-note", "data-slot": "browser-tab-note", children: note }) : null,
|
|
1820
|
+
onClose ? /* @__PURE__ */ jsx4(
|
|
1821
|
+
"button",
|
|
1822
|
+
{
|
|
1823
|
+
type: "button",
|
|
1824
|
+
className: "mxc-tab-close",
|
|
1825
|
+
"data-slot": "browser-tab-close",
|
|
1826
|
+
"aria-label": `Close ${shown}`,
|
|
1827
|
+
onClick: (event) => {
|
|
1828
|
+
event.stopPropagation();
|
|
1829
|
+
onClose();
|
|
1830
|
+
},
|
|
1831
|
+
children: /* @__PURE__ */ jsx4(CloseIcon, {})
|
|
1832
|
+
}
|
|
1833
|
+
) : null
|
|
1834
|
+
]
|
|
1835
|
+
}
|
|
1836
|
+
);
|
|
1837
|
+
});
|
|
1838
|
+
function BrowserTabs({ tabs, activeId, onSelect, onClose, onNew, newLabel = "New tab", label = "Tabs", className }) {
|
|
1839
|
+
return /* @__PURE__ */ jsxs3("div", { "data-slot": "browser-tabs", role: "tablist", "aria-label": label, className: cx("mxc-tabs", className), children: [
|
|
1840
|
+
tabs.map((tab) => /* @__PURE__ */ jsx4(
|
|
1841
|
+
CanvasTab,
|
|
1842
|
+
{
|
|
1843
|
+
"data-tab": tab.id,
|
|
1844
|
+
title: tab.title,
|
|
1845
|
+
note: tab.note,
|
|
1846
|
+
active: tab.id === activeId,
|
|
1847
|
+
icon: tab.glyph ?? (tab.icon ? /* @__PURE__ */ jsx4("img", { src: tab.icon, alt: "", className: "mxc-favicon" }) : /* @__PURE__ */ jsx4(GlobeIcon, {})),
|
|
1848
|
+
onSelect: () => onSelect(tab.id),
|
|
1849
|
+
onClose: () => onClose(tab.id)
|
|
1850
|
+
},
|
|
1851
|
+
tab.id
|
|
1852
|
+
)),
|
|
1853
|
+
onNew ? /* @__PURE__ */ jsx4(CanvasHeaderButton, { label: newLabel, "data-slot": "browser-tab-new", onClick: onNew, children: /* @__PURE__ */ jsx4(PlusIcon, {}) }) : null
|
|
1854
|
+
] });
|
|
1855
|
+
}
|
|
1856
|
+
function BrowserBar({ url, canGoBack, canGoForward, onBack, onForward, onReload, onNavigate, trailing, className }) {
|
|
1857
|
+
const [draft, setDraft] = useState3(url);
|
|
1858
|
+
const [editing, setEditing] = useState3(false);
|
|
1859
|
+
useEffect3(() => {
|
|
1860
|
+
if (!editing) setDraft(url);
|
|
1861
|
+
}, [url, editing]);
|
|
1862
|
+
return /* @__PURE__ */ jsxs3("div", { "data-slot": "browser-bar", className: cx("mxc-browser-bar", className), children: [
|
|
1863
|
+
/* @__PURE__ */ jsx4(CanvasHeaderButton, { size: "md", label: "Back", available: canGoBack, onClick: onBack, children: /* @__PURE__ */ jsx4(ArrowLeftIcon, {}) }),
|
|
1864
|
+
/* @__PURE__ */ jsx4(CanvasHeaderButton, { size: "md", label: "Forward", available: canGoForward, onClick: onForward, children: /* @__PURE__ */ jsx4(ArrowRightIcon, {}) }),
|
|
1865
|
+
/* @__PURE__ */ jsx4(CanvasHeaderButton, { size: "md", label: "Reload", onClick: onReload, children: /* @__PURE__ */ jsx4(RotateCwIcon, {}) }),
|
|
1866
|
+
/* @__PURE__ */ jsx4(
|
|
1867
|
+
"form",
|
|
1868
|
+
{
|
|
1869
|
+
className: "mxc-address-form",
|
|
1870
|
+
onSubmit: (event) => {
|
|
1871
|
+
event.preventDefault();
|
|
1872
|
+
setEditing(false);
|
|
1873
|
+
onNavigate(draft.trim());
|
|
1874
|
+
event.currentTarget.elements.namedItem("address")?.blur();
|
|
1875
|
+
},
|
|
1876
|
+
children: /* @__PURE__ */ jsx4(
|
|
1877
|
+
"input",
|
|
1878
|
+
{
|
|
1879
|
+
name: "address",
|
|
1880
|
+
"aria-label": "Address",
|
|
1881
|
+
"data-slot": "browser-address",
|
|
1882
|
+
className: "mxc-address",
|
|
1883
|
+
value: draft,
|
|
1884
|
+
placeholder: "Search or enter an address",
|
|
1885
|
+
spellCheck: false,
|
|
1886
|
+
onFocus: (event) => {
|
|
1887
|
+
setEditing(true);
|
|
1888
|
+
event.currentTarget.select();
|
|
1889
|
+
},
|
|
1890
|
+
onBlur: () => setEditing(false),
|
|
1891
|
+
onChange: (event) => setDraft(event.target.value)
|
|
1892
|
+
}
|
|
1893
|
+
)
|
|
1894
|
+
}
|
|
1895
|
+
),
|
|
1896
|
+
trailing ? /* @__PURE__ */ jsx4("div", { "data-slot": "browser-bar-trailing", className: "mxc-browser-bar-trailing", children: trailing }) : null
|
|
1897
|
+
] });
|
|
1898
|
+
}
|
|
916
1899
|
|
|
917
1900
|
// src/react/CanvasPane.tsx
|
|
918
|
-
import { Fragment, jsx as
|
|
1901
|
+
import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
919
1902
|
var DRAG_MIME = "application/x-matrx-canvas-item";
|
|
1903
|
+
var useIsoLayoutEffect = typeof window === "undefined" ? useEffect4 : useLayoutEffect2;
|
|
920
1904
|
var LAZY = /* @__PURE__ */ Symbol.for("ai-matrx.canvas.lazy");
|
|
921
1905
|
function kindComponent(kind) {
|
|
922
1906
|
if (kind.component) return kind.component;
|
|
923
1907
|
const holder = kind;
|
|
924
1908
|
if (!holder[LAZY] && kind.load) holder[LAZY] = lazy(kind.load);
|
|
925
|
-
|
|
926
|
-
if (!resolved) throw new Error(`[@ai-matrx/canvas] kind "${kind.id}" has nothing to render.`);
|
|
927
|
-
return resolved;
|
|
1909
|
+
return holder[LAZY] ?? null;
|
|
928
1910
|
}
|
|
929
1911
|
function resetKindComponent(kind) {
|
|
930
1912
|
if (kind) delete kind[LAZY];
|
|
@@ -940,128 +1922,134 @@ function itemTitle(item, kind) {
|
|
|
940
1922
|
}
|
|
941
1923
|
return kind?.label ?? item.kind;
|
|
942
1924
|
}
|
|
1925
|
+
var isMac = () => typeof navigator !== "undefined" && /mac|iphone|ipad/i.test(navigator.platform ?? "");
|
|
1926
|
+
function useTabDrag(itemId, title) {
|
|
1927
|
+
const { layer } = useCanvasInternals();
|
|
1928
|
+
const active = useRef4([]);
|
|
1929
|
+
const end = () => {
|
|
1930
|
+
for (const dispose of active.current.splice(0)) dispose();
|
|
1931
|
+
};
|
|
1932
|
+
useEffect4(() => end, []);
|
|
1933
|
+
return {
|
|
1934
|
+
draggable: true,
|
|
1935
|
+
onDragStart: (event) => {
|
|
1936
|
+
event.dataTransfer.setData(DRAG_MIME, itemId);
|
|
1937
|
+
event.dataTransfer.effectAllowed = "move";
|
|
1938
|
+
end();
|
|
1939
|
+
active.current.push(layer.beginBlocking(), layer.show({ type: "tab-drag", itemId, title }));
|
|
1940
|
+
},
|
|
1941
|
+
onDragEnd: end
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
943
1944
|
function CanvasPaneView({ paneId }) {
|
|
944
1945
|
const canvas = useCanvas();
|
|
1946
|
+
const { windowId, layer, ports, tabStrip } = useCanvasInternals();
|
|
945
1947
|
const pane = useCanvasState((s) => s.panes[paneId]);
|
|
946
1948
|
const items = useCanvasState((s) => s.items);
|
|
947
|
-
const
|
|
948
|
-
const
|
|
1949
|
+
const isFullscreen = useCanvasState((s) => selectCanvasIsFullscreen(s, windowId));
|
|
1950
|
+
const focused = useCanvasState((s) => selectCanvasFocusedPaneId(s, windowId) === paneId);
|
|
1951
|
+
const paneCount = useCanvasState((s) => selectCanvasPaneCount(s, windowId));
|
|
1952
|
+
const role = useCanvasState((s) => windowId === CANVAS_MAIN_WINDOW ? "main" : s.windows[windowId]?.role ?? "window");
|
|
1953
|
+
const active = pane?.activeItemId ? items[pane.activeItemId] : void 0;
|
|
1954
|
+
const kind = useCanvasKind(active?.kind ?? "");
|
|
1955
|
+
const drop = useRef4(null);
|
|
1956
|
+
const sectionRef = useRef4(null);
|
|
1957
|
+
useEffect4(() => () => drop.current?.(), []);
|
|
949
1958
|
if (!pane) return null;
|
|
950
|
-
const
|
|
1959
|
+
const kindProps = active && kind ? { item: active, data: active.data, paneId, isFocused: focused, canvas, windowId } : null;
|
|
1960
|
+
const Leading = kind?.HeaderLeading;
|
|
1961
|
+
const Action = kind?.HeaderAction;
|
|
1962
|
+
const single = tabStrip === "auto" && pane.itemIds.length === 1 && active;
|
|
1963
|
+
const hideDrop = () => {
|
|
1964
|
+
drop.current?.();
|
|
1965
|
+
drop.current = null;
|
|
1966
|
+
};
|
|
951
1967
|
const onDragOver = (event) => {
|
|
952
1968
|
if (!event.dataTransfer.types.includes(DRAG_MIME)) return;
|
|
953
1969
|
event.preventDefault();
|
|
954
1970
|
event.dataTransfer.dropEffect = "move";
|
|
955
|
-
|
|
1971
|
+
if (!drop.current && sectionRef.current) {
|
|
1972
|
+
drop.current = layer.show({ type: "drop-indicator", rect: toCanvasRect(sectionRef.current.getBoundingClientRect()), paneId });
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1975
|
+
const onDragLeave = (event) => {
|
|
1976
|
+
const next = event.relatedTarget;
|
|
1977
|
+
if (next && sectionRef.current?.contains(next)) return;
|
|
1978
|
+
hideDrop();
|
|
956
1979
|
};
|
|
957
1980
|
const onDrop = (event) => {
|
|
958
|
-
|
|
1981
|
+
hideDrop();
|
|
959
1982
|
const itemId = event.dataTransfer.getData(DRAG_MIME);
|
|
960
1983
|
if (!itemId) return;
|
|
961
1984
|
event.preventDefault();
|
|
962
1985
|
canvas.moveItem(itemId, paneId);
|
|
963
1986
|
};
|
|
964
|
-
|
|
965
|
-
|
|
1987
|
+
const closeLabel = role === "pop-out" && paneCount === 1 ? "Close window" : paneCount > 1 ? "Close pane" : "Close canvas";
|
|
1988
|
+
return /* @__PURE__ */ jsx5(
|
|
1989
|
+
CanvasPanel,
|
|
966
1990
|
{
|
|
1991
|
+
ref: sectionRef,
|
|
1992
|
+
panelId: paneId,
|
|
967
1993
|
className: "mxc-pane",
|
|
968
|
-
|
|
969
|
-
|
|
1994
|
+
title: active ? itemTitle(active, kind) : "Canvas",
|
|
1995
|
+
focused,
|
|
1996
|
+
"data-mxc-pane": paneId,
|
|
1997
|
+
header: single && active ? /* @__PURE__ */ jsx5(SingleTitle, { item: active, kind }) : pane.itemIds.length > 0 ? /* @__PURE__ */ jsx5("div", { className: "mxc-tabs", role: "tablist", "aria-label": "Canvas tabs", children: pane.itemIds.map((itemId, index) => /* @__PURE__ */ jsx5(ItemTab, { itemId, paneId, index, active: itemId === pane.activeItemId }, itemId)) }) : /* @__PURE__ */ jsx5("span", { className: "mxc-panel-title", children: "New tab" }),
|
|
1998
|
+
leading: Leading && kindProps ? /* @__PURE__ */ jsx5(Leading, { ...kindProps }) : null,
|
|
1999
|
+
actions: Action && kindProps ? /* @__PURE__ */ jsx5(Action, { ...kindProps }) : null,
|
|
2000
|
+
renderMenu: () => /* @__PURE__ */ jsx5(PaneMenu, { paneId, windowId, activeItem: active, kind, kindProps, tabCount: pane.itemIds.length, role }),
|
|
2001
|
+
expanded: isFullscreen,
|
|
2002
|
+
onToggleExpand: () => canvas.setFullscreen(!isFullscreen),
|
|
2003
|
+
expandShortcut: canvasShortcutHint("canvas.toggleFullscreen", isMac()),
|
|
2004
|
+
onHide: windowId === CANVAS_MAIN_WINDOW ? () => canvas.hide() : void 0,
|
|
2005
|
+
onClose: () => canvas.closePane(paneId),
|
|
2006
|
+
closeLabel,
|
|
970
2007
|
onPointerDownCapture: () => {
|
|
971
2008
|
if (!focused) canvas.focusPane(paneId);
|
|
972
2009
|
},
|
|
973
2010
|
onDragOver,
|
|
974
|
-
onDragLeave
|
|
2011
|
+
onDragLeave,
|
|
975
2012
|
onDrop,
|
|
976
|
-
children:
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
return /* @__PURE__ */ jsx3(ItemBody, { item, paneId, isActive, isFocused: focused && isActive }, itemId);
|
|
983
|
-
}) })
|
|
984
|
-
]
|
|
2013
|
+
children: pane.itemIds.length === 0 ? /* @__PURE__ */ jsx5(Launcher, { paneId }) : pane.itemIds.map((itemId) => {
|
|
2014
|
+
const item = items[itemId];
|
|
2015
|
+
if (!item) return null;
|
|
2016
|
+
const isActive = itemId === pane.activeItemId;
|
|
2017
|
+
return /* @__PURE__ */ jsx5(ItemBody, { item, paneId, windowId, isActive, isFocused: focused && isActive, hasNativePort: !!ports.onNativeBodyFrame }, itemId);
|
|
2018
|
+
})
|
|
985
2019
|
}
|
|
986
2020
|
);
|
|
987
2021
|
}
|
|
988
|
-
function
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
})
|
|
994
|
-
|
|
995
|
-
const isFullscreen = useCanvasState((s) => s.isFullscreen);
|
|
996
|
-
const paneCount = useCanvasState(selectCanvasPaneCount);
|
|
997
|
-
const kind = useCanvasKind(activeItem?.kind ?? "");
|
|
998
|
-
const kindProps = activeItem && kind ? { item: activeItem, data: activeItem.data, paneId, isFocused: true, canvas } : null;
|
|
999
|
-
const HeaderAction = kind?.HeaderAction;
|
|
1000
|
-
return /* @__PURE__ */ jsxs2("header", { className: "mxc-pane-header", children: [
|
|
1001
|
-
/* @__PURE__ */ jsx3("div", { className: "mxc-tabs", role: "tablist", "aria-label": "Canvas tabs", children: itemIds.map((itemId, index) => /* @__PURE__ */ jsx3(Tab, { itemId, paneId, index, active: itemId === activeItemId }, itemId)) }),
|
|
1002
|
-
/* @__PURE__ */ jsxs2("div", { className: "mxc-pane-actions", children: [
|
|
1003
|
-
HeaderAction && kindProps ? /* @__PURE__ */ jsx3(HeaderAction, { ...kindProps }) : null,
|
|
1004
|
-
/* @__PURE__ */ jsx3(PaneMenu, { paneId, activeItem, kind, kindProps, canSplitMove: itemIds.length > 1 }),
|
|
1005
|
-
/* @__PURE__ */ jsx3(
|
|
1006
|
-
TapTargetButtonTransparent,
|
|
1007
|
-
{
|
|
1008
|
-
className: "mxc-desktop-only",
|
|
1009
|
-
ariaLabel: isFullscreen ? "Exit full screen" : "Full screen",
|
|
1010
|
-
icon: isFullscreen ? /* @__PURE__ */ jsx3(MinimizeIcon, {}) : /* @__PURE__ */ jsx3(MaximizeIcon, {}),
|
|
1011
|
-
onClick: () => canvas.setFullscreen(!isFullscreen)
|
|
1012
|
-
}
|
|
1013
|
-
),
|
|
1014
|
-
/* @__PURE__ */ jsx3(
|
|
1015
|
-
TapTargetButtonTransparent,
|
|
1016
|
-
{
|
|
1017
|
-
className: "mxc-phone-only",
|
|
1018
|
-
ariaLabel: "Hide canvas",
|
|
1019
|
-
icon: /* @__PURE__ */ jsx3(PanelRightIcon, {}),
|
|
1020
|
-
onClick: () => canvas.hide()
|
|
1021
|
-
}
|
|
1022
|
-
),
|
|
1023
|
-
/* @__PURE__ */ jsx3(
|
|
1024
|
-
TapTargetButtonTransparent,
|
|
1025
|
-
{
|
|
1026
|
-
ariaLabel: paneCount > 1 ? "Close pane" : "Close canvas",
|
|
1027
|
-
icon: /* @__PURE__ */ jsx3(CloseIcon, {}),
|
|
1028
|
-
onClick: () => canvas.closePane(paneId)
|
|
1029
|
-
}
|
|
1030
|
-
)
|
|
1031
|
-
] })
|
|
2022
|
+
function SingleTitle({ item, kind }) {
|
|
2023
|
+
const title = itemTitle(item, kind);
|
|
2024
|
+
const drag = useTabDrag(item.id, title);
|
|
2025
|
+
const Icon = kind?.icon ?? AlertIcon;
|
|
2026
|
+
return /* @__PURE__ */ jsxs4("span", { className: "mxc-panel-title", "data-draggable": "", title, ...drag, children: [
|
|
2027
|
+
/* @__PURE__ */ jsx5(Icon, { className: "mxc-title-icon" }),
|
|
2028
|
+
/* @__PURE__ */ jsx5("span", { className: "mxc-tab-title", children: title })
|
|
1032
2029
|
] });
|
|
1033
2030
|
}
|
|
1034
|
-
function
|
|
1035
|
-
itemId,
|
|
1036
|
-
paneId,
|
|
1037
|
-
index,
|
|
1038
|
-
active
|
|
1039
|
-
}) {
|
|
2031
|
+
function ItemTab({ itemId, paneId, index, active }) {
|
|
1040
2032
|
const canvas = useCanvas();
|
|
1041
2033
|
const item = useCanvasState((s) => s.items[itemId]);
|
|
1042
2034
|
const kind = useCanvasKind(item?.kind ?? "");
|
|
1043
|
-
const ref =
|
|
1044
|
-
|
|
1045
|
-
|
|
2035
|
+
const ref = useRef4(null);
|
|
2036
|
+
const title = item ? itemTitle(item, kind) : "";
|
|
2037
|
+
const drag = useTabDrag(itemId, title);
|
|
2038
|
+
useEffect4(() => {
|
|
2039
|
+
if (active) ref.current?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
|
|
1046
2040
|
}, [active]);
|
|
1047
2041
|
if (!item) return null;
|
|
1048
|
-
const Icon = kind?.icon;
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
"div",
|
|
2042
|
+
const Icon = kind?.icon ?? AlertIcon;
|
|
2043
|
+
return /* @__PURE__ */ jsx5(
|
|
2044
|
+
CanvasTab,
|
|
1052
2045
|
{
|
|
1053
2046
|
ref,
|
|
1054
|
-
className: "mxc-tab",
|
|
1055
|
-
role: "tab",
|
|
1056
|
-
"aria-selected": active,
|
|
1057
|
-
"data-active": active ? "" : void 0,
|
|
1058
|
-
tabIndex: active ? 0 : -1,
|
|
1059
2047
|
title,
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
2048
|
+
icon: /* @__PURE__ */ jsx5(Icon, {}),
|
|
2049
|
+
active,
|
|
2050
|
+
onSelect: () => canvas.activate(itemId),
|
|
2051
|
+
onClose: () => canvas.close(itemId),
|
|
2052
|
+
...drag,
|
|
1065
2053
|
onDragOver: (event) => {
|
|
1066
2054
|
if (event.dataTransfer.types.includes(DRAG_MIME)) event.preventDefault();
|
|
1067
2055
|
},
|
|
@@ -1069,83 +2057,89 @@ function Tab({
|
|
|
1069
2057
|
const dragged = event.dataTransfer.getData(DRAG_MIME);
|
|
1070
2058
|
if (!dragged) return;
|
|
1071
2059
|
event.preventDefault();
|
|
1072
|
-
event.stopPropagation();
|
|
1073
2060
|
if (dragged !== itemId) canvas.moveItem(dragged, paneId, index);
|
|
1074
2061
|
},
|
|
1075
|
-
onClick: () => canvas.activate(itemId),
|
|
1076
2062
|
onAuxClick: (event) => {
|
|
1077
2063
|
if (event.button === 1) canvas.close(itemId);
|
|
1078
|
-
}
|
|
1079
|
-
onKeyDown: (event) => {
|
|
1080
|
-
if (event.key === "Enter" || event.key === " ") canvas.activate(itemId);
|
|
1081
|
-
},
|
|
1082
|
-
children: [
|
|
1083
|
-
Icon ? /* @__PURE__ */ jsx3(Icon, { className: "mxc-tab-icon" }) : /* @__PURE__ */ jsx3(AlertIcon, { className: "mxc-tab-icon" }),
|
|
1084
|
-
/* @__PURE__ */ jsx3("span", { className: "mxc-tab-title", children: title }),
|
|
1085
|
-
/* @__PURE__ */ jsx3(
|
|
1086
|
-
"button",
|
|
1087
|
-
{
|
|
1088
|
-
type: "button",
|
|
1089
|
-
className: "mxc-tab-close",
|
|
1090
|
-
"aria-label": `Close ${title}`,
|
|
1091
|
-
onClick: (event) => {
|
|
1092
|
-
event.stopPropagation();
|
|
1093
|
-
canvas.close(itemId);
|
|
1094
|
-
},
|
|
1095
|
-
children: /* @__PURE__ */ jsx3(CloseIcon, { width: 12, height: 12 })
|
|
1096
|
-
}
|
|
1097
|
-
)
|
|
1098
|
-
]
|
|
2064
|
+
}
|
|
1099
2065
|
}
|
|
1100
2066
|
);
|
|
1101
2067
|
}
|
|
1102
2068
|
function PaneMenu({
|
|
1103
2069
|
paneId,
|
|
2070
|
+
windowId,
|
|
1104
2071
|
activeItem,
|
|
1105
2072
|
kind,
|
|
1106
2073
|
kindProps,
|
|
1107
|
-
|
|
2074
|
+
tabCount,
|
|
2075
|
+
role
|
|
1108
2076
|
}) {
|
|
1109
2077
|
const canvas = useCanvas();
|
|
1110
|
-
const ports =
|
|
1111
|
-
const moveId =
|
|
2078
|
+
const { ports, layer } = useCanvasInternals();
|
|
2079
|
+
const moveId = tabCount > 1 ? activeItem?.id : void 0;
|
|
2080
|
+
const blocking = useRef4(null);
|
|
2081
|
+
useEffect4(() => () => blocking.current?.(), []);
|
|
1112
2082
|
const kindItems = kind?.menuItems && kindProps ? safeMenu(() => kind.menuItems?.(kindProps)) : [];
|
|
1113
2083
|
const hostItems = activeItem && ports.itemActions ? safeMenu(() => ports.itemActions?.(activeItem, kind)) : [];
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
2084
|
+
const sections = [
|
|
2085
|
+
[...kindItems, ...hostItems].map((entry) => ({ ...entry, id: `item:${entry.id}` })),
|
|
2086
|
+
[
|
|
2087
|
+
{ id: "split-right", label: "Split right", icon: /* @__PURE__ */ jsx5(SplitRightIcon, {}), onSelect: () => canvas.splitPane(paneId, "horizontal", moveId) },
|
|
2088
|
+
{ id: "split-down", label: "Split down", icon: /* @__PURE__ */ jsx5(SplitDownIcon, {}), onSelect: () => canvas.splitPane(paneId, "vertical", moveId) },
|
|
2089
|
+
...activeItem && role === "pop-out" ? [{ id: "dock-back", label: "Dock back", icon: /* @__PURE__ */ jsx5(DockIcon, {}), onSelect: () => void canvas.dockBack(activeItem.id) }] : activeItem && ports.popOut ? [{ id: "pop-out", label: "Pop out", icon: /* @__PURE__ */ jsx5(PopOutIcon, {}), onSelect: () => void canvas.popOut(activeItem.id) }] : []
|
|
2090
|
+
],
|
|
2091
|
+
activeItem ? [
|
|
2092
|
+
{ id: "close-tab", label: "Close tab", onSelect: () => canvas.close(activeItem.id) },
|
|
2093
|
+
...tabCount > 1 ? [{ id: "close-others", label: "Close other tabs", onSelect: () => canvas.closeOthers(activeItem.id) }] : []
|
|
2094
|
+
] : []
|
|
2095
|
+
].filter((section) => section.length > 0);
|
|
2096
|
+
const showMenu = ports.showMenu;
|
|
2097
|
+
if (showMenu) {
|
|
2098
|
+
const entries = [];
|
|
2099
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
2100
|
+
sections.forEach((section, i) => {
|
|
2101
|
+
if (i > 0) entries.push({ type: "separator" });
|
|
2102
|
+
for (const action of section) {
|
|
2103
|
+
entries.push({ type: "item", id: action.id, label: action.label, ...action.destructive ? { destructive: true } : {} });
|
|
2104
|
+
handlers.set(action.id, action.onSelect);
|
|
2105
|
+
}
|
|
2106
|
+
});
|
|
2107
|
+
return /* @__PURE__ */ jsx5(
|
|
2108
|
+
CanvasMenuTrigger,
|
|
2109
|
+
{
|
|
2110
|
+
onClick: (event) => {
|
|
2111
|
+
const anchor = toCanvasRect(event.currentTarget.getBoundingClientRect());
|
|
2112
|
+
void showMenu(entries, anchor).then(
|
|
2113
|
+
(chosen) => {
|
|
2114
|
+
if (chosen) handlers.get(chosen)?.();
|
|
2115
|
+
},
|
|
2116
|
+
(error) => console.error("[@ai-matrx/canvas] the host menu failed", error)
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
);
|
|
2121
|
+
}
|
|
2122
|
+
return /* @__PURE__ */ jsxs4(
|
|
2123
|
+
Menu.Root,
|
|
2124
|
+
{
|
|
2125
|
+
onOpenChange: (open) => {
|
|
2126
|
+
blocking.current?.();
|
|
2127
|
+
blocking.current = open ? layer.beginBlocking() : null;
|
|
2128
|
+
},
|
|
2129
|
+
children: [
|
|
2130
|
+
/* @__PURE__ */ jsx5(Menu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx5(CanvasMenuTrigger, {}) }),
|
|
2131
|
+
/* @__PURE__ */ jsx5(Menu.Portal, { children: /* @__PURE__ */ jsx5(Menu.Content, { align: "end", sideOffset: 4, className: "mxc-menu", children: sections.map((section, i) => /* @__PURE__ */ jsx5(MenuSection, { first: i === 0, actions: section }, i)) }) })
|
|
2132
|
+
]
|
|
2133
|
+
}
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
function MenuSection({ first, actions }) {
|
|
2137
|
+
return /* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
2138
|
+
first ? null : /* @__PURE__ */ jsx5(Menu.Separator, { className: "mxc-menu-separator" }),
|
|
2139
|
+
actions.map((action) => /* @__PURE__ */ jsxs4(Menu.Item, { onSelect: action.onSelect, className: "mxc-menu-item", "data-destructive": action.destructive ? "" : void 0, children: [
|
|
2140
|
+
action.icon,
|
|
2141
|
+
action.label
|
|
2142
|
+
] }, action.id))
|
|
1149
2143
|
] });
|
|
1150
2144
|
}
|
|
1151
2145
|
function safeMenu(build) {
|
|
@@ -1159,25 +2153,80 @@ function safeMenu(build) {
|
|
|
1159
2153
|
function ItemBody({
|
|
1160
2154
|
item,
|
|
1161
2155
|
paneId,
|
|
2156
|
+
windowId,
|
|
1162
2157
|
isActive,
|
|
1163
|
-
isFocused
|
|
2158
|
+
isFocused,
|
|
2159
|
+
hasNativePort
|
|
1164
2160
|
}) {
|
|
1165
2161
|
const canvas = useCanvas();
|
|
1166
2162
|
const kind = useCanvasKind(item.kind);
|
|
1167
2163
|
if (!isActive && !kind?.keepAlive) return null;
|
|
1168
|
-
|
|
2164
|
+
const props = { item, data: item.data, paneId, isFocused, canvas, windowId };
|
|
2165
|
+
let unavailable = null;
|
|
2166
|
+
try {
|
|
2167
|
+
unavailable = kind?.unavailable?.(item.data, item) ?? null;
|
|
2168
|
+
} catch (error) {
|
|
2169
|
+
console.error(`[@ai-matrx/canvas] "${item.kind}" unavailable() threw`, error);
|
|
2170
|
+
}
|
|
2171
|
+
let body;
|
|
2172
|
+
if (!kind) {
|
|
2173
|
+
body = /* @__PURE__ */ jsx5(ItemProblem, { message: "This item can no longer be shown.", onClose: () => canvas.close(item.id) });
|
|
2174
|
+
} else if (unavailable) {
|
|
2175
|
+
body = /* @__PURE__ */ jsx5(CanvasPanelEmpty, { children: unavailable });
|
|
2176
|
+
} else if (kind.body === "native") {
|
|
2177
|
+
body = hasNativePort ? /* @__PURE__ */ jsx5(NativeBody, { itemId: item.id, windowId, children: kindComponent(kind) ? /* @__PURE__ */ jsx5(Rendered, { kind, item, props }) : null }) : /* @__PURE__ */ jsx5(CanvasPanelEmpty, { children: "Opens in the desktop app" });
|
|
2178
|
+
} else {
|
|
2179
|
+
body = /* @__PURE__ */ jsx5(Rendered, { kind, item, props });
|
|
2180
|
+
}
|
|
2181
|
+
return /* @__PURE__ */ jsx5("div", { className: "mxc-item", hidden: !isActive, "data-kind": item.kind, children: body });
|
|
2182
|
+
}
|
|
2183
|
+
function Rendered({ kind, item, props }) {
|
|
2184
|
+
const canvas = useCanvas();
|
|
2185
|
+
return /* @__PURE__ */ jsx5(ItemBoundary, { item, onClose: () => canvas.close(item.id), onRetry: () => resetKindComponent(kind), children: /* @__PURE__ */ jsx5(Suspense, { fallback: /* @__PURE__ */ jsx5("div", { className: "mxc-loading", "aria-busy": "true" }), children: /* @__PURE__ */ jsx5(KindRender, { kind, props }) }) });
|
|
1169
2186
|
}
|
|
1170
2187
|
function KindRender({ kind, props }) {
|
|
1171
2188
|
const Render = kindComponent(kind);
|
|
1172
|
-
|
|
2189
|
+
if (!Render) throw new Error(`[@ai-matrx/canvas] kind "${kind.id}" has nothing to render.`);
|
|
2190
|
+
return /* @__PURE__ */ jsx5(Render, { ...props });
|
|
2191
|
+
}
|
|
2192
|
+
function NativeBody({ itemId, windowId, children }) {
|
|
2193
|
+
const { layer } = useCanvasInternals();
|
|
2194
|
+
const ref = useRef4(null);
|
|
2195
|
+
const measure = useRef4(() => void 0);
|
|
2196
|
+
measure.current = () => {
|
|
2197
|
+
const el = ref.current;
|
|
2198
|
+
const box = el?.isConnected ? el.getBoundingClientRect() : null;
|
|
2199
|
+
if (!box || box.width <= 0 || box.height <= 0) {
|
|
2200
|
+
layer.native.report(itemId, null, { visible: false, windowId });
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
const rect = { ...toCanvasRect(box), devicePixelRatio: window.devicePixelRatio || 1 };
|
|
2204
|
+
layer.native.report(itemId, rect, { visible: !layer.isBlocked(), windowId });
|
|
2205
|
+
};
|
|
2206
|
+
useIsoLayoutEffect(() => measure.current());
|
|
2207
|
+
useIsoLayoutEffect(() => {
|
|
2208
|
+
const el = ref.current;
|
|
2209
|
+
const run = () => measure.current();
|
|
2210
|
+
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(run);
|
|
2211
|
+
if (el) observer?.observe(el);
|
|
2212
|
+
const unregister = layer.native.register(run);
|
|
2213
|
+
window.addEventListener("resize", run);
|
|
2214
|
+
return () => {
|
|
2215
|
+
observer?.disconnect();
|
|
2216
|
+
unregister();
|
|
2217
|
+
window.removeEventListener("resize", run);
|
|
2218
|
+
layer.native.report(itemId, null, { visible: false, windowId });
|
|
2219
|
+
};
|
|
2220
|
+
}, [itemId, windowId, layer]);
|
|
2221
|
+
return /* @__PURE__ */ jsx5("div", { ref, className: "mxc-native-body", "data-native-body": itemId, children });
|
|
1173
2222
|
}
|
|
1174
2223
|
function ItemProblem({ message, onClose, onRetry }) {
|
|
1175
|
-
return /* @__PURE__ */
|
|
1176
|
-
/* @__PURE__ */
|
|
1177
|
-
/* @__PURE__ */
|
|
1178
|
-
/* @__PURE__ */
|
|
1179
|
-
onRetry ? /* @__PURE__ */
|
|
1180
|
-
/* @__PURE__ */
|
|
2224
|
+
return /* @__PURE__ */ jsxs4("div", { className: "mxc-problem", role: "alert", children: [
|
|
2225
|
+
/* @__PURE__ */ jsx5(AlertIcon, {}),
|
|
2226
|
+
/* @__PURE__ */ jsx5("p", { children: message }),
|
|
2227
|
+
/* @__PURE__ */ jsxs4("div", { className: "mxc-problem-actions", children: [
|
|
2228
|
+
onRetry ? /* @__PURE__ */ jsx5("button", { type: "button", className: "mxc-text-button", onClick: onRetry, children: "Try again" }) : null,
|
|
2229
|
+
/* @__PURE__ */ jsx5("button", { type: "button", className: "mxc-text-button", onClick: onClose, children: "Close tab" })
|
|
1181
2230
|
] })
|
|
1182
2231
|
] });
|
|
1183
2232
|
}
|
|
@@ -1191,7 +2240,7 @@ var ItemBoundary = class extends Component {
|
|
|
1191
2240
|
}
|
|
1192
2241
|
render() {
|
|
1193
2242
|
if (this.state.error) {
|
|
1194
|
-
return /* @__PURE__ */
|
|
2243
|
+
return /* @__PURE__ */ jsx5(
|
|
1195
2244
|
ItemProblem,
|
|
1196
2245
|
{
|
|
1197
2246
|
message: "This item failed to load.",
|
|
@@ -1209,12 +2258,12 @@ var ItemBoundary = class extends Component {
|
|
|
1209
2258
|
function Launcher({ paneId }) {
|
|
1210
2259
|
const canvas = useCanvas();
|
|
1211
2260
|
const kinds = useCanvasKinds().filter((kind) => kind.launcher);
|
|
1212
|
-
if (kinds.length === 0) return /* @__PURE__ */
|
|
1213
|
-
return /* @__PURE__ */
|
|
2261
|
+
if (kinds.length === 0) return /* @__PURE__ */ jsx5(CanvasPanelEmpty, { children: "Nothing open" });
|
|
2262
|
+
return /* @__PURE__ */ jsx5("div", { className: "mxc-launcher", children: kinds.map((kind) => {
|
|
1214
2263
|
const Icon = kind.icon;
|
|
1215
2264
|
const launcher = kind.launcher;
|
|
1216
2265
|
if (!launcher) return null;
|
|
1217
|
-
return /* @__PURE__ */
|
|
2266
|
+
return /* @__PURE__ */ jsxs4(
|
|
1218
2267
|
"button",
|
|
1219
2268
|
{
|
|
1220
2269
|
type: "button",
|
|
@@ -1227,8 +2276,8 @@ function Launcher({ paneId }) {
|
|
|
1227
2276
|
target: { paneId }
|
|
1228
2277
|
}),
|
|
1229
2278
|
children: [
|
|
1230
|
-
/* @__PURE__ */
|
|
1231
|
-
/* @__PURE__ */
|
|
2279
|
+
/* @__PURE__ */ jsx5(Icon, { className: "mxc-launcher-icon" }),
|
|
2280
|
+
/* @__PURE__ */ jsx5("span", { children: launcher.title ?? kind.label })
|
|
1232
2281
|
]
|
|
1233
2282
|
},
|
|
1234
2283
|
kind.id
|
|
@@ -1237,69 +2286,72 @@ function Launcher({ paneId }) {
|
|
|
1237
2286
|
}
|
|
1238
2287
|
|
|
1239
2288
|
// src/react/CanvasColumn.tsx
|
|
1240
|
-
import { Fragment as Fragment2, jsx as
|
|
1241
|
-
var
|
|
2289
|
+
import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2290
|
+
var useIsoLayoutEffect2 = typeof window === "undefined" ? useEffect5 : useLayoutEffect3;
|
|
1242
2291
|
function subscribeViewport(listener) {
|
|
1243
2292
|
window.addEventListener("resize", listener);
|
|
1244
2293
|
return () => window.removeEventListener("resize", listener);
|
|
1245
2294
|
}
|
|
1246
2295
|
function useViewportWidth() {
|
|
1247
|
-
return
|
|
2296
|
+
return useSyncExternalStore3(subscribeViewport, () => window.innerWidth, () => 0);
|
|
1248
2297
|
}
|
|
1249
|
-
function
|
|
1250
|
-
|
|
1251
|
-
|
|
2298
|
+
function useRegionWidth() {
|
|
2299
|
+
const viewport = useViewportWidth();
|
|
2300
|
+
const { regionWidth } = useCanvasInternals();
|
|
2301
|
+
return regionWidth ?? viewport;
|
|
1252
2302
|
}
|
|
1253
2303
|
function useCanvasColumnWidth() {
|
|
1254
|
-
const
|
|
1255
|
-
const
|
|
1256
|
-
const
|
|
1257
|
-
const
|
|
2304
|
+
const { controller, windowId } = useCanvasInternals();
|
|
2305
|
+
const isOpen = useCanvasState((s) => selectCanvasIsOpen(s, windowId));
|
|
2306
|
+
const isFullscreen = useCanvasState((s) => selectCanvasIsFullscreen(s, windowId));
|
|
2307
|
+
const width = useCanvasState((s) => selectCanvasWidth(s, windowId));
|
|
2308
|
+
const region = useRegionWidth();
|
|
1258
2309
|
if (!isOpen) return 0;
|
|
1259
2310
|
if (isFullscreen) return null;
|
|
1260
|
-
return
|
|
1261
|
-
}
|
|
1262
|
-
function CanvasColumn({ className, style, onLiveWidth }) {
|
|
1263
|
-
const
|
|
1264
|
-
const
|
|
1265
|
-
const
|
|
1266
|
-
const
|
|
1267
|
-
const
|
|
1268
|
-
const
|
|
2311
|
+
return fitCanvasWidth(width, region, controller.rules);
|
|
2312
|
+
}
|
|
2313
|
+
function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
|
|
2314
|
+
const { controller, windowId } = useCanvasInternals();
|
|
2315
|
+
const isOpen = useCanvasState((s) => selectCanvasIsOpen(s, windowId));
|
|
2316
|
+
const isFullscreen = useCanvasState((s) => selectCanvasIsFullscreen(s, windowId));
|
|
2317
|
+
const width = useCanvasState((s) => selectCanvasWidth(s, windowId));
|
|
2318
|
+
const layout = useCanvasState((s) => selectCanvasLayout(s, windowId));
|
|
2319
|
+
const region = useRegionWidth();
|
|
2320
|
+
const [dragWidth, setDragWidth] = useState4(null);
|
|
1269
2321
|
useRegisterCanvasPresence();
|
|
1270
|
-
if (!isOpen) return null;
|
|
1271
|
-
const fitted =
|
|
2322
|
+
if (!isOpen || !layout) return null;
|
|
2323
|
+
const fitted = fitCanvasWidth(width, region, controller.rules);
|
|
1272
2324
|
const shown = dragWidth ?? fitted;
|
|
1273
|
-
return /* @__PURE__ */
|
|
2325
|
+
return /* @__PURE__ */ jsxs5(
|
|
1274
2326
|
"aside",
|
|
1275
2327
|
{
|
|
1276
2328
|
className: ["mxc-column", className].filter(Boolean).join(" "),
|
|
1277
2329
|
"data-fullscreen": isFullscreen ? "" : void 0,
|
|
2330
|
+
"data-placement": placement,
|
|
2331
|
+
"data-mxc-window": windowId,
|
|
1278
2332
|
"aria-label": "Canvas",
|
|
1279
2333
|
style: { ...style, ["--mxc-width"]: `${shown}px` },
|
|
1280
2334
|
children: [
|
|
1281
|
-
!isFullscreen ? /* @__PURE__ */
|
|
2335
|
+
placement === "docked" && !isFullscreen ? /* @__PURE__ */ jsx6(
|
|
1282
2336
|
WidthHandle,
|
|
1283
2337
|
{
|
|
1284
2338
|
width: fitted,
|
|
2339
|
+
region,
|
|
1285
2340
|
onDrag: (next) => {
|
|
1286
2341
|
setDragWidth(next);
|
|
1287
2342
|
onLiveWidth?.(next);
|
|
1288
2343
|
}
|
|
1289
2344
|
}
|
|
1290
2345
|
) : null,
|
|
1291
|
-
/* @__PURE__ */
|
|
2346
|
+
/* @__PURE__ */ jsx6("div", { className: "mxc-layout", children: /* @__PURE__ */ jsx6(LayoutNodeView, { node: layout }) })
|
|
1292
2347
|
]
|
|
1293
2348
|
}
|
|
1294
2349
|
);
|
|
1295
2350
|
}
|
|
1296
|
-
function WidthHandle({ width, onDrag }) {
|
|
2351
|
+
function WidthHandle({ width, region, onDrag }) {
|
|
1297
2352
|
const canvas = useCanvas();
|
|
1298
|
-
const start =
|
|
1299
|
-
const clamp = (next) =>
|
|
1300
|
-
const max = Math.max(CANVAS_MIN_WIDTH, window.innerWidth - MIN_MAIN_WIDTH);
|
|
1301
|
-
return Math.min(max, Math.max(CANVAS_MIN_WIDTH, next));
|
|
1302
|
-
};
|
|
2353
|
+
const start = useRef5(null);
|
|
2354
|
+
const clamp = (next) => clampDraggedCanvasWidth(next, region, canvas.rules);
|
|
1303
2355
|
const onPointerDown = (event) => {
|
|
1304
2356
|
event.preventDefault();
|
|
1305
2357
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
@@ -1318,7 +2370,7 @@ function WidthHandle({ width, onDrag }) {
|
|
|
1318
2370
|
onDrag(null);
|
|
1319
2371
|
canvas.setWidth(next);
|
|
1320
2372
|
};
|
|
1321
|
-
return /* @__PURE__ */
|
|
2373
|
+
return /* @__PURE__ */ jsx6(
|
|
1322
2374
|
"div",
|
|
1323
2375
|
{
|
|
1324
2376
|
className: "mxc-width-handle",
|
|
@@ -1331,7 +2383,7 @@ function WidthHandle({ width, onDrag }) {
|
|
|
1331
2383
|
onPointerMove,
|
|
1332
2384
|
onPointerUp: finish,
|
|
1333
2385
|
onPointerCancel: finish,
|
|
1334
|
-
onDoubleClick: () => canvas.setWidth(Math.round(
|
|
2386
|
+
onDoubleClick: () => canvas.setWidth(clamp(Math.round(region / 2))),
|
|
1335
2387
|
onKeyDown: (event) => {
|
|
1336
2388
|
if (event.key === "ArrowLeft") canvas.setWidth(clamp(width + 32));
|
|
1337
2389
|
if (event.key === "ArrowRight") canvas.setWidth(clamp(width - 32));
|
|
@@ -1340,15 +2392,31 @@ function WidthHandle({ width, onDrag }) {
|
|
|
1340
2392
|
);
|
|
1341
2393
|
}
|
|
1342
2394
|
function LayoutNodeView({ node }) {
|
|
1343
|
-
if (node.type === "pane") return /* @__PURE__ */
|
|
1344
|
-
return /* @__PURE__ */
|
|
2395
|
+
if (node.type === "pane") return /* @__PURE__ */ jsx6(CanvasPaneView, { paneId: node.paneId });
|
|
2396
|
+
return /* @__PURE__ */ jsx6(SplitView, { node });
|
|
2397
|
+
}
|
|
2398
|
+
function useMeasuredWidth(ref, enabled) {
|
|
2399
|
+
const [width, setWidth] = useState4(0);
|
|
2400
|
+
useIsoLayoutEffect2(() => {
|
|
2401
|
+
const el = ref.current;
|
|
2402
|
+
if (!enabled || !el) return;
|
|
2403
|
+
setWidth(el.getBoundingClientRect().width);
|
|
2404
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
2405
|
+
const observer = new ResizeObserver(() => setWidth(el.getBoundingClientRect().width));
|
|
2406
|
+
observer.observe(el);
|
|
2407
|
+
return () => observer.disconnect();
|
|
2408
|
+
}, [enabled]);
|
|
2409
|
+
return width;
|
|
1345
2410
|
}
|
|
1346
2411
|
function SplitView({ node }) {
|
|
1347
2412
|
const canvas = useCanvas();
|
|
1348
|
-
const ref =
|
|
1349
|
-
const [liveSizes, setLiveSizes] =
|
|
2413
|
+
const ref = useRef5(null);
|
|
2414
|
+
const [liveSizes, setLiveSizes] = useState4(null);
|
|
1350
2415
|
const sizes = liveSizes ?? node.sizes;
|
|
1351
|
-
const
|
|
2416
|
+
const measuredWidth = useMeasuredWidth(ref, node.orientation === "horizontal" && canvas.rules.paneMinWidth > 0);
|
|
2417
|
+
const stacked = node.orientation === "horizontal" && shouldStackSplit(measuredWidth, node.children.length, canvas.rules);
|
|
2418
|
+
const orientation = stacked ? "vertical" : node.orientation;
|
|
2419
|
+
const horizontal = orientation === "horizontal";
|
|
1352
2420
|
const beginDrag = (index) => (event) => {
|
|
1353
2421
|
const el = ref.current;
|
|
1354
2422
|
if (!el) return;
|
|
@@ -1360,10 +2428,10 @@ function SplitView({ node }) {
|
|
|
1360
2428
|
const base = [...node.sizes];
|
|
1361
2429
|
const pair = (base[index] ?? 0) + (base[index + 1] ?? 0);
|
|
1362
2430
|
const target = event.currentTarget;
|
|
1363
|
-
document.documentElement.dataset.mxcResizing = "";
|
|
2431
|
+
document.documentElement.dataset.mxcResizing = horizontal ? "col" : "row";
|
|
1364
2432
|
const compute = (pos) => {
|
|
1365
2433
|
const delta = (pos - origin) / total;
|
|
1366
|
-
const min =
|
|
2434
|
+
const min = CANVAS_MIN_SPLIT_FRACTION;
|
|
1367
2435
|
const first = Math.min(pair - min, Math.max(min, (base[index] ?? 0) + delta));
|
|
1368
2436
|
const next = [...base];
|
|
1369
2437
|
next[index] = first;
|
|
@@ -1383,11 +2451,11 @@ function SplitView({ node }) {
|
|
|
1383
2451
|
target.addEventListener("pointerup", up);
|
|
1384
2452
|
target.addEventListener("pointercancel", up);
|
|
1385
2453
|
};
|
|
1386
|
-
return /* @__PURE__ */
|
|
2454
|
+
return /* @__PURE__ */ jsx6("div", { ref, className: "mxc-split", "data-orientation": orientation, "data-stacked": stacked ? "" : void 0, children: node.children.map((child, i) => /* @__PURE__ */ jsx6(
|
|
1387
2455
|
SplitChild,
|
|
1388
2456
|
{
|
|
1389
2457
|
size: sizes[i] ?? 1 / node.children.length,
|
|
1390
|
-
handle: i < node.children.length - 1 ? /* @__PURE__ */
|
|
2458
|
+
handle: i < node.children.length - 1 ? /* @__PURE__ */ jsx6(
|
|
1391
2459
|
"div",
|
|
1392
2460
|
{
|
|
1393
2461
|
className: "mxc-split-handle",
|
|
@@ -1397,48 +2465,60 @@ function SplitView({ node }) {
|
|
|
1397
2465
|
onDoubleClick: () => canvas.resizeSplit(node.id, node.children.map(() => 1 / node.children.length))
|
|
1398
2466
|
}
|
|
1399
2467
|
) : null,
|
|
1400
|
-
children: /* @__PURE__ */
|
|
2468
|
+
children: /* @__PURE__ */ jsx6(LayoutNodeView, { node: child })
|
|
1401
2469
|
},
|
|
1402
2470
|
child.type === "pane" ? child.paneId : child.id
|
|
1403
2471
|
)) });
|
|
1404
2472
|
}
|
|
1405
2473
|
function SplitChild({ size, handle, children }) {
|
|
1406
|
-
return /* @__PURE__ */
|
|
1407
|
-
/* @__PURE__ */
|
|
2474
|
+
return /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
2475
|
+
/* @__PURE__ */ jsx6("div", { className: "mxc-split-child", style: { flexGrow: size, flexBasis: 0 }, children }),
|
|
1408
2476
|
handle
|
|
1409
2477
|
] });
|
|
1410
2478
|
}
|
|
2479
|
+
var isMac2 = () => typeof navigator !== "undefined" && /mac|iphone|ipad/i.test(navigator.platform ?? "");
|
|
1411
2480
|
function CanvasToggle({ className }) {
|
|
1412
|
-
const
|
|
1413
|
-
const isOpen = useCanvasState(selectCanvasIsOpen);
|
|
1414
|
-
const count = useCanvasState(
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
2481
|
+
const { controller, windowId } = useCanvasInternals();
|
|
2482
|
+
const isOpen = useCanvasState((s) => selectCanvasIsOpen(s, windowId));
|
|
2483
|
+
const count = useCanvasState((s) => selectCanvasWindowItemCount(s, windowId));
|
|
2484
|
+
const hint = canvasShortcutHint("canvas.toggle", isMac2());
|
|
2485
|
+
return /* @__PURE__ */ jsxs5("span", { className: ["mxc-toggle", className].filter(Boolean).join(" "), "data-open": isOpen ? "" : void 0, children: [
|
|
2486
|
+
/* @__PURE__ */ jsx6(
|
|
2487
|
+
TapTargetButtonTransparent,
|
|
1418
2488
|
{
|
|
1419
2489
|
ariaLabel: isOpen ? "Hide canvas" : "Show canvas",
|
|
1420
|
-
tooltip: isOpen ? "Hide canvas
|
|
1421
|
-
icon: /* @__PURE__ */
|
|
1422
|
-
onClick: () =>
|
|
2490
|
+
tooltip: `${isOpen ? "Hide canvas" : "Show canvas"}${hint ? ` (${hint})` : ""}`,
|
|
2491
|
+
icon: /* @__PURE__ */ jsx6(PanelRightIcon, {}),
|
|
2492
|
+
onClick: () => controller.toggle()
|
|
1423
2493
|
}
|
|
1424
2494
|
),
|
|
1425
|
-
count > 0 && !isOpen ? /* @__PURE__ */
|
|
2495
|
+
count > 0 && !isOpen ? /* @__PURE__ */ jsx6("span", { className: "mxc-toggle-badge", children: count }) : null
|
|
1426
2496
|
] });
|
|
1427
2497
|
}
|
|
1428
2498
|
function CanvasFrame({ children, className }) {
|
|
1429
|
-
const
|
|
1430
|
-
const
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
/* @__PURE__ */
|
|
2499
|
+
const { windowId } = useCanvasInternals();
|
|
2500
|
+
const isFullscreen = useCanvasState((s) => selectCanvasIsFullscreen(s, windowId));
|
|
2501
|
+
const isOpen = useCanvasState((s) => selectCanvasIsOpen(s, windowId));
|
|
2502
|
+
return /* @__PURE__ */ jsxs5("div", { className: ["mxc-frame", className].filter(Boolean).join(" "), "data-canvas-open": isOpen ? "" : void 0, children: [
|
|
2503
|
+
/* @__PURE__ */ jsx6("div", { className: "mxc-frame-main", hidden: isOpen && isFullscreen, children }),
|
|
2504
|
+
/* @__PURE__ */ jsx6(CanvasColumn, {})
|
|
1434
2505
|
] });
|
|
1435
2506
|
}
|
|
1436
2507
|
export {
|
|
2508
|
+
BrowserBar,
|
|
2509
|
+
BrowserTabs,
|
|
1437
2510
|
CanvasColumn,
|
|
1438
2511
|
CanvasFrame,
|
|
2512
|
+
CanvasHeaderButton,
|
|
2513
|
+
CanvasMenuTrigger,
|
|
1439
2514
|
CanvasPaneView,
|
|
2515
|
+
CanvasPanel,
|
|
2516
|
+
CanvasPanelEmpty,
|
|
1440
2517
|
CanvasProvider,
|
|
2518
|
+
CanvasTab,
|
|
1441
2519
|
CanvasToggle,
|
|
2520
|
+
CanvasWindowScope,
|
|
2521
|
+
canvasSessionKey,
|
|
1442
2522
|
defineCanvasKind,
|
|
1443
2523
|
getCanvasKind,
|
|
1444
2524
|
itemTitle,
|
|
@@ -1451,7 +2531,9 @@ export {
|
|
|
1451
2531
|
useCanvasIsPresented,
|
|
1452
2532
|
useCanvasKind,
|
|
1453
2533
|
useCanvasKinds,
|
|
2534
|
+
useCanvasNativeBodies,
|
|
1454
2535
|
useCanvasState,
|
|
2536
|
+
useCanvasWindowId,
|
|
1455
2537
|
useOptionalCanvas,
|
|
1456
2538
|
useOptionalCanvasState
|
|
1457
2539
|
};
|