@ai-matrx/canvas 0.0.0-bootstrap.0 → 0.1.1
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 +20 -0
- package/LICENSE +21 -0
- package/README.md +20 -2
- package/dist/controller-oY84BN2c.d.cts +345 -0
- package/dist/controller-oY84BN2c.d.ts +345 -0
- package/dist/index.cjs +758 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +39 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +735 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +1477 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +147 -0
- package/dist/react.d.ts +147 -0
- package/dist/react.js +1458 -0
- package/dist/react.js.map +1 -0
- package/dist/styles.css +451 -0
- package/dist/tokens.css +49 -0
- package/package.json +85 -6
package/dist/react.js
ADDED
|
@@ -0,0 +1,1458 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
// src/react/provider.tsx
|
|
5
|
+
import {
|
|
6
|
+
createContext,
|
|
7
|
+
useContext,
|
|
8
|
+
useEffect,
|
|
9
|
+
useState,
|
|
10
|
+
useSyncExternalStore
|
|
11
|
+
} from "react";
|
|
12
|
+
|
|
13
|
+
// src/core/types.ts
|
|
14
|
+
var CANVAS_MIN_WIDTH = 360;
|
|
15
|
+
var CANVAS_DEFAULT_WIDTH = 640;
|
|
16
|
+
var CANVAS_MIN_SPLIT_FRACTION = 0.12;
|
|
17
|
+
|
|
18
|
+
// src/core/ids.ts
|
|
19
|
+
var SEP = "::";
|
|
20
|
+
function canvasItemId(kind, key) {
|
|
21
|
+
return `${kind}${SEP}${key}`;
|
|
22
|
+
}
|
|
23
|
+
function paneIdFromSeq(seq) {
|
|
24
|
+
return `pane-${seq}`;
|
|
25
|
+
}
|
|
26
|
+
function splitIdFromSeq(seq) {
|
|
27
|
+
return `split-${seq}`;
|
|
28
|
+
}
|
|
29
|
+
function findNonJson(value, path = "data") {
|
|
30
|
+
if (value === null) return null;
|
|
31
|
+
switch (typeof value) {
|
|
32
|
+
case "string":
|
|
33
|
+
case "boolean":
|
|
34
|
+
return null;
|
|
35
|
+
case "number":
|
|
36
|
+
return Number.isFinite(value) ? null : path;
|
|
37
|
+
case "object": {
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
for (let i = 0; i < value.length; i++) {
|
|
40
|
+
const bad = findNonJson(value[i], `${path}[${i}]`);
|
|
41
|
+
if (bad) return bad;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const proto = Object.getPrototypeOf(value);
|
|
46
|
+
if (proto !== Object.prototype && proto !== null) return path;
|
|
47
|
+
for (const [k, v] of Object.entries(value)) {
|
|
48
|
+
if (v === void 0) continue;
|
|
49
|
+
const bad = findNonJson(v, `${path}.${k}`);
|
|
50
|
+
if (bad) return bad;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
default:
|
|
55
|
+
return path;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/core/layout.ts
|
|
60
|
+
function listPaneIds(node) {
|
|
61
|
+
if (node.type === "pane") return [node.paneId];
|
|
62
|
+
return node.children.flatMap(listPaneIds);
|
|
63
|
+
}
|
|
64
|
+
function evenSizes(count) {
|
|
65
|
+
return Array.from({ length: count }, () => 1 / count);
|
|
66
|
+
}
|
|
67
|
+
function normalizeSizes(sizes, count) {
|
|
68
|
+
if (sizes.length !== count || sizes.some((s) => !Number.isFinite(s) || s <= 0)) {
|
|
69
|
+
return evenSizes(count);
|
|
70
|
+
}
|
|
71
|
+
const total = sizes.reduce((sum, s) => sum + s, 0);
|
|
72
|
+
return sizes.map((s) => s / total);
|
|
73
|
+
}
|
|
74
|
+
function splitPane(node, paneId, newPaneId, orientation, newSplitId) {
|
|
75
|
+
if (node.type === "pane") {
|
|
76
|
+
if (node.paneId !== paneId) return node;
|
|
77
|
+
return {
|
|
78
|
+
type: "split",
|
|
79
|
+
id: newSplitId,
|
|
80
|
+
orientation,
|
|
81
|
+
children: [node, { type: "pane", paneId: newPaneId }],
|
|
82
|
+
sizes: [0.5, 0.5]
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const index = node.children.findIndex(
|
|
86
|
+
(child) => child.type === "pane" && child.paneId === paneId
|
|
87
|
+
);
|
|
88
|
+
if (index >= 0 && node.orientation === orientation) {
|
|
89
|
+
const share = node.sizes[index] ?? 1 / node.children.length;
|
|
90
|
+
const children = [...node.children];
|
|
91
|
+
children.splice(index + 1, 0, { type: "pane", paneId: newPaneId });
|
|
92
|
+
const sizes = [...node.sizes];
|
|
93
|
+
sizes.splice(index, 1, share / 2, share / 2);
|
|
94
|
+
return { ...node, children, sizes: normalizeSizes(sizes, children.length) };
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
...node,
|
|
98
|
+
children: node.children.map(
|
|
99
|
+
(child) => splitPane(child, paneId, newPaneId, orientation, newSplitId)
|
|
100
|
+
)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function removePane(node, paneId) {
|
|
104
|
+
if (node.type === "pane") return node.paneId === paneId ? null : node;
|
|
105
|
+
const children = [];
|
|
106
|
+
const sizes = [];
|
|
107
|
+
node.children.forEach((child, i) => {
|
|
108
|
+
const next = removePane(child, paneId);
|
|
109
|
+
if (next) {
|
|
110
|
+
children.push(next);
|
|
111
|
+
sizes.push(node.sizes[i] ?? 1 / node.children.length);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
if (children.length === 0) return null;
|
|
115
|
+
if (children.length === 1) return children[0] ?? null;
|
|
116
|
+
return { ...node, children, sizes: normalizeSizes(sizes, children.length) };
|
|
117
|
+
}
|
|
118
|
+
function resizeSplit(node, splitId, sizes) {
|
|
119
|
+
if (node.type === "pane") return node;
|
|
120
|
+
if (node.id === splitId) {
|
|
121
|
+
const clamped = sizes.map((s) => Math.max(CANVAS_MIN_SPLIT_FRACTION, s));
|
|
122
|
+
return { ...node, sizes: normalizeSizes(clamped, node.children.length) };
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
...node,
|
|
126
|
+
children: node.children.map((child) => resizeSplit(child, splitId, sizes))
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function isValidLayout(node, knownPanes, seen = /* @__PURE__ */ new Set()) {
|
|
130
|
+
if (!node || typeof node !== "object") return false;
|
|
131
|
+
const n = node;
|
|
132
|
+
if (n.type === "pane") {
|
|
133
|
+
const id = n.paneId;
|
|
134
|
+
if (typeof id !== "string" || !knownPanes.has(id) || seen.has(id)) return false;
|
|
135
|
+
seen.add(id);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
if (n.type !== "split") return false;
|
|
139
|
+
if (typeof n.id !== "string") return false;
|
|
140
|
+
if (n.orientation !== "horizontal" && n.orientation !== "vertical") return false;
|
|
141
|
+
if (!Array.isArray(n.children) || n.children.length < 2) return false;
|
|
142
|
+
if (!Array.isArray(n.sizes) || n.sizes.length !== n.children.length) return false;
|
|
143
|
+
return n.children.every((child) => isValidLayout(child, knownPanes, seen));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/core/reducer.ts
|
|
147
|
+
var P = "matrxCanvas/";
|
|
148
|
+
var canvasActions = {
|
|
149
|
+
open: (input) => ({ type: `${P}open`, payload: { ...input, now: Date.now() } }),
|
|
150
|
+
update: (itemId, patch) => ({
|
|
151
|
+
type: `${P}update`,
|
|
152
|
+
payload: { itemId, ...patch, now: Date.now() }
|
|
153
|
+
}),
|
|
154
|
+
/** Gives an item a new identity in place (e.g. a draft that was just saved and now has an id). */
|
|
155
|
+
rekey: (itemId, key) => ({ type: `${P}rekey`, payload: { itemId, key } }),
|
|
156
|
+
closeItem: (itemId) => ({ type: `${P}closeItem`, payload: { itemId } }),
|
|
157
|
+
closeOthers: (itemId) => ({ type: `${P}closeOthers`, payload: { itemId } }),
|
|
158
|
+
activate: (itemId) => ({ type: `${P}activate`, payload: { itemId } }),
|
|
159
|
+
focusPane: (paneId) => ({ type: `${P}focusPane`, payload: { paneId } }),
|
|
160
|
+
moveItem: (itemId, toPaneId, index) => ({
|
|
161
|
+
type: `${P}moveItem`,
|
|
162
|
+
payload: { itemId, toPaneId, index }
|
|
163
|
+
}),
|
|
164
|
+
splitPane: (paneId, orientation, moveItemId) => ({
|
|
165
|
+
type: `${P}splitPane`,
|
|
166
|
+
payload: { paneId, orientation, moveItemId }
|
|
167
|
+
}),
|
|
168
|
+
closePane: (paneId) => ({ type: `${P}closePane`, payload: { paneId } }),
|
|
169
|
+
resizeSplit: (splitId, sizes) => ({
|
|
170
|
+
type: `${P}resizeSplit`,
|
|
171
|
+
payload: { splitId, sizes }
|
|
172
|
+
}),
|
|
173
|
+
setOpen: (open) => ({ type: `${P}setOpen`, payload: { open } }),
|
|
174
|
+
toggle: () => ({ type: `${P}toggle` }),
|
|
175
|
+
setFullscreen: (fullscreen) => ({ type: `${P}setFullscreen`, payload: { fullscreen } }),
|
|
176
|
+
setWidth: (width) => ({ type: `${P}setWidth`, payload: { width } }),
|
|
177
|
+
hydrate: (snapshot) => ({ type: `${P}hydrate`, payload: { snapshot } }),
|
|
178
|
+
reset: () => ({ type: `${P}reset` })
|
|
179
|
+
};
|
|
180
|
+
function isCanvasAction(action) {
|
|
181
|
+
return !!action && typeof action === "object" && typeof action.type === "string" && action.type.startsWith(P);
|
|
182
|
+
}
|
|
183
|
+
function createInitialCanvasState() {
|
|
184
|
+
const paneId = paneIdFromSeq(1);
|
|
185
|
+
return {
|
|
186
|
+
version: 1,
|
|
187
|
+
isOpen: false,
|
|
188
|
+
isFullscreen: false,
|
|
189
|
+
width: CANVAS_DEFAULT_WIDTH,
|
|
190
|
+
layout: { type: "pane", paneId },
|
|
191
|
+
panes: { [paneId]: { id: paneId, itemIds: [], activeItemId: null } },
|
|
192
|
+
items: {},
|
|
193
|
+
focusedPaneId: paneId,
|
|
194
|
+
seq: 1,
|
|
195
|
+
hydrated: false
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function paneOf(state, itemId) {
|
|
199
|
+
for (const pane of Object.values(state.panes)) {
|
|
200
|
+
if (pane.itemIds.includes(itemId)) return pane;
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
function withPane(state, pane) {
|
|
205
|
+
return { ...state, panes: { ...state.panes, [pane.id]: pane } };
|
|
206
|
+
}
|
|
207
|
+
function nextActive(itemIds, removed, current) {
|
|
208
|
+
if (current !== removed) return current;
|
|
209
|
+
const index = itemIds.indexOf(removed);
|
|
210
|
+
const rest = itemIds.filter((id) => id !== removed);
|
|
211
|
+
return rest[Math.min(index, rest.length - 1)] ?? null;
|
|
212
|
+
}
|
|
213
|
+
function detachItem(state, itemId) {
|
|
214
|
+
const pane = paneOf(state, itemId);
|
|
215
|
+
if (!pane) return state;
|
|
216
|
+
return withPane(state, {
|
|
217
|
+
...pane,
|
|
218
|
+
itemIds: pane.itemIds.filter((id) => id !== itemId),
|
|
219
|
+
activeItemId: nextActive(pane.itemIds, itemId, pane.activeItemId)
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function dropPane(state, paneId) {
|
|
223
|
+
const layout = removePane(state.layout, paneId);
|
|
224
|
+
if (!layout) return state;
|
|
225
|
+
const panes = { ...state.panes };
|
|
226
|
+
delete panes[paneId];
|
|
227
|
+
const focusedPaneId = state.focusedPaneId === paneId ? listPaneIds(layout)[0] ?? state.focusedPaneId : state.focusedPaneId;
|
|
228
|
+
return { ...state, layout, panes, focusedPaneId };
|
|
229
|
+
}
|
|
230
|
+
function addSplit(state, paneId, orientation) {
|
|
231
|
+
const seq = state.seq + 1;
|
|
232
|
+
const newPaneId = paneIdFromSeq(seq);
|
|
233
|
+
const layout = splitPane(state.layout, paneId, newPaneId, orientation, splitIdFromSeq(seq));
|
|
234
|
+
return {
|
|
235
|
+
newPaneId,
|
|
236
|
+
state: {
|
|
237
|
+
...state,
|
|
238
|
+
seq,
|
|
239
|
+
layout,
|
|
240
|
+
panes: { ...state.panes, [newPaneId]: { id: newPaneId, itemIds: [], activeItemId: null } },
|
|
241
|
+
focusedPaneId: newPaneId
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function insertInto(state, paneId, itemId, activate, index) {
|
|
246
|
+
const pane = state.panes[paneId];
|
|
247
|
+
if (!pane) return state;
|
|
248
|
+
const itemIds = [...pane.itemIds.filter((id) => id !== itemId)];
|
|
249
|
+
const at = index === void 0 ? afterActive(itemIds, pane.activeItemId) : Math.max(0, Math.min(index, itemIds.length));
|
|
250
|
+
itemIds.splice(at, 0, itemId);
|
|
251
|
+
return withPane(state, {
|
|
252
|
+
...pane,
|
|
253
|
+
itemIds,
|
|
254
|
+
activeItemId: activate || pane.activeItemId === null ? itemId : pane.activeItemId
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
function afterActive(itemIds, active) {
|
|
258
|
+
const index = active ? itemIds.indexOf(active) : -1;
|
|
259
|
+
return index < 0 ? itemIds.length : index + 1;
|
|
260
|
+
}
|
|
261
|
+
function clampWidth(width) {
|
|
262
|
+
return Number.isFinite(width) ? Math.max(CANVAS_MIN_WIDTH, Math.round(width)) : CANVAS_DEFAULT_WIDTH;
|
|
263
|
+
}
|
|
264
|
+
function sanitizeCanvasSnapshot(raw) {
|
|
265
|
+
if (!raw || typeof raw !== "object") return null;
|
|
266
|
+
const s = raw;
|
|
267
|
+
if (s.version !== 1 || !s.panes || !s.items || !s.layout) return null;
|
|
268
|
+
const paneIds = new Set(Object.keys(s.panes));
|
|
269
|
+
if (!isValidLayout(s.layout, paneIds)) return null;
|
|
270
|
+
const layoutPanes = new Set(listPaneIds(s.layout));
|
|
271
|
+
if (layoutPanes.size !== paneIds.size) return null;
|
|
272
|
+
const items = {};
|
|
273
|
+
for (const [id, item] of Object.entries(s.items)) {
|
|
274
|
+
if (item && typeof item.kind === "string" && typeof item.key === "string" && canvasItemId(item.kind, item.key) === id) {
|
|
275
|
+
items[id] = item;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const panes = {};
|
|
279
|
+
for (const [id, pane] of Object.entries(s.panes)) {
|
|
280
|
+
const itemIds = (Array.isArray(pane.itemIds) ? pane.itemIds : []).filter((itemId) => itemId in items);
|
|
281
|
+
const activeItemId = pane.activeItemId && itemIds.includes(pane.activeItemId) ? pane.activeItemId : itemIds[0] ?? null;
|
|
282
|
+
panes[id] = { id, itemIds, activeItemId };
|
|
283
|
+
}
|
|
284
|
+
const focused = s.focusedPaneId && paneIds.has(s.focusedPaneId) ? s.focusedPaneId : listPaneIds(s.layout)[0];
|
|
285
|
+
if (!focused) return null;
|
|
286
|
+
const layout = normalizeLayout(s.layout);
|
|
287
|
+
const seq = Math.max(typeof s.seq === "number" && Number.isFinite(s.seq) ? s.seq : 0, highestIdNumber(layout));
|
|
288
|
+
return {
|
|
289
|
+
version: 1,
|
|
290
|
+
isOpen: s.isOpen === true,
|
|
291
|
+
// Full screen is a moment, not a preference: a reload never starts there.
|
|
292
|
+
isFullscreen: false,
|
|
293
|
+
width: clampWidth(typeof s.width === "number" ? s.width : CANVAS_DEFAULT_WIDTH),
|
|
294
|
+
layout,
|
|
295
|
+
panes,
|
|
296
|
+
items,
|
|
297
|
+
focusedPaneId: focused,
|
|
298
|
+
seq,
|
|
299
|
+
hydrated: true
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function normalizeLayout(node) {
|
|
303
|
+
if (node.type === "pane") return node;
|
|
304
|
+
return { ...node, sizes: normalizeSizes(node.sizes, node.children.length), children: node.children.map(normalizeLayout) };
|
|
305
|
+
}
|
|
306
|
+
function highestIdNumber(node) {
|
|
307
|
+
const own = Number((node.type === "pane" ? node.paneId : node.id).split("-").pop());
|
|
308
|
+
const mine = Number.isFinite(own) ? own : 0;
|
|
309
|
+
return node.type === "pane" ? mine : Math.max(mine, ...node.children.map(highestIdNumber));
|
|
310
|
+
}
|
|
311
|
+
function canvasReducer(state = createInitialCanvasState(), action) {
|
|
312
|
+
if (!isCanvasAction(action)) return state;
|
|
313
|
+
switch (action.type) {
|
|
314
|
+
case `${P}open`: {
|
|
315
|
+
const { kind, key, title, data, target = "focused", reveal = true, activate = true, now } = action.payload;
|
|
316
|
+
const itemId = canvasItemId(kind, key);
|
|
317
|
+
const existing = state.items[itemId];
|
|
318
|
+
let next = state;
|
|
319
|
+
if (existing) {
|
|
320
|
+
const item = {
|
|
321
|
+
...existing,
|
|
322
|
+
...data !== void 0 ? { data } : {},
|
|
323
|
+
...title !== void 0 ? { title } : {},
|
|
324
|
+
updatedAt: now
|
|
325
|
+
};
|
|
326
|
+
next = { ...next, items: { ...next.items, [itemId]: item } };
|
|
327
|
+
const pane = paneOf(next, itemId);
|
|
328
|
+
if (pane && activate) next = { ...withPane(next, { ...pane, activeItemId: itemId }), focusedPaneId: pane.id };
|
|
329
|
+
if (!pane) next = insertInto(next, next.focusedPaneId, itemId, true);
|
|
330
|
+
} else {
|
|
331
|
+
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
|
+
let paneId = next.focusedPaneId;
|
|
334
|
+
if (typeof target === "object" && next.panes[target.paneId]) {
|
|
335
|
+
paneId = target.paneId;
|
|
336
|
+
} else if (target === "split-right" || target === "split-down") {
|
|
337
|
+
const split = addSplit(next, next.focusedPaneId, target === "split-right" ? "horizontal" : "vertical");
|
|
338
|
+
next = split.state;
|
|
339
|
+
paneId = split.newPaneId;
|
|
340
|
+
}
|
|
341
|
+
next = insertInto(next, paneId, itemId, activate);
|
|
342
|
+
if (activate) next = { ...next, focusedPaneId: paneId };
|
|
343
|
+
}
|
|
344
|
+
return reveal ? { ...next, isOpen: true } : next;
|
|
345
|
+
}
|
|
346
|
+
case `${P}update`: {
|
|
347
|
+
const { itemId, data, title, now } = action.payload;
|
|
348
|
+
const item = state.items[itemId];
|
|
349
|
+
if (!item) return state;
|
|
350
|
+
return {
|
|
351
|
+
...state,
|
|
352
|
+
items: {
|
|
353
|
+
...state.items,
|
|
354
|
+
[itemId]: { ...item, ...data !== void 0 ? { data } : {}, ...title !== void 0 ? { title } : {}, updatedAt: now }
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
case `${P}rekey`: {
|
|
359
|
+
const { itemId, key } = action.payload;
|
|
360
|
+
const item = state.items[itemId];
|
|
361
|
+
if (!item || item.key === key) return state;
|
|
362
|
+
const nextId = canvasItemId(item.kind, key);
|
|
363
|
+
if (state.items[nextId]) return canvasReducer(canvasReducer(state, canvasActions.closeItem(itemId)), canvasActions.activate(nextId));
|
|
364
|
+
const items = { ...state.items, [nextId]: { ...item, id: nextId, key } };
|
|
365
|
+
delete items[itemId];
|
|
366
|
+
const panes = {};
|
|
367
|
+
for (const pane of Object.values(state.panes)) {
|
|
368
|
+
panes[pane.id] = pane.itemIds.includes(itemId) ? {
|
|
369
|
+
...pane,
|
|
370
|
+
itemIds: pane.itemIds.map((id) => id === itemId ? nextId : id),
|
|
371
|
+
activeItemId: pane.activeItemId === itemId ? nextId : pane.activeItemId
|
|
372
|
+
} : pane;
|
|
373
|
+
}
|
|
374
|
+
return { ...state, items, panes };
|
|
375
|
+
}
|
|
376
|
+
case `${P}closeItem`: {
|
|
377
|
+
const { itemId } = action.payload;
|
|
378
|
+
if (!state.items[itemId]) return state;
|
|
379
|
+
const pane = paneOf(state, itemId);
|
|
380
|
+
let next = detachItem(state, itemId);
|
|
381
|
+
const items = { ...next.items };
|
|
382
|
+
delete items[itemId];
|
|
383
|
+
next = { ...next, items };
|
|
384
|
+
if (pane && next.panes[pane.id]?.itemIds.length === 0) next = dropPane(next, pane.id);
|
|
385
|
+
return next;
|
|
386
|
+
}
|
|
387
|
+
case `${P}closeOthers`: {
|
|
388
|
+
const { itemId } = action.payload;
|
|
389
|
+
const pane = paneOf(state, itemId);
|
|
390
|
+
if (!pane) return state;
|
|
391
|
+
const items = { ...state.items };
|
|
392
|
+
for (const id of pane.itemIds) if (id !== itemId) delete items[id];
|
|
393
|
+
return { ...withPane(state, { ...pane, itemIds: [itemId], activeItemId: itemId }), items };
|
|
394
|
+
}
|
|
395
|
+
case `${P}activate`: {
|
|
396
|
+
const pane = paneOf(state, action.payload.itemId);
|
|
397
|
+
if (!pane) return state;
|
|
398
|
+
return { ...withPane(state, { ...pane, activeItemId: action.payload.itemId }), focusedPaneId: pane.id };
|
|
399
|
+
}
|
|
400
|
+
case `${P}focusPane`:
|
|
401
|
+
return state.panes[action.payload.paneId] ? { ...state, focusedPaneId: action.payload.paneId } : state;
|
|
402
|
+
case `${P}moveItem`: {
|
|
403
|
+
const { itemId, toPaneId, index } = action.payload;
|
|
404
|
+
const from = paneOf(state, itemId);
|
|
405
|
+
if (!from || !state.panes[toPaneId]) return state;
|
|
406
|
+
if (from.id === toPaneId && index === void 0) return state.focusedPaneId === toPaneId ? state : { ...state, focusedPaneId: toPaneId };
|
|
407
|
+
let next = from.id === toPaneId ? state : detachItem(state, itemId);
|
|
408
|
+
next = insertInto(next, toPaneId, itemId, true, index);
|
|
409
|
+
next = { ...next, focusedPaneId: toPaneId };
|
|
410
|
+
if (from.id !== toPaneId && next.panes[from.id]?.itemIds.length === 0) next = dropPane(next, from.id);
|
|
411
|
+
return next;
|
|
412
|
+
}
|
|
413
|
+
case `${P}splitPane`: {
|
|
414
|
+
const { paneId, orientation, moveItemId } = action.payload;
|
|
415
|
+
const pane = state.panes[paneId];
|
|
416
|
+
if (!pane) return state;
|
|
417
|
+
const split = addSplit(state, paneId, orientation);
|
|
418
|
+
let next = split.state;
|
|
419
|
+
if (moveItemId && pane.itemIds.includes(moveItemId) && pane.itemIds.length > 1) {
|
|
420
|
+
next = detachItem(next, moveItemId);
|
|
421
|
+
next = insertInto(next, split.newPaneId, moveItemId, true);
|
|
422
|
+
}
|
|
423
|
+
return next;
|
|
424
|
+
}
|
|
425
|
+
case `${P}closePane`: {
|
|
426
|
+
const pane = state.panes[action.payload.paneId];
|
|
427
|
+
if (!pane) return state;
|
|
428
|
+
const onlyPane = listPaneIds(state.layout).length === 1;
|
|
429
|
+
if (onlyPane) return { ...state, isOpen: false, isFullscreen: false };
|
|
430
|
+
const items = { ...state.items };
|
|
431
|
+
for (const id of pane.itemIds) delete items[id];
|
|
432
|
+
return dropPane({ ...state, items }, pane.id);
|
|
433
|
+
}
|
|
434
|
+
case `${P}resizeSplit`:
|
|
435
|
+
return { ...state, layout: resizeSplit(state.layout, action.payload.splitId, action.payload.sizes) };
|
|
436
|
+
case `${P}setOpen`:
|
|
437
|
+
return action.payload.open === state.isOpen ? state : { ...state, isOpen: action.payload.open, isFullscreen: action.payload.open ? state.isFullscreen : false };
|
|
438
|
+
case `${P}toggle`:
|
|
439
|
+
return { ...state, isOpen: !state.isOpen, isFullscreen: state.isOpen ? false : state.isFullscreen };
|
|
440
|
+
case `${P}setFullscreen`:
|
|
441
|
+
return { ...state, isFullscreen: action.payload.fullscreen, isOpen: action.payload.fullscreen ? true : state.isOpen };
|
|
442
|
+
case `${P}setWidth`:
|
|
443
|
+
return { ...state, width: clampWidth(action.payload.width) };
|
|
444
|
+
case `${P}hydrate`: {
|
|
445
|
+
const snapshot = action.payload.snapshot ? sanitizeCanvasSnapshot(action.payload.snapshot) : null;
|
|
446
|
+
if (!snapshot) return { ...state, hydrated: true };
|
|
447
|
+
let next = snapshot;
|
|
448
|
+
for (const item of Object.values(state.items)) {
|
|
449
|
+
const pane = paneOf(state, item.id);
|
|
450
|
+
next = { ...next, items: { ...next.items, [item.id]: item } };
|
|
451
|
+
if (!paneOf(next, item.id)) next = insertInto(next, next.focusedPaneId, item.id, pane?.activeItemId === item.id);
|
|
452
|
+
}
|
|
453
|
+
return { ...next, isOpen: next.isOpen || state.isOpen, hydrated: true };
|
|
454
|
+
}
|
|
455
|
+
case `${P}reset`:
|
|
456
|
+
return { ...createInitialCanvasState(), hydrated: true };
|
|
457
|
+
default:
|
|
458
|
+
return state;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/core/store.ts
|
|
463
|
+
function createCanvasStore(initial) {
|
|
464
|
+
let state = initial ?? createInitialCanvasState();
|
|
465
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
466
|
+
return {
|
|
467
|
+
getState: () => state,
|
|
468
|
+
dispatch(action) {
|
|
469
|
+
const next = canvasReducer(state, action);
|
|
470
|
+
if (next === state) return;
|
|
471
|
+
state = next;
|
|
472
|
+
for (const listener of [...listeners]) listener();
|
|
473
|
+
},
|
|
474
|
+
subscribe(listener) {
|
|
475
|
+
listeners.add(listener);
|
|
476
|
+
return () => listeners.delete(listener);
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/core/persistence.ts
|
|
482
|
+
var CANVAS_STORAGE_KEY = "ai-matrx.canvas.v1";
|
|
483
|
+
function createLocalStorageCanvasPersistence(key = CANVAS_STORAGE_KEY, storage = typeof window === "undefined" ? null : safeLocalStorage()) {
|
|
484
|
+
return {
|
|
485
|
+
load() {
|
|
486
|
+
if (!storage) throw new Error("Canvas storage is unavailable; allow browser storage or provide a persistence port.");
|
|
487
|
+
const raw = storage.getItem(key);
|
|
488
|
+
if (!raw) return null;
|
|
489
|
+
return JSON.parse(raw);
|
|
490
|
+
},
|
|
491
|
+
save(snapshot) {
|
|
492
|
+
if (!storage) throw new Error("Canvas storage is unavailable; allow browser storage or provide a persistence port.");
|
|
493
|
+
storage.setItem(key, JSON.stringify(snapshot));
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
function safeLocalStorage() {
|
|
498
|
+
try {
|
|
499
|
+
const probe = "__mxc_probe__";
|
|
500
|
+
window.localStorage.setItem(probe, "1");
|
|
501
|
+
window.localStorage.removeItem(probe);
|
|
502
|
+
return window.localStorage;
|
|
503
|
+
} catch {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function toPersistableSnapshot(state, isRestorable) {
|
|
508
|
+
const keep = new Set(
|
|
509
|
+
Object.values(state.items).filter((item) => isRestorable(item.kind)).map((item) => item.id)
|
|
510
|
+
);
|
|
511
|
+
if (keep.size === Object.keys(state.items).length) return state;
|
|
512
|
+
const items = Object.fromEntries(Object.entries(state.items).filter(([id]) => keep.has(id)));
|
|
513
|
+
let layout = state.layout;
|
|
514
|
+
const panes = {};
|
|
515
|
+
for (const pane of Object.values(state.panes)) {
|
|
516
|
+
const itemIds = pane.itemIds.filter((id) => keep.has(id));
|
|
517
|
+
if (itemIds.length === 0 && pane.itemIds.length > 0) {
|
|
518
|
+
const pruned = removePane(layout, pane.id);
|
|
519
|
+
if (pruned) {
|
|
520
|
+
layout = pruned;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
panes[pane.id] = {
|
|
525
|
+
...pane,
|
|
526
|
+
itemIds,
|
|
527
|
+
activeItemId: pane.activeItemId && keep.has(pane.activeItemId) ? pane.activeItemId : itemIds[0] ?? null
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
const focusedPaneId = panes[state.focusedPaneId] ? state.focusedPaneId : Object.values(panes)[0]?.id ?? state.focusedPaneId;
|
|
531
|
+
return { ...state, items, panes, layout, focusedPaneId };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/core/controller.ts
|
|
535
|
+
var consoleCanvasErrorSink = (report) => {
|
|
536
|
+
console.error(`[@ai-matrx/canvas] ${report.code}: ${report.message}`, report.detail ?? "");
|
|
537
|
+
};
|
|
538
|
+
function createCanvasController(options) {
|
|
539
|
+
const { store, persistence = null } = options;
|
|
540
|
+
const onError = options.onError ?? consoleCanvasErrorSink;
|
|
541
|
+
const isRestorable = options.isRestorable ?? (() => true);
|
|
542
|
+
const dispatch = (action) => store.dispatch(action);
|
|
543
|
+
let presentations = 0;
|
|
544
|
+
const presentationListeners = /* @__PURE__ */ new Set();
|
|
545
|
+
const notifyPresentation = () => {
|
|
546
|
+
for (const listener of [...presentationListeners]) listener();
|
|
547
|
+
};
|
|
548
|
+
const controller = {
|
|
549
|
+
store,
|
|
550
|
+
getState: () => store.getState(),
|
|
551
|
+
open(input) {
|
|
552
|
+
if (options.isKnownKind && !options.isKnownKind(input.kind)) {
|
|
553
|
+
onError({ code: "unknown-kind", message: `No canvas kind "${input.kind}" is registered.`, detail: input });
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
if (input.data !== void 0) {
|
|
557
|
+
const bad = findNonJson(input.data);
|
|
558
|
+
if (bad) {
|
|
559
|
+
onError({
|
|
560
|
+
code: "non-json-data",
|
|
561
|
+
message: `Canvas data for "${input.kind}" must be plain JSON; ${bad} is not.`,
|
|
562
|
+
detail: { kind: input.kind, key: input.key }
|
|
563
|
+
});
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
dispatch(canvasActions.open(input));
|
|
568
|
+
return canvasItemId(input.kind, input.key);
|
|
569
|
+
},
|
|
570
|
+
update(itemId, patch) {
|
|
571
|
+
if (patch.data !== void 0) {
|
|
572
|
+
const bad = findNonJson(patch.data);
|
|
573
|
+
if (bad) {
|
|
574
|
+
onError({ code: "non-json-data", message: `Canvas update for ${itemId} must be plain JSON; ${bad} is not.` });
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (!store.getState().items[itemId]) return false;
|
|
579
|
+
dispatch(canvasActions.update(itemId, patch));
|
|
580
|
+
return true;
|
|
581
|
+
},
|
|
582
|
+
rekey(itemId, key) {
|
|
583
|
+
const item = store.getState().items[itemId];
|
|
584
|
+
if (!item) return null;
|
|
585
|
+
dispatch(canvasActions.rekey(itemId, key));
|
|
586
|
+
return canvasItemId(item.kind, key);
|
|
587
|
+
},
|
|
588
|
+
close: (itemId) => dispatch(canvasActions.closeItem(itemId)),
|
|
589
|
+
closeOthers: (itemId) => dispatch(canvasActions.closeOthers(itemId)),
|
|
590
|
+
activate: (itemId) => dispatch(canvasActions.activate(itemId)),
|
|
591
|
+
focusPane: (paneId) => dispatch(canvasActions.focusPane(paneId)),
|
|
592
|
+
moveItem: (itemId, toPaneId, index) => dispatch(canvasActions.moveItem(itemId, toPaneId, index)),
|
|
593
|
+
splitPane: (paneId, orientation, moveItemId) => dispatch(canvasActions.splitPane(paneId, orientation, moveItemId)),
|
|
594
|
+
closePane: (paneId) => dispatch(canvasActions.closePane(paneId)),
|
|
595
|
+
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)),
|
|
601
|
+
has: (kind, key) => canvasItemId(kind, key) in store.getState().items,
|
|
602
|
+
isPresented: () => presentations > 0,
|
|
603
|
+
registerPresentation() {
|
|
604
|
+
presentations += 1;
|
|
605
|
+
notifyPresentation();
|
|
606
|
+
let done = false;
|
|
607
|
+
return () => {
|
|
608
|
+
if (done) return;
|
|
609
|
+
done = true;
|
|
610
|
+
presentations -= 1;
|
|
611
|
+
notifyPresentation();
|
|
612
|
+
};
|
|
613
|
+
},
|
|
614
|
+
subscribePresentation(listener) {
|
|
615
|
+
presentationListeners.add(listener);
|
|
616
|
+
return () => presentationListeners.delete(listener);
|
|
617
|
+
},
|
|
618
|
+
start() {
|
|
619
|
+
let disposed = false;
|
|
620
|
+
let timer = null;
|
|
621
|
+
let lastSaved = null;
|
|
622
|
+
const save = () => {
|
|
623
|
+
timer = null;
|
|
624
|
+
const state = store.getState();
|
|
625
|
+
if (!persistence || !state.hydrated || state === lastSaved) return;
|
|
626
|
+
lastSaved = state;
|
|
627
|
+
try {
|
|
628
|
+
void Promise.resolve(persistence.save(toPersistableSnapshot(state, isRestorable))).catch(
|
|
629
|
+
(error) => onError({ code: "persistence-save", message: "Saving the canvas layout failed.", detail: error })
|
|
630
|
+
);
|
|
631
|
+
} catch (error) {
|
|
632
|
+
onError({ code: "persistence-save", message: "Saving the canvas layout failed.", detail: error });
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
let seen = store.getState();
|
|
636
|
+
const unsubscribe = store.subscribe(() => {
|
|
637
|
+
if (!persistence || disposed) return;
|
|
638
|
+
const current = store.getState();
|
|
639
|
+
if (current === seen) return;
|
|
640
|
+
seen = current;
|
|
641
|
+
if (timer) clearTimeout(timer);
|
|
642
|
+
timer = setTimeout(save, options.saveDelayMs ?? 250);
|
|
643
|
+
});
|
|
644
|
+
if (store.getState().hydrated) {
|
|
645
|
+
} else if (!persistence) {
|
|
646
|
+
dispatch(canvasActions.hydrate(null));
|
|
647
|
+
} else {
|
|
648
|
+
Promise.resolve().then(() => persistence.load()).then(
|
|
649
|
+
(snapshot) => {
|
|
650
|
+
if (disposed) return;
|
|
651
|
+
const openedThisSession = store.getState().isOpen;
|
|
652
|
+
const validSnapshot = snapshot === null ? null : sanitizeCanvasSnapshot(snapshot);
|
|
653
|
+
if (snapshot !== null && validSnapshot === null) {
|
|
654
|
+
onError({ code: "persistence-load", message: "The saved canvas layout is invalid. Clear the saved layout or repair the persistence port." });
|
|
655
|
+
}
|
|
656
|
+
dispatch(canvasActions.hydrate(validSnapshot));
|
|
657
|
+
if (!openedThisSession && options.mayRestoreOpen && !options.mayRestoreOpen() && store.getState().isOpen) {
|
|
658
|
+
dispatch(canvasActions.setOpen(false));
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
(error) => {
|
|
662
|
+
onError({ code: "persistence-load", message: "The saved canvas layout could not be read.", detail: error });
|
|
663
|
+
if (!disposed) dispatch(canvasActions.hydrate(null));
|
|
664
|
+
}
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
return () => {
|
|
668
|
+
disposed = true;
|
|
669
|
+
unsubscribe();
|
|
670
|
+
if (timer) {
|
|
671
|
+
clearTimeout(timer);
|
|
672
|
+
save();
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
return controller;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// src/core/selectors.ts
|
|
681
|
+
var selectCanvasIsOpen = (s) => s.isOpen;
|
|
682
|
+
var selectCanvasIsFullscreen = (s) => s.isFullscreen;
|
|
683
|
+
var selectCanvasWidth = (s) => s.width;
|
|
684
|
+
var selectCanvasLayout = (s) => s.layout;
|
|
685
|
+
var selectCanvasItemCount = (s) => Object.keys(s.items).length;
|
|
686
|
+
function selectCanvasPaneCount(s) {
|
|
687
|
+
return listPaneIds(s.layout).length;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/react/registry.ts
|
|
691
|
+
function defineCanvasKind(kind) {
|
|
692
|
+
if (!kind.component && !kind.load) {
|
|
693
|
+
throw new Error(`[@ai-matrx/canvas] kind "${kind.id}" needs a component or a load().`);
|
|
694
|
+
}
|
|
695
|
+
return kind;
|
|
696
|
+
}
|
|
697
|
+
var REGISTRY = /* @__PURE__ */ Symbol.for("ai-matrx.canvas.kinds");
|
|
698
|
+
function registry() {
|
|
699
|
+
const g = globalThis;
|
|
700
|
+
g[REGISTRY] ??= { kinds: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set(), version: 0 };
|
|
701
|
+
return g[REGISTRY];
|
|
702
|
+
}
|
|
703
|
+
function notify(state) {
|
|
704
|
+
state.version += 1;
|
|
705
|
+
for (const listener of [...state.listeners]) listener();
|
|
706
|
+
}
|
|
707
|
+
function registerCanvasKind(kind) {
|
|
708
|
+
const state = registry();
|
|
709
|
+
state.kinds.set(kind.id, kind);
|
|
710
|
+
notify(state);
|
|
711
|
+
return () => {
|
|
712
|
+
if (state.kinds.get(kind.id) === kind) {
|
|
713
|
+
state.kinds.delete(kind.id);
|
|
714
|
+
notify(state);
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function registerCanvasKinds(kinds) {
|
|
719
|
+
const undo = kinds.map(registerCanvasKind);
|
|
720
|
+
return () => undo.forEach((fn) => fn());
|
|
721
|
+
}
|
|
722
|
+
function getCanvasKind(id) {
|
|
723
|
+
return registry().kinds.get(id);
|
|
724
|
+
}
|
|
725
|
+
function listCanvasKinds() {
|
|
726
|
+
return [...registry().kinds.values()];
|
|
727
|
+
}
|
|
728
|
+
function subscribeCanvasKinds(listener) {
|
|
729
|
+
const state = registry();
|
|
730
|
+
state.listeners.add(listener);
|
|
731
|
+
return () => state.listeners.delete(listener);
|
|
732
|
+
}
|
|
733
|
+
function getCanvasKindsVersion() {
|
|
734
|
+
return registry().version;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// src/react/provider.tsx
|
|
738
|
+
import { jsx } from "react/jsx-runtime";
|
|
739
|
+
var PHONE_MAX_WIDTH = 767;
|
|
740
|
+
var isPhoneViewport = () => typeof window !== "undefined" && window.innerWidth <= PHONE_MAX_WIDTH;
|
|
741
|
+
function escapeBelongsElsewhere(event) {
|
|
742
|
+
if (event.defaultPrevented) return true;
|
|
743
|
+
const active = typeof document === "undefined" ? null : document.activeElement;
|
|
744
|
+
return !!active?.closest?.('[role="menu"], [role="dialog"], [role="alertdialog"], [role="listbox"]');
|
|
745
|
+
}
|
|
746
|
+
var CanvasContext = createContext(null);
|
|
747
|
+
function CanvasProvider({
|
|
748
|
+
children,
|
|
749
|
+
store,
|
|
750
|
+
persistence,
|
|
751
|
+
onError,
|
|
752
|
+
hotkeys = true,
|
|
753
|
+
itemActions,
|
|
754
|
+
popOut
|
|
755
|
+
}) {
|
|
756
|
+
const [controller] = useState(
|
|
757
|
+
() => createCanvasController({
|
|
758
|
+
store: store ?? createCanvasStore(),
|
|
759
|
+
persistence: persistence === void 0 ? createLocalStorageCanvasPersistence() : persistence,
|
|
760
|
+
isRestorable: (kind) => getCanvasKind(kind)?.restore !== false,
|
|
761
|
+
mayRestoreOpen: () => !isPhoneViewport(),
|
|
762
|
+
onError
|
|
763
|
+
})
|
|
764
|
+
);
|
|
765
|
+
useEffect(() => controller.start(), [controller]);
|
|
766
|
+
useEffect(() => {
|
|
767
|
+
if (!hotkeys) return;
|
|
768
|
+
const onKey = (event) => {
|
|
769
|
+
if (!controller.isPresented()) return;
|
|
770
|
+
if (event.key === "\\" && (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey) {
|
|
771
|
+
event.preventDefault();
|
|
772
|
+
controller.toggle();
|
|
773
|
+
} else if (event.key === "Escape" && controller.getState().isFullscreen && !escapeBelongsElsewhere(event)) {
|
|
774
|
+
controller.setFullscreen(false);
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
window.addEventListener("keydown", onKey);
|
|
778
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
779
|
+
}, [controller, hotkeys]);
|
|
780
|
+
const ports = {
|
|
781
|
+
...itemActions ? { itemActions } : {},
|
|
782
|
+
...popOut ? { popOut } : {}
|
|
783
|
+
};
|
|
784
|
+
return /* @__PURE__ */ jsx(CanvasContext.Provider, { value: { controller, ports }, children });
|
|
785
|
+
}
|
|
786
|
+
function useCanvasContext() {
|
|
787
|
+
const value = useContext(CanvasContext);
|
|
788
|
+
if (!value) throw new Error("[@ai-matrx/canvas] useCanvas* must be used inside <CanvasProvider>.");
|
|
789
|
+
return value;
|
|
790
|
+
}
|
|
791
|
+
function useCanvas() {
|
|
792
|
+
return useCanvasContext().controller;
|
|
793
|
+
}
|
|
794
|
+
function useOptionalCanvas() {
|
|
795
|
+
return useContext(CanvasContext)?.controller ?? null;
|
|
796
|
+
}
|
|
797
|
+
function useCanvasHostPorts() {
|
|
798
|
+
return useCanvasContext().ports;
|
|
799
|
+
}
|
|
800
|
+
function useRegisterCanvasPresence() {
|
|
801
|
+
const controller = useContext(CanvasContext)?.controller;
|
|
802
|
+
useEffect(() => controller?.registerPresentation(), [controller]);
|
|
803
|
+
}
|
|
804
|
+
var noPresence = () => () => void 0;
|
|
805
|
+
function useCanvasIsPresented() {
|
|
806
|
+
const controller = useContext(CanvasContext)?.controller;
|
|
807
|
+
return useSyncExternalStore(
|
|
808
|
+
controller ? controller.subscribePresentation : noPresence,
|
|
809
|
+
() => controller?.isPresented() ?? false,
|
|
810
|
+
() => false
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
function useCanvasState(selector) {
|
|
814
|
+
const { store } = useCanvasContext().controller;
|
|
815
|
+
return useSyncExternalStore(
|
|
816
|
+
store.subscribe,
|
|
817
|
+
() => selector(store.getState()),
|
|
818
|
+
() => selector(store.getState())
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
var NO_SUBSCRIPTION = () => () => void 0;
|
|
822
|
+
function useOptionalCanvasState(selector, fallback) {
|
|
823
|
+
const store = useContext(CanvasContext)?.controller.store;
|
|
824
|
+
const read = () => store ? selector(store.getState()) : fallback;
|
|
825
|
+
return useSyncExternalStore(store ? store.subscribe : NO_SUBSCRIPTION, read, read);
|
|
826
|
+
}
|
|
827
|
+
function useCanvasKinds() {
|
|
828
|
+
useSyncExternalStore(subscribeCanvasKinds, getCanvasKindsVersion, getCanvasKindsVersion);
|
|
829
|
+
return listCanvasKinds();
|
|
830
|
+
}
|
|
831
|
+
function useCanvasKind(id) {
|
|
832
|
+
useSyncExternalStore(subscribeCanvasKinds, getCanvasKindsVersion, getCanvasKindsVersion);
|
|
833
|
+
return getCanvasKind(id);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/react/CanvasColumn.tsx
|
|
837
|
+
import { useRef as useRef2, useState as useState3, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
838
|
+
import { TapTargetButtonTransparent as TapTargetButtonTransparent2 } from "@ai-matrx/tap-target";
|
|
839
|
+
|
|
840
|
+
// src/react/CanvasPane.tsx
|
|
841
|
+
import {
|
|
842
|
+
Component,
|
|
843
|
+
Suspense,
|
|
844
|
+
lazy,
|
|
845
|
+
useEffect as useEffect2,
|
|
846
|
+
useRef,
|
|
847
|
+
useState as useState2
|
|
848
|
+
} from "react";
|
|
849
|
+
import { TapTargetButtonTransparent } from "@ai-matrx/tap-target";
|
|
850
|
+
import * as Menu from "@radix-ui/react-dropdown-menu";
|
|
851
|
+
|
|
852
|
+
// src/react/icons.tsx
|
|
853
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
854
|
+
function Glyph({ children, ...props }) {
|
|
855
|
+
return /* @__PURE__ */ jsx2(
|
|
856
|
+
"svg",
|
|
857
|
+
{
|
|
858
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
859
|
+
width: "16",
|
|
860
|
+
height: "16",
|
|
861
|
+
viewBox: "0 0 24 24",
|
|
862
|
+
fill: "none",
|
|
863
|
+
stroke: "currentColor",
|
|
864
|
+
strokeWidth: 2,
|
|
865
|
+
strokeLinecap: "round",
|
|
866
|
+
strokeLinejoin: "round",
|
|
867
|
+
"aria-hidden": "true",
|
|
868
|
+
...props,
|
|
869
|
+
children
|
|
870
|
+
}
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
var CloseIcon = (p) => /* @__PURE__ */ jsxs(Glyph, { ...p, children: [
|
|
874
|
+
/* @__PURE__ */ jsx2("path", { d: "M18 6 6 18" }),
|
|
875
|
+
/* @__PURE__ */ jsx2("path", { d: "m6 6 12 12" })
|
|
876
|
+
] });
|
|
877
|
+
var MaximizeIcon = (p) => /* @__PURE__ */ jsxs(Glyph, { ...p, children: [
|
|
878
|
+
/* @__PURE__ */ jsx2("path", { d: "M15 3h6v6" }),
|
|
879
|
+
/* @__PURE__ */ jsx2("path", { d: "m21 3-7 7" }),
|
|
880
|
+
/* @__PURE__ */ jsx2("path", { d: "m3 21 7-7" }),
|
|
881
|
+
/* @__PURE__ */ jsx2("path", { d: "M9 21H3v-6" })
|
|
882
|
+
] });
|
|
883
|
+
var MinimizeIcon = (p) => /* @__PURE__ */ jsxs(Glyph, { ...p, children: [
|
|
884
|
+
/* @__PURE__ */ jsx2("path", { d: "m14 10 7-7" }),
|
|
885
|
+
/* @__PURE__ */ jsx2("path", { d: "M20 10h-6V4" }),
|
|
886
|
+
/* @__PURE__ */ jsx2("path", { d: "m3 21 7-7" }),
|
|
887
|
+
/* @__PURE__ */ jsx2("path", { d: "M4 14h6v6" })
|
|
888
|
+
] });
|
|
889
|
+
var MoreIcon = (p) => /* @__PURE__ */ jsxs(Glyph, { ...p, children: [
|
|
890
|
+
/* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "1" }),
|
|
891
|
+
/* @__PURE__ */ jsx2("circle", { cx: "12", cy: "5", r: "1" }),
|
|
892
|
+
/* @__PURE__ */ jsx2("circle", { cx: "12", cy: "19", r: "1" })
|
|
893
|
+
] });
|
|
894
|
+
var SplitRightIcon = (p) => /* @__PURE__ */ jsxs(Glyph, { ...p, children: [
|
|
895
|
+
/* @__PURE__ */ jsx2("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
|
|
896
|
+
/* @__PURE__ */ jsx2("path", { d: "M12 3v18" })
|
|
897
|
+
] });
|
|
898
|
+
var SplitDownIcon = (p) => /* @__PURE__ */ jsxs(Glyph, { ...p, children: [
|
|
899
|
+
/* @__PURE__ */ jsx2("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
|
|
900
|
+
/* @__PURE__ */ jsx2("path", { d: "M3 12h18" })
|
|
901
|
+
] });
|
|
902
|
+
var PanelRightIcon = (p) => /* @__PURE__ */ jsxs(Glyph, { ...p, children: [
|
|
903
|
+
/* @__PURE__ */ jsx2("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }),
|
|
904
|
+
/* @__PURE__ */ jsx2("path", { d: "M15 3v18" })
|
|
905
|
+
] });
|
|
906
|
+
var PopOutIcon = (p) => /* @__PURE__ */ jsxs(Glyph, { ...p, children: [
|
|
907
|
+
/* @__PURE__ */ jsx2("path", { d: "M15 3h6v6" }),
|
|
908
|
+
/* @__PURE__ */ jsx2("path", { d: "M10 14 21 3" }),
|
|
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" })
|
|
910
|
+
] });
|
|
911
|
+
var AlertIcon = (p) => /* @__PURE__ */ jsxs(Glyph, { ...p, children: [
|
|
912
|
+
/* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "10" }),
|
|
913
|
+
/* @__PURE__ */ jsx2("path", { d: "M12 8v4" }),
|
|
914
|
+
/* @__PURE__ */ jsx2("path", { d: "M12 16h.01" })
|
|
915
|
+
] });
|
|
916
|
+
|
|
917
|
+
// src/react/CanvasPane.tsx
|
|
918
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
919
|
+
var DRAG_MIME = "application/x-matrx-canvas-item";
|
|
920
|
+
var LAZY = /* @__PURE__ */ Symbol.for("ai-matrx.canvas.lazy");
|
|
921
|
+
function kindComponent(kind) {
|
|
922
|
+
if (kind.component) return kind.component;
|
|
923
|
+
const holder = kind;
|
|
924
|
+
if (!holder[LAZY] && kind.load) holder[LAZY] = lazy(kind.load);
|
|
925
|
+
const resolved = holder[LAZY];
|
|
926
|
+
if (!resolved) throw new Error(`[@ai-matrx/canvas] kind "${kind.id}" has nothing to render.`);
|
|
927
|
+
return resolved;
|
|
928
|
+
}
|
|
929
|
+
function resetKindComponent(kind) {
|
|
930
|
+
if (kind) delete kind[LAZY];
|
|
931
|
+
}
|
|
932
|
+
function itemTitle(item, kind) {
|
|
933
|
+
if (item.title) return item.title;
|
|
934
|
+
if (kind?.title) {
|
|
935
|
+
try {
|
|
936
|
+
const title = kind.title(item.data, item);
|
|
937
|
+
if (title) return title;
|
|
938
|
+
} catch {
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return kind?.label ?? item.kind;
|
|
942
|
+
}
|
|
943
|
+
function CanvasPaneView({ paneId }) {
|
|
944
|
+
const canvas = useCanvas();
|
|
945
|
+
const pane = useCanvasState((s) => s.panes[paneId]);
|
|
946
|
+
const items = useCanvasState((s) => s.items);
|
|
947
|
+
const focused = useCanvasState((s) => s.focusedPaneId === paneId);
|
|
948
|
+
const [dropActive, setDropActive] = useState2(false);
|
|
949
|
+
if (!pane) return null;
|
|
950
|
+
const active = pane.activeItemId ? items[pane.activeItemId] : void 0;
|
|
951
|
+
const onDragOver = (event) => {
|
|
952
|
+
if (!event.dataTransfer.types.includes(DRAG_MIME)) return;
|
|
953
|
+
event.preventDefault();
|
|
954
|
+
event.dataTransfer.dropEffect = "move";
|
|
955
|
+
setDropActive(true);
|
|
956
|
+
};
|
|
957
|
+
const onDrop = (event) => {
|
|
958
|
+
setDropActive(false);
|
|
959
|
+
const itemId = event.dataTransfer.getData(DRAG_MIME);
|
|
960
|
+
if (!itemId) return;
|
|
961
|
+
event.preventDefault();
|
|
962
|
+
canvas.moveItem(itemId, paneId);
|
|
963
|
+
};
|
|
964
|
+
return /* @__PURE__ */ jsxs2(
|
|
965
|
+
"section",
|
|
966
|
+
{
|
|
967
|
+
className: "mxc-pane",
|
|
968
|
+
"data-focused": focused ? "" : void 0,
|
|
969
|
+
"data-drop": dropActive ? "" : void 0,
|
|
970
|
+
onPointerDownCapture: () => {
|
|
971
|
+
if (!focused) canvas.focusPane(paneId);
|
|
972
|
+
},
|
|
973
|
+
onDragOver,
|
|
974
|
+
onDragLeave: () => setDropActive(false),
|
|
975
|
+
onDrop,
|
|
976
|
+
children: [
|
|
977
|
+
/* @__PURE__ */ jsx3(PaneHeader, { paneId, itemIds: pane.itemIds, activeItemId: pane.activeItemId, activeItem: active }),
|
|
978
|
+
/* @__PURE__ */ jsx3("div", { className: "mxc-pane-body", children: pane.itemIds.length === 0 ? /* @__PURE__ */ jsx3(Launcher, { paneId }) : pane.itemIds.map((itemId) => {
|
|
979
|
+
const item = items[itemId];
|
|
980
|
+
if (!item) return null;
|
|
981
|
+
const isActive = itemId === pane.activeItemId;
|
|
982
|
+
return /* @__PURE__ */ jsx3(ItemBody, { item, paneId, isActive, isFocused: focused && isActive }, itemId);
|
|
983
|
+
}) })
|
|
984
|
+
]
|
|
985
|
+
}
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
function PaneHeader({
|
|
989
|
+
paneId,
|
|
990
|
+
itemIds,
|
|
991
|
+
activeItemId,
|
|
992
|
+
activeItem
|
|
993
|
+
}) {
|
|
994
|
+
const canvas = useCanvas();
|
|
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
|
+
] })
|
|
1032
|
+
] });
|
|
1033
|
+
}
|
|
1034
|
+
function Tab({
|
|
1035
|
+
itemId,
|
|
1036
|
+
paneId,
|
|
1037
|
+
index,
|
|
1038
|
+
active
|
|
1039
|
+
}) {
|
|
1040
|
+
const canvas = useCanvas();
|
|
1041
|
+
const item = useCanvasState((s) => s.items[itemId]);
|
|
1042
|
+
const kind = useCanvasKind(item?.kind ?? "");
|
|
1043
|
+
const ref = useRef(null);
|
|
1044
|
+
useEffect2(() => {
|
|
1045
|
+
if (active) ref.current?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
1046
|
+
}, [active]);
|
|
1047
|
+
if (!item) return null;
|
|
1048
|
+
const Icon = kind?.icon;
|
|
1049
|
+
const title = itemTitle(item, kind);
|
|
1050
|
+
return /* @__PURE__ */ jsxs2(
|
|
1051
|
+
"div",
|
|
1052
|
+
{
|
|
1053
|
+
ref,
|
|
1054
|
+
className: "mxc-tab",
|
|
1055
|
+
role: "tab",
|
|
1056
|
+
"aria-selected": active,
|
|
1057
|
+
"data-active": active ? "" : void 0,
|
|
1058
|
+
tabIndex: active ? 0 : -1,
|
|
1059
|
+
title,
|
|
1060
|
+
draggable: true,
|
|
1061
|
+
onDragStart: (event) => {
|
|
1062
|
+
event.dataTransfer.setData(DRAG_MIME, itemId);
|
|
1063
|
+
event.dataTransfer.effectAllowed = "move";
|
|
1064
|
+
},
|
|
1065
|
+
onDragOver: (event) => {
|
|
1066
|
+
if (event.dataTransfer.types.includes(DRAG_MIME)) event.preventDefault();
|
|
1067
|
+
},
|
|
1068
|
+
onDrop: (event) => {
|
|
1069
|
+
const dragged = event.dataTransfer.getData(DRAG_MIME);
|
|
1070
|
+
if (!dragged) return;
|
|
1071
|
+
event.preventDefault();
|
|
1072
|
+
event.stopPropagation();
|
|
1073
|
+
if (dragged !== itemId) canvas.moveItem(dragged, paneId, index);
|
|
1074
|
+
},
|
|
1075
|
+
onClick: () => canvas.activate(itemId),
|
|
1076
|
+
onAuxClick: (event) => {
|
|
1077
|
+
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
|
+
]
|
|
1099
|
+
}
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
function PaneMenu({
|
|
1103
|
+
paneId,
|
|
1104
|
+
activeItem,
|
|
1105
|
+
kind,
|
|
1106
|
+
kindProps,
|
|
1107
|
+
canSplitMove
|
|
1108
|
+
}) {
|
|
1109
|
+
const canvas = useCanvas();
|
|
1110
|
+
const ports = useCanvasHostPorts();
|
|
1111
|
+
const moveId = canSplitMove ? activeItem?.id : void 0;
|
|
1112
|
+
const kindItems = kind?.menuItems && kindProps ? safeMenu(() => kind.menuItems?.(kindProps)) : [];
|
|
1113
|
+
const hostItems = activeItem && ports.itemActions ? safeMenu(() => ports.itemActions?.(activeItem, kind)) : [];
|
|
1114
|
+
const itemEntries = [...kindItems, ...hostItems];
|
|
1115
|
+
return /* @__PURE__ */ jsxs2(Menu.Root, { children: [
|
|
1116
|
+
/* @__PURE__ */ jsx3(Menu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx3(TapTargetButtonTransparent, { ariaLabel: "More", icon: /* @__PURE__ */ jsx3(MoreIcon, {}) }) }),
|
|
1117
|
+
/* @__PURE__ */ jsx3(Menu.Portal, { children: /* @__PURE__ */ jsxs2(Menu.Content, { align: "end", sideOffset: 4, className: "mxc-menu", children: [
|
|
1118
|
+
itemEntries.map((entry) => /* @__PURE__ */ jsxs2(
|
|
1119
|
+
Menu.Item,
|
|
1120
|
+
{
|
|
1121
|
+
onSelect: entry.onSelect,
|
|
1122
|
+
className: "mxc-menu-item",
|
|
1123
|
+
"data-destructive": entry.destructive ? "" : void 0,
|
|
1124
|
+
children: [
|
|
1125
|
+
entry.icon,
|
|
1126
|
+
entry.label
|
|
1127
|
+
]
|
|
1128
|
+
},
|
|
1129
|
+
entry.id
|
|
1130
|
+
)),
|
|
1131
|
+
itemEntries.length > 0 ? /* @__PURE__ */ jsx3(Menu.Separator, { className: "mxc-menu-separator" }) : null,
|
|
1132
|
+
/* @__PURE__ */ jsxs2(Menu.Item, { className: "mxc-menu-item", onSelect: () => canvas.splitPane(paneId, "horizontal", moveId), children: [
|
|
1133
|
+
/* @__PURE__ */ jsx3(SplitRightIcon, {}),
|
|
1134
|
+
"Split right"
|
|
1135
|
+
] }),
|
|
1136
|
+
/* @__PURE__ */ jsxs2(Menu.Item, { className: "mxc-menu-item", onSelect: () => canvas.splitPane(paneId, "vertical", moveId), children: [
|
|
1137
|
+
/* @__PURE__ */ jsx3(SplitDownIcon, {}),
|
|
1138
|
+
"Split down"
|
|
1139
|
+
] }),
|
|
1140
|
+
activeItem && ports.popOut ? /* @__PURE__ */ jsxs2(Menu.Item, { className: "mxc-menu-item", onSelect: () => ports.popOut?.(activeItem), children: [
|
|
1141
|
+
/* @__PURE__ */ jsx3(PopOutIcon, {}),
|
|
1142
|
+
"Pop out"
|
|
1143
|
+
] }) : null,
|
|
1144
|
+
activeItem && canSplitMove ? /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1145
|
+
/* @__PURE__ */ jsx3(Menu.Separator, { className: "mxc-menu-separator" }),
|
|
1146
|
+
/* @__PURE__ */ jsx3(Menu.Item, { className: "mxc-menu-item", onSelect: () => canvas.closeOthers(activeItem.id), children: "Close other tabs" })
|
|
1147
|
+
] }) : null
|
|
1148
|
+
] }) })
|
|
1149
|
+
] });
|
|
1150
|
+
}
|
|
1151
|
+
function safeMenu(build) {
|
|
1152
|
+
try {
|
|
1153
|
+
return build() ?? [];
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
console.error("[@ai-matrx/canvas] a menu builder threw", error);
|
|
1156
|
+
return [];
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
function ItemBody({
|
|
1160
|
+
item,
|
|
1161
|
+
paneId,
|
|
1162
|
+
isActive,
|
|
1163
|
+
isFocused
|
|
1164
|
+
}) {
|
|
1165
|
+
const canvas = useCanvas();
|
|
1166
|
+
const kind = useCanvasKind(item.kind);
|
|
1167
|
+
if (!isActive && !kind?.keepAlive) return null;
|
|
1168
|
+
return /* @__PURE__ */ jsx3("div", { className: "mxc-item", hidden: !isActive, "data-kind": item.kind, children: kind ? /* @__PURE__ */ jsx3(ItemBoundary, { item, onClose: () => canvas.close(item.id), onRetry: () => resetKindComponent(kind), children: /* @__PURE__ */ jsx3(Suspense, { fallback: /* @__PURE__ */ jsx3("div", { className: "mxc-loading", "aria-busy": "true" }), children: /* @__PURE__ */ jsx3(KindRender, { kind, props: { item, data: item.data, paneId, isFocused, canvas } }) }) }) : /* @__PURE__ */ jsx3(ItemProblem, { message: "This item can no longer be shown.", onClose: () => canvas.close(item.id) }) });
|
|
1169
|
+
}
|
|
1170
|
+
function KindRender({ kind, props }) {
|
|
1171
|
+
const Render = kindComponent(kind);
|
|
1172
|
+
return /* @__PURE__ */ jsx3(Render, { ...props });
|
|
1173
|
+
}
|
|
1174
|
+
function ItemProblem({ message, onClose, onRetry }) {
|
|
1175
|
+
return /* @__PURE__ */ jsxs2("div", { className: "mxc-problem", role: "alert", children: [
|
|
1176
|
+
/* @__PURE__ */ jsx3(AlertIcon, {}),
|
|
1177
|
+
/* @__PURE__ */ jsx3("p", { children: message }),
|
|
1178
|
+
/* @__PURE__ */ jsxs2("div", { className: "mxc-problem-actions", children: [
|
|
1179
|
+
onRetry ? /* @__PURE__ */ jsx3("button", { type: "button", className: "mxc-text-button", onClick: onRetry, children: "Try again" }) : null,
|
|
1180
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: "mxc-text-button", onClick: onClose, children: "Close tab" })
|
|
1181
|
+
] })
|
|
1182
|
+
] });
|
|
1183
|
+
}
|
|
1184
|
+
var ItemBoundary = class extends Component {
|
|
1185
|
+
state = { error: null };
|
|
1186
|
+
static getDerivedStateFromError(error) {
|
|
1187
|
+
return { error };
|
|
1188
|
+
}
|
|
1189
|
+
componentDidCatch(error, info) {
|
|
1190
|
+
console.error(`[@ai-matrx/canvas] "${this.props.item.kind}" failed to render`, error, info.componentStack);
|
|
1191
|
+
}
|
|
1192
|
+
render() {
|
|
1193
|
+
if (this.state.error) {
|
|
1194
|
+
return /* @__PURE__ */ jsx3(
|
|
1195
|
+
ItemProblem,
|
|
1196
|
+
{
|
|
1197
|
+
message: "This item failed to load.",
|
|
1198
|
+
onClose: this.props.onClose,
|
|
1199
|
+
onRetry: () => {
|
|
1200
|
+
this.props.onRetry();
|
|
1201
|
+
this.setState({ error: null });
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
return this.props.children;
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
function Launcher({ paneId }) {
|
|
1210
|
+
const canvas = useCanvas();
|
|
1211
|
+
const kinds = useCanvasKinds().filter((kind) => kind.launcher);
|
|
1212
|
+
if (kinds.length === 0) return /* @__PURE__ */ jsx3("div", { className: "mxc-empty", children: "Nothing open" });
|
|
1213
|
+
return /* @__PURE__ */ jsx3("div", { className: "mxc-launcher", children: kinds.map((kind) => {
|
|
1214
|
+
const Icon = kind.icon;
|
|
1215
|
+
const launcher = kind.launcher;
|
|
1216
|
+
if (!launcher) return null;
|
|
1217
|
+
return /* @__PURE__ */ jsxs2(
|
|
1218
|
+
"button",
|
|
1219
|
+
{
|
|
1220
|
+
type: "button",
|
|
1221
|
+
className: "mxc-launcher-item",
|
|
1222
|
+
onClick: () => canvas.open({
|
|
1223
|
+
kind: kind.id,
|
|
1224
|
+
key: launcher.key,
|
|
1225
|
+
data: launcher.data,
|
|
1226
|
+
title: launcher.title ?? null,
|
|
1227
|
+
target: { paneId }
|
|
1228
|
+
}),
|
|
1229
|
+
children: [
|
|
1230
|
+
/* @__PURE__ */ jsx3(Icon, { className: "mxc-launcher-icon" }),
|
|
1231
|
+
/* @__PURE__ */ jsx3("span", { children: launcher.title ?? kind.label })
|
|
1232
|
+
]
|
|
1233
|
+
},
|
|
1234
|
+
kind.id
|
|
1235
|
+
);
|
|
1236
|
+
}) });
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// src/react/CanvasColumn.tsx
|
|
1240
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1241
|
+
var MIN_MAIN_WIDTH = 420;
|
|
1242
|
+
function subscribeViewport(listener) {
|
|
1243
|
+
window.addEventListener("resize", listener);
|
|
1244
|
+
return () => window.removeEventListener("resize", listener);
|
|
1245
|
+
}
|
|
1246
|
+
function useViewportWidth() {
|
|
1247
|
+
return useSyncExternalStore2(subscribeViewport, () => window.innerWidth, () => 0);
|
|
1248
|
+
}
|
|
1249
|
+
function fitWidth(width, viewport) {
|
|
1250
|
+
if (viewport <= 0) return width;
|
|
1251
|
+
return Math.max(CANVAS_MIN_WIDTH, Math.min(width, viewport - MIN_MAIN_WIDTH));
|
|
1252
|
+
}
|
|
1253
|
+
function useCanvasColumnWidth() {
|
|
1254
|
+
const isOpen = useCanvasState(selectCanvasIsOpen);
|
|
1255
|
+
const isFullscreen = useCanvasState(selectCanvasIsFullscreen);
|
|
1256
|
+
const width = useCanvasState(selectCanvasWidth);
|
|
1257
|
+
const viewport = useViewportWidth();
|
|
1258
|
+
if (!isOpen) return 0;
|
|
1259
|
+
if (isFullscreen) return null;
|
|
1260
|
+
return fitWidth(width, viewport);
|
|
1261
|
+
}
|
|
1262
|
+
function CanvasColumn({ className, style, onLiveWidth }) {
|
|
1263
|
+
const isOpen = useCanvasState(selectCanvasIsOpen);
|
|
1264
|
+
const isFullscreen = useCanvasState(selectCanvasIsFullscreen);
|
|
1265
|
+
const width = useCanvasState(selectCanvasWidth);
|
|
1266
|
+
const layout = useCanvasState(selectCanvasLayout);
|
|
1267
|
+
const viewport = useViewportWidth();
|
|
1268
|
+
const [dragWidth, setDragWidth] = useState3(null);
|
|
1269
|
+
useRegisterCanvasPresence();
|
|
1270
|
+
if (!isOpen) return null;
|
|
1271
|
+
const fitted = fitWidth(width, viewport);
|
|
1272
|
+
const shown = dragWidth ?? fitted;
|
|
1273
|
+
return /* @__PURE__ */ jsxs3(
|
|
1274
|
+
"aside",
|
|
1275
|
+
{
|
|
1276
|
+
className: ["mxc-column", className].filter(Boolean).join(" "),
|
|
1277
|
+
"data-fullscreen": isFullscreen ? "" : void 0,
|
|
1278
|
+
"aria-label": "Canvas",
|
|
1279
|
+
style: { ...style, ["--mxc-width"]: `${shown}px` },
|
|
1280
|
+
children: [
|
|
1281
|
+
!isFullscreen ? /* @__PURE__ */ jsx4(
|
|
1282
|
+
WidthHandle,
|
|
1283
|
+
{
|
|
1284
|
+
width: fitted,
|
|
1285
|
+
onDrag: (next) => {
|
|
1286
|
+
setDragWidth(next);
|
|
1287
|
+
onLiveWidth?.(next);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
) : null,
|
|
1291
|
+
/* @__PURE__ */ jsx4("div", { className: "mxc-layout", children: /* @__PURE__ */ jsx4(LayoutNodeView, { node: layout }) })
|
|
1292
|
+
]
|
|
1293
|
+
}
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
function WidthHandle({ width, onDrag }) {
|
|
1297
|
+
const canvas = useCanvas();
|
|
1298
|
+
const start = useRef2(null);
|
|
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
|
+
};
|
|
1303
|
+
const onPointerDown = (event) => {
|
|
1304
|
+
event.preventDefault();
|
|
1305
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
1306
|
+
start.current = { x: event.clientX, width };
|
|
1307
|
+
document.documentElement.dataset.mxcResizing = "";
|
|
1308
|
+
};
|
|
1309
|
+
const onPointerMove = (event) => {
|
|
1310
|
+
if (!start.current) return;
|
|
1311
|
+
onDrag(clamp(start.current.width + (start.current.x - event.clientX)));
|
|
1312
|
+
};
|
|
1313
|
+
const finish = (event) => {
|
|
1314
|
+
if (!start.current) return;
|
|
1315
|
+
const next = clamp(start.current.width + (start.current.x - event.clientX));
|
|
1316
|
+
start.current = null;
|
|
1317
|
+
delete document.documentElement.dataset.mxcResizing;
|
|
1318
|
+
onDrag(null);
|
|
1319
|
+
canvas.setWidth(next);
|
|
1320
|
+
};
|
|
1321
|
+
return /* @__PURE__ */ jsx4(
|
|
1322
|
+
"div",
|
|
1323
|
+
{
|
|
1324
|
+
className: "mxc-width-handle",
|
|
1325
|
+
role: "separator",
|
|
1326
|
+
"aria-orientation": "vertical",
|
|
1327
|
+
"aria-label": "Resize canvas",
|
|
1328
|
+
"aria-valuenow": width,
|
|
1329
|
+
tabIndex: 0,
|
|
1330
|
+
onPointerDown,
|
|
1331
|
+
onPointerMove,
|
|
1332
|
+
onPointerUp: finish,
|
|
1333
|
+
onPointerCancel: finish,
|
|
1334
|
+
onDoubleClick: () => canvas.setWidth(Math.round(window.innerWidth / 2)),
|
|
1335
|
+
onKeyDown: (event) => {
|
|
1336
|
+
if (event.key === "ArrowLeft") canvas.setWidth(clamp(width + 32));
|
|
1337
|
+
if (event.key === "ArrowRight") canvas.setWidth(clamp(width - 32));
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
function LayoutNodeView({ node }) {
|
|
1343
|
+
if (node.type === "pane") return /* @__PURE__ */ jsx4(CanvasPaneView, { paneId: node.paneId });
|
|
1344
|
+
return /* @__PURE__ */ jsx4(SplitView, { node });
|
|
1345
|
+
}
|
|
1346
|
+
function SplitView({ node }) {
|
|
1347
|
+
const canvas = useCanvas();
|
|
1348
|
+
const ref = useRef2(null);
|
|
1349
|
+
const [liveSizes, setLiveSizes] = useState3(null);
|
|
1350
|
+
const sizes = liveSizes ?? node.sizes;
|
|
1351
|
+
const horizontal = node.orientation === "horizontal";
|
|
1352
|
+
const beginDrag = (index) => (event) => {
|
|
1353
|
+
const el = ref.current;
|
|
1354
|
+
if (!el) return;
|
|
1355
|
+
event.preventDefault();
|
|
1356
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
1357
|
+
const rect = el.getBoundingClientRect();
|
|
1358
|
+
const total = horizontal ? rect.width : rect.height;
|
|
1359
|
+
const origin = horizontal ? event.clientX : event.clientY;
|
|
1360
|
+
const base = [...node.sizes];
|
|
1361
|
+
const pair = (base[index] ?? 0) + (base[index + 1] ?? 0);
|
|
1362
|
+
const target = event.currentTarget;
|
|
1363
|
+
document.documentElement.dataset.mxcResizing = "";
|
|
1364
|
+
const compute = (pos) => {
|
|
1365
|
+
const delta = (pos - origin) / total;
|
|
1366
|
+
const min = 0.12;
|
|
1367
|
+
const first = Math.min(pair - min, Math.max(min, (base[index] ?? 0) + delta));
|
|
1368
|
+
const next = [...base];
|
|
1369
|
+
next[index] = first;
|
|
1370
|
+
next[index + 1] = pair - first;
|
|
1371
|
+
return next;
|
|
1372
|
+
};
|
|
1373
|
+
const move = (e) => setLiveSizes(compute(horizontal ? e.clientX : e.clientY));
|
|
1374
|
+
const up = (e) => {
|
|
1375
|
+
target.removeEventListener("pointermove", move);
|
|
1376
|
+
target.removeEventListener("pointerup", up);
|
|
1377
|
+
target.removeEventListener("pointercancel", up);
|
|
1378
|
+
delete document.documentElement.dataset.mxcResizing;
|
|
1379
|
+
setLiveSizes(null);
|
|
1380
|
+
canvas.resizeSplit(node.id, compute(horizontal ? e.clientX : e.clientY));
|
|
1381
|
+
};
|
|
1382
|
+
target.addEventListener("pointermove", move);
|
|
1383
|
+
target.addEventListener("pointerup", up);
|
|
1384
|
+
target.addEventListener("pointercancel", up);
|
|
1385
|
+
};
|
|
1386
|
+
return /* @__PURE__ */ jsx4("div", { ref, className: "mxc-split", "data-orientation": node.orientation, children: node.children.map((child, i) => /* @__PURE__ */ jsx4(
|
|
1387
|
+
SplitChild,
|
|
1388
|
+
{
|
|
1389
|
+
size: sizes[i] ?? 1 / node.children.length,
|
|
1390
|
+
handle: i < node.children.length - 1 ? /* @__PURE__ */ jsx4(
|
|
1391
|
+
"div",
|
|
1392
|
+
{
|
|
1393
|
+
className: "mxc-split-handle",
|
|
1394
|
+
role: "separator",
|
|
1395
|
+
"aria-orientation": horizontal ? "vertical" : "horizontal",
|
|
1396
|
+
onPointerDown: beginDrag(i),
|
|
1397
|
+
onDoubleClick: () => canvas.resizeSplit(node.id, node.children.map(() => 1 / node.children.length))
|
|
1398
|
+
}
|
|
1399
|
+
) : null,
|
|
1400
|
+
children: /* @__PURE__ */ jsx4(LayoutNodeView, { node: child })
|
|
1401
|
+
},
|
|
1402
|
+
child.type === "pane" ? child.paneId : child.id
|
|
1403
|
+
)) });
|
|
1404
|
+
}
|
|
1405
|
+
function SplitChild({ size, handle, children }) {
|
|
1406
|
+
return /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
1407
|
+
/* @__PURE__ */ jsx4("div", { className: "mxc-split-child", style: { flexGrow: size, flexBasis: 0 }, children }),
|
|
1408
|
+
handle
|
|
1409
|
+
] });
|
|
1410
|
+
}
|
|
1411
|
+
function CanvasToggle({ className }) {
|
|
1412
|
+
const canvas = useCanvas();
|
|
1413
|
+
const isOpen = useCanvasState(selectCanvasIsOpen);
|
|
1414
|
+
const count = useCanvasState(selectCanvasItemCount);
|
|
1415
|
+
return /* @__PURE__ */ jsxs3("span", { className: ["mxc-toggle", className].filter(Boolean).join(" "), "data-open": isOpen ? "" : void 0, children: [
|
|
1416
|
+
/* @__PURE__ */ jsx4(
|
|
1417
|
+
TapTargetButtonTransparent2,
|
|
1418
|
+
{
|
|
1419
|
+
ariaLabel: isOpen ? "Hide canvas" : "Show canvas",
|
|
1420
|
+
tooltip: isOpen ? "Hide canvas (\u2318\\)" : "Show canvas (\u2318\\)",
|
|
1421
|
+
icon: /* @__PURE__ */ jsx4(PanelRightIcon, {}),
|
|
1422
|
+
onClick: () => canvas.toggle()
|
|
1423
|
+
}
|
|
1424
|
+
),
|
|
1425
|
+
count > 0 && !isOpen ? /* @__PURE__ */ jsx4("span", { className: "mxc-toggle-badge", children: count }) : null
|
|
1426
|
+
] });
|
|
1427
|
+
}
|
|
1428
|
+
function CanvasFrame({ children, className }) {
|
|
1429
|
+
const isFullscreen = useCanvasState(selectCanvasIsFullscreen);
|
|
1430
|
+
const isOpen = useCanvasState(selectCanvasIsOpen);
|
|
1431
|
+
return /* @__PURE__ */ jsxs3("div", { className: ["mxc-frame", className].filter(Boolean).join(" "), "data-canvas-open": isOpen ? "" : void 0, children: [
|
|
1432
|
+
/* @__PURE__ */ jsx4("div", { className: "mxc-frame-main", hidden: isOpen && isFullscreen, children }),
|
|
1433
|
+
/* @__PURE__ */ jsx4(CanvasColumn, {})
|
|
1434
|
+
] });
|
|
1435
|
+
}
|
|
1436
|
+
export {
|
|
1437
|
+
CanvasColumn,
|
|
1438
|
+
CanvasFrame,
|
|
1439
|
+
CanvasPaneView,
|
|
1440
|
+
CanvasProvider,
|
|
1441
|
+
CanvasToggle,
|
|
1442
|
+
defineCanvasKind,
|
|
1443
|
+
getCanvasKind,
|
|
1444
|
+
itemTitle,
|
|
1445
|
+
listCanvasKinds,
|
|
1446
|
+
registerCanvasKind,
|
|
1447
|
+
registerCanvasKinds,
|
|
1448
|
+
useCanvas,
|
|
1449
|
+
useCanvasColumnWidth,
|
|
1450
|
+
useCanvasHostPorts,
|
|
1451
|
+
useCanvasIsPresented,
|
|
1452
|
+
useCanvasKind,
|
|
1453
|
+
useCanvasKinds,
|
|
1454
|
+
useCanvasState,
|
|
1455
|
+
useOptionalCanvas,
|
|
1456
|
+
useOptionalCanvasState
|
|
1457
|
+
};
|
|
1458
|
+
//# sourceMappingURL=react.js.map
|