@ai-matrx/canvas 0.0.0-bootstrap.0 → 0.1.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/dist/index.cjs ADDED
@@ -0,0 +1,758 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ CANVAS_DEFAULT_WIDTH: () => CANVAS_DEFAULT_WIDTH,
24
+ CANVAS_MIN_SPLIT_FRACTION: () => CANVAS_MIN_SPLIT_FRACTION,
25
+ CANVAS_MIN_WIDTH: () => CANVAS_MIN_WIDTH,
26
+ CANVAS_STORAGE_KEY: () => CANVAS_STORAGE_KEY,
27
+ bindCanvasToReduxStore: () => bindCanvasToReduxStore,
28
+ canvasActions: () => canvasActions,
29
+ canvasItemId: () => canvasItemId,
30
+ canvasReducer: () => canvasReducer,
31
+ consoleCanvasErrorSink: () => consoleCanvasErrorSink,
32
+ createCanvasController: () => createCanvasController,
33
+ createCanvasStore: () => createCanvasStore,
34
+ createInitialCanvasState: () => createInitialCanvasState,
35
+ createLocalStorageCanvasPersistence: () => createLocalStorageCanvasPersistence,
36
+ findNonJson: () => findNonJson,
37
+ isCanvasAction: () => isCanvasAction,
38
+ isCanvasJson: () => isCanvasJson,
39
+ listPaneIds: () => listPaneIds,
40
+ normalizeSizes: () => normalizeSizes,
41
+ parseCanvasItemId: () => parseCanvasItemId,
42
+ sanitizeCanvasSnapshot: () => sanitizeCanvasSnapshot,
43
+ selectCanvasActiveItem: () => selectCanvasActiveItem,
44
+ selectCanvasFocusedPaneId: () => selectCanvasFocusedPaneId,
45
+ selectCanvasIsFullscreen: () => selectCanvasIsFullscreen,
46
+ selectCanvasIsHydrated: () => selectCanvasIsHydrated,
47
+ selectCanvasIsOpen: () => selectCanvasIsOpen,
48
+ selectCanvasItem: () => selectCanvasItem,
49
+ selectCanvasItemCount: () => selectCanvasItemCount,
50
+ selectCanvasLayout: () => selectCanvasLayout,
51
+ selectCanvasPane: () => selectCanvasPane,
52
+ selectCanvasPaneCount: () => selectCanvasPaneCount,
53
+ selectCanvasWidth: () => selectCanvasWidth,
54
+ toPersistableSnapshot: () => toPersistableSnapshot
55
+ });
56
+ module.exports = __toCommonJS(src_exports);
57
+
58
+ // src/core/types.ts
59
+ var CANVAS_MIN_WIDTH = 360;
60
+ var CANVAS_DEFAULT_WIDTH = 640;
61
+ var CANVAS_MIN_SPLIT_FRACTION = 0.12;
62
+
63
+ // src/core/ids.ts
64
+ var SEP = "::";
65
+ function canvasItemId(kind, key) {
66
+ return `${kind}${SEP}${key}`;
67
+ }
68
+ function parseCanvasItemId(id) {
69
+ const at = id.indexOf(SEP);
70
+ return at < 0 ? { kind: id, key: "" } : { kind: id.slice(0, at), key: id.slice(at + SEP.length) };
71
+ }
72
+ function paneIdFromSeq(seq) {
73
+ return `pane-${seq}`;
74
+ }
75
+ function splitIdFromSeq(seq) {
76
+ return `split-${seq}`;
77
+ }
78
+ function findNonJson(value, path = "data") {
79
+ if (value === null) return null;
80
+ switch (typeof value) {
81
+ case "string":
82
+ case "boolean":
83
+ return null;
84
+ case "number":
85
+ return Number.isFinite(value) ? null : path;
86
+ case "object": {
87
+ if (Array.isArray(value)) {
88
+ for (let i = 0; i < value.length; i++) {
89
+ const bad = findNonJson(value[i], `${path}[${i}]`);
90
+ if (bad) return bad;
91
+ }
92
+ return null;
93
+ }
94
+ const proto = Object.getPrototypeOf(value);
95
+ if (proto !== Object.prototype && proto !== null) return path;
96
+ for (const [k, v] of Object.entries(value)) {
97
+ if (v === void 0) continue;
98
+ const bad = findNonJson(v, `${path}.${k}`);
99
+ if (bad) return bad;
100
+ }
101
+ return null;
102
+ }
103
+ default:
104
+ return path;
105
+ }
106
+ }
107
+ function isCanvasJson(value) {
108
+ return findNonJson(value) === null;
109
+ }
110
+
111
+ // src/core/layout.ts
112
+ function listPaneIds(node) {
113
+ if (node.type === "pane") return [node.paneId];
114
+ return node.children.flatMap(listPaneIds);
115
+ }
116
+ function evenSizes(count) {
117
+ return Array.from({ length: count }, () => 1 / count);
118
+ }
119
+ function normalizeSizes(sizes, count) {
120
+ if (sizes.length !== count || sizes.some((s) => !Number.isFinite(s) || s <= 0)) {
121
+ return evenSizes(count);
122
+ }
123
+ const total = sizes.reduce((sum, s) => sum + s, 0);
124
+ return sizes.map((s) => s / total);
125
+ }
126
+ function splitPane(node, paneId, newPaneId, orientation, newSplitId) {
127
+ if (node.type === "pane") {
128
+ if (node.paneId !== paneId) return node;
129
+ return {
130
+ type: "split",
131
+ id: newSplitId,
132
+ orientation,
133
+ children: [node, { type: "pane", paneId: newPaneId }],
134
+ sizes: [0.5, 0.5]
135
+ };
136
+ }
137
+ const index = node.children.findIndex(
138
+ (child) => child.type === "pane" && child.paneId === paneId
139
+ );
140
+ if (index >= 0 && node.orientation === orientation) {
141
+ const share = node.sizes[index] ?? 1 / node.children.length;
142
+ const children = [...node.children];
143
+ children.splice(index + 1, 0, { type: "pane", paneId: newPaneId });
144
+ const sizes = [...node.sizes];
145
+ sizes.splice(index, 1, share / 2, share / 2);
146
+ return { ...node, children, sizes: normalizeSizes(sizes, children.length) };
147
+ }
148
+ return {
149
+ ...node,
150
+ children: node.children.map(
151
+ (child) => splitPane(child, paneId, newPaneId, orientation, newSplitId)
152
+ )
153
+ };
154
+ }
155
+ function removePane(node, paneId) {
156
+ if (node.type === "pane") return node.paneId === paneId ? null : node;
157
+ const children = [];
158
+ const sizes = [];
159
+ node.children.forEach((child, i) => {
160
+ const next = removePane(child, paneId);
161
+ if (next) {
162
+ children.push(next);
163
+ sizes.push(node.sizes[i] ?? 1 / node.children.length);
164
+ }
165
+ });
166
+ if (children.length === 0) return null;
167
+ if (children.length === 1) return children[0] ?? null;
168
+ return { ...node, children, sizes: normalizeSizes(sizes, children.length) };
169
+ }
170
+ function resizeSplit(node, splitId, sizes) {
171
+ if (node.type === "pane") return node;
172
+ if (node.id === splitId) {
173
+ const clamped = sizes.map((s) => Math.max(CANVAS_MIN_SPLIT_FRACTION, s));
174
+ return { ...node, sizes: normalizeSizes(clamped, node.children.length) };
175
+ }
176
+ return {
177
+ ...node,
178
+ children: node.children.map((child) => resizeSplit(child, splitId, sizes))
179
+ };
180
+ }
181
+ function isValidLayout(node, knownPanes, seen = /* @__PURE__ */ new Set()) {
182
+ if (!node || typeof node !== "object") return false;
183
+ const n = node;
184
+ if (n.type === "pane") {
185
+ const id = n.paneId;
186
+ if (typeof id !== "string" || !knownPanes.has(id) || seen.has(id)) return false;
187
+ seen.add(id);
188
+ return true;
189
+ }
190
+ if (n.type !== "split") return false;
191
+ if (typeof n.id !== "string") return false;
192
+ if (n.orientation !== "horizontal" && n.orientation !== "vertical") return false;
193
+ if (!Array.isArray(n.children) || n.children.length < 2) return false;
194
+ if (!Array.isArray(n.sizes) || n.sizes.length !== n.children.length) return false;
195
+ return n.children.every((child) => isValidLayout(child, knownPanes, seen));
196
+ }
197
+
198
+ // src/core/reducer.ts
199
+ var P = "matrxCanvas/";
200
+ var canvasActions = {
201
+ open: (input) => ({ type: `${P}open`, payload: { ...input, now: Date.now() } }),
202
+ update: (itemId, patch) => ({
203
+ type: `${P}update`,
204
+ payload: { itemId, ...patch, now: Date.now() }
205
+ }),
206
+ /** Gives an item a new identity in place (e.g. a draft that was just saved and now has an id). */
207
+ rekey: (itemId, key) => ({ type: `${P}rekey`, payload: { itemId, key } }),
208
+ closeItem: (itemId) => ({ type: `${P}closeItem`, payload: { itemId } }),
209
+ closeOthers: (itemId) => ({ type: `${P}closeOthers`, payload: { itemId } }),
210
+ activate: (itemId) => ({ type: `${P}activate`, payload: { itemId } }),
211
+ focusPane: (paneId) => ({ type: `${P}focusPane`, payload: { paneId } }),
212
+ moveItem: (itemId, toPaneId, index) => ({
213
+ type: `${P}moveItem`,
214
+ payload: { itemId, toPaneId, index }
215
+ }),
216
+ splitPane: (paneId, orientation, moveItemId) => ({
217
+ type: `${P}splitPane`,
218
+ payload: { paneId, orientation, moveItemId }
219
+ }),
220
+ closePane: (paneId) => ({ type: `${P}closePane`, payload: { paneId } }),
221
+ resizeSplit: (splitId, sizes) => ({
222
+ type: `${P}resizeSplit`,
223
+ payload: { splitId, sizes }
224
+ }),
225
+ setOpen: (open) => ({ type: `${P}setOpen`, payload: { open } }),
226
+ toggle: () => ({ type: `${P}toggle` }),
227
+ setFullscreen: (fullscreen) => ({ type: `${P}setFullscreen`, payload: { fullscreen } }),
228
+ setWidth: (width) => ({ type: `${P}setWidth`, payload: { width } }),
229
+ hydrate: (snapshot) => ({ type: `${P}hydrate`, payload: { snapshot } }),
230
+ reset: () => ({ type: `${P}reset` })
231
+ };
232
+ function isCanvasAction(action) {
233
+ return !!action && typeof action === "object" && typeof action.type === "string" && action.type.startsWith(P);
234
+ }
235
+ function createInitialCanvasState() {
236
+ const paneId = paneIdFromSeq(1);
237
+ return {
238
+ version: 1,
239
+ isOpen: false,
240
+ isFullscreen: false,
241
+ width: CANVAS_DEFAULT_WIDTH,
242
+ layout: { type: "pane", paneId },
243
+ panes: { [paneId]: { id: paneId, itemIds: [], activeItemId: null } },
244
+ items: {},
245
+ focusedPaneId: paneId,
246
+ seq: 1,
247
+ hydrated: false
248
+ };
249
+ }
250
+ function paneOf(state, itemId) {
251
+ for (const pane of Object.values(state.panes)) {
252
+ if (pane.itemIds.includes(itemId)) return pane;
253
+ }
254
+ return null;
255
+ }
256
+ function withPane(state, pane) {
257
+ return { ...state, panes: { ...state.panes, [pane.id]: pane } };
258
+ }
259
+ function nextActive(itemIds, removed, current) {
260
+ if (current !== removed) return current;
261
+ const index = itemIds.indexOf(removed);
262
+ const rest = itemIds.filter((id) => id !== removed);
263
+ return rest[Math.min(index, rest.length - 1)] ?? null;
264
+ }
265
+ function detachItem(state, itemId) {
266
+ const pane = paneOf(state, itemId);
267
+ if (!pane) return state;
268
+ return withPane(state, {
269
+ ...pane,
270
+ itemIds: pane.itemIds.filter((id) => id !== itemId),
271
+ activeItemId: nextActive(pane.itemIds, itemId, pane.activeItemId)
272
+ });
273
+ }
274
+ function dropPane(state, paneId) {
275
+ const layout = removePane(state.layout, paneId);
276
+ if (!layout) return state;
277
+ const panes = { ...state.panes };
278
+ delete panes[paneId];
279
+ const focusedPaneId = state.focusedPaneId === paneId ? listPaneIds(layout)[0] ?? state.focusedPaneId : state.focusedPaneId;
280
+ return { ...state, layout, panes, focusedPaneId };
281
+ }
282
+ function addSplit(state, paneId, orientation) {
283
+ const seq = state.seq + 1;
284
+ const newPaneId = paneIdFromSeq(seq);
285
+ const layout = splitPane(state.layout, paneId, newPaneId, orientation, splitIdFromSeq(seq));
286
+ return {
287
+ newPaneId,
288
+ state: {
289
+ ...state,
290
+ seq,
291
+ layout,
292
+ panes: { ...state.panes, [newPaneId]: { id: newPaneId, itemIds: [], activeItemId: null } },
293
+ focusedPaneId: newPaneId
294
+ }
295
+ };
296
+ }
297
+ function insertInto(state, paneId, itemId, activate, index) {
298
+ const pane = state.panes[paneId];
299
+ if (!pane) return state;
300
+ const itemIds = [...pane.itemIds.filter((id) => id !== itemId)];
301
+ const at = index === void 0 ? afterActive(itemIds, pane.activeItemId) : Math.max(0, Math.min(index, itemIds.length));
302
+ itemIds.splice(at, 0, itemId);
303
+ return withPane(state, {
304
+ ...pane,
305
+ itemIds,
306
+ activeItemId: activate || pane.activeItemId === null ? itemId : pane.activeItemId
307
+ });
308
+ }
309
+ function afterActive(itemIds, active) {
310
+ const index = active ? itemIds.indexOf(active) : -1;
311
+ return index < 0 ? itemIds.length : index + 1;
312
+ }
313
+ function clampWidth(width) {
314
+ return Number.isFinite(width) ? Math.max(CANVAS_MIN_WIDTH, Math.round(width)) : CANVAS_DEFAULT_WIDTH;
315
+ }
316
+ function sanitizeCanvasSnapshot(raw) {
317
+ if (!raw || typeof raw !== "object") return null;
318
+ const s = raw;
319
+ if (s.version !== 1 || !s.panes || !s.items || !s.layout) return null;
320
+ const paneIds = new Set(Object.keys(s.panes));
321
+ if (!isValidLayout(s.layout, paneIds)) return null;
322
+ const layoutPanes = new Set(listPaneIds(s.layout));
323
+ if (layoutPanes.size !== paneIds.size) return null;
324
+ const items = {};
325
+ for (const [id, item] of Object.entries(s.items)) {
326
+ if (item && typeof item.kind === "string" && typeof item.key === "string" && canvasItemId(item.kind, item.key) === id) {
327
+ items[id] = item;
328
+ }
329
+ }
330
+ const panes = {};
331
+ for (const [id, pane] of Object.entries(s.panes)) {
332
+ const itemIds = (Array.isArray(pane.itemIds) ? pane.itemIds : []).filter((itemId) => itemId in items);
333
+ const activeItemId = pane.activeItemId && itemIds.includes(pane.activeItemId) ? pane.activeItemId : itemIds[0] ?? null;
334
+ panes[id] = { id, itemIds, activeItemId };
335
+ }
336
+ const focused = s.focusedPaneId && paneIds.has(s.focusedPaneId) ? s.focusedPaneId : listPaneIds(s.layout)[0];
337
+ if (!focused) return null;
338
+ const layout = normalizeLayout(s.layout);
339
+ const seq = Math.max(typeof s.seq === "number" && Number.isFinite(s.seq) ? s.seq : 0, highestIdNumber(layout));
340
+ return {
341
+ version: 1,
342
+ isOpen: s.isOpen === true,
343
+ // Full screen is a moment, not a preference: a reload never starts there.
344
+ isFullscreen: false,
345
+ width: clampWidth(typeof s.width === "number" ? s.width : CANVAS_DEFAULT_WIDTH),
346
+ layout,
347
+ panes,
348
+ items,
349
+ focusedPaneId: focused,
350
+ seq,
351
+ hydrated: true
352
+ };
353
+ }
354
+ function normalizeLayout(node) {
355
+ if (node.type === "pane") return node;
356
+ return { ...node, sizes: normalizeSizes(node.sizes, node.children.length), children: node.children.map(normalizeLayout) };
357
+ }
358
+ function highestIdNumber(node) {
359
+ const own = Number((node.type === "pane" ? node.paneId : node.id).split("-").pop());
360
+ const mine = Number.isFinite(own) ? own : 0;
361
+ return node.type === "pane" ? mine : Math.max(mine, ...node.children.map(highestIdNumber));
362
+ }
363
+ function canvasReducer(state = createInitialCanvasState(), action) {
364
+ if (!isCanvasAction(action)) return state;
365
+ switch (action.type) {
366
+ case `${P}open`: {
367
+ const { kind, key, title, data, target = "focused", reveal = true, activate = true, now } = action.payload;
368
+ const itemId = canvasItemId(kind, key);
369
+ const existing = state.items[itemId];
370
+ let next = state;
371
+ if (existing) {
372
+ const item = {
373
+ ...existing,
374
+ ...data !== void 0 ? { data } : {},
375
+ ...title !== void 0 ? { title } : {},
376
+ updatedAt: now
377
+ };
378
+ next = { ...next, items: { ...next.items, [itemId]: item } };
379
+ const pane = paneOf(next, itemId);
380
+ if (pane && activate) next = { ...withPane(next, { ...pane, activeItemId: itemId }), focusedPaneId: pane.id };
381
+ if (!pane) next = insertInto(next, next.focusedPaneId, itemId, true);
382
+ } else {
383
+ const item = { id: itemId, kind, key, title: title ?? null, data: data ?? null, openedAt: now, updatedAt: now };
384
+ next = { ...next, items: { ...next.items, [itemId]: item } };
385
+ let paneId = next.focusedPaneId;
386
+ if (typeof target === "object" && next.panes[target.paneId]) {
387
+ paneId = target.paneId;
388
+ } else if (target === "split-right" || target === "split-down") {
389
+ const split = addSplit(next, next.focusedPaneId, target === "split-right" ? "horizontal" : "vertical");
390
+ next = split.state;
391
+ paneId = split.newPaneId;
392
+ }
393
+ next = insertInto(next, paneId, itemId, activate);
394
+ if (activate) next = { ...next, focusedPaneId: paneId };
395
+ }
396
+ return reveal ? { ...next, isOpen: true } : next;
397
+ }
398
+ case `${P}update`: {
399
+ const { itemId, data, title, now } = action.payload;
400
+ const item = state.items[itemId];
401
+ if (!item) return state;
402
+ return {
403
+ ...state,
404
+ items: {
405
+ ...state.items,
406
+ [itemId]: { ...item, ...data !== void 0 ? { data } : {}, ...title !== void 0 ? { title } : {}, updatedAt: now }
407
+ }
408
+ };
409
+ }
410
+ case `${P}rekey`: {
411
+ const { itemId, key } = action.payload;
412
+ const item = state.items[itemId];
413
+ if (!item || item.key === key) return state;
414
+ const nextId = canvasItemId(item.kind, key);
415
+ if (state.items[nextId]) return canvasReducer(canvasReducer(state, canvasActions.closeItem(itemId)), canvasActions.activate(nextId));
416
+ const items = { ...state.items, [nextId]: { ...item, id: nextId, key } };
417
+ delete items[itemId];
418
+ const panes = {};
419
+ for (const pane of Object.values(state.panes)) {
420
+ panes[pane.id] = pane.itemIds.includes(itemId) ? {
421
+ ...pane,
422
+ itemIds: pane.itemIds.map((id) => id === itemId ? nextId : id),
423
+ activeItemId: pane.activeItemId === itemId ? nextId : pane.activeItemId
424
+ } : pane;
425
+ }
426
+ return { ...state, items, panes };
427
+ }
428
+ case `${P}closeItem`: {
429
+ const { itemId } = action.payload;
430
+ if (!state.items[itemId]) return state;
431
+ const pane = paneOf(state, itemId);
432
+ let next = detachItem(state, itemId);
433
+ const items = { ...next.items };
434
+ delete items[itemId];
435
+ next = { ...next, items };
436
+ if (pane && next.panes[pane.id]?.itemIds.length === 0) next = dropPane(next, pane.id);
437
+ return next;
438
+ }
439
+ case `${P}closeOthers`: {
440
+ const { itemId } = action.payload;
441
+ const pane = paneOf(state, itemId);
442
+ if (!pane) return state;
443
+ const items = { ...state.items };
444
+ for (const id of pane.itemIds) if (id !== itemId) delete items[id];
445
+ return { ...withPane(state, { ...pane, itemIds: [itemId], activeItemId: itemId }), items };
446
+ }
447
+ case `${P}activate`: {
448
+ const pane = paneOf(state, action.payload.itemId);
449
+ if (!pane) return state;
450
+ return { ...withPane(state, { ...pane, activeItemId: action.payload.itemId }), focusedPaneId: pane.id };
451
+ }
452
+ case `${P}focusPane`:
453
+ return state.panes[action.payload.paneId] ? { ...state, focusedPaneId: action.payload.paneId } : state;
454
+ case `${P}moveItem`: {
455
+ const { itemId, toPaneId, index } = action.payload;
456
+ const from = paneOf(state, itemId);
457
+ if (!from || !state.panes[toPaneId]) return state;
458
+ if (from.id === toPaneId && index === void 0) return state.focusedPaneId === toPaneId ? state : { ...state, focusedPaneId: toPaneId };
459
+ let next = from.id === toPaneId ? state : detachItem(state, itemId);
460
+ next = insertInto(next, toPaneId, itemId, true, index);
461
+ next = { ...next, focusedPaneId: toPaneId };
462
+ if (from.id !== toPaneId && next.panes[from.id]?.itemIds.length === 0) next = dropPane(next, from.id);
463
+ return next;
464
+ }
465
+ case `${P}splitPane`: {
466
+ const { paneId, orientation, moveItemId } = action.payload;
467
+ const pane = state.panes[paneId];
468
+ if (!pane) return state;
469
+ const split = addSplit(state, paneId, orientation);
470
+ let next = split.state;
471
+ if (moveItemId && pane.itemIds.includes(moveItemId) && pane.itemIds.length > 1) {
472
+ next = detachItem(next, moveItemId);
473
+ next = insertInto(next, split.newPaneId, moveItemId, true);
474
+ }
475
+ return next;
476
+ }
477
+ case `${P}closePane`: {
478
+ const pane = state.panes[action.payload.paneId];
479
+ if (!pane) return state;
480
+ const onlyPane = listPaneIds(state.layout).length === 1;
481
+ if (onlyPane) return { ...state, isOpen: false, isFullscreen: false };
482
+ const items = { ...state.items };
483
+ for (const id of pane.itemIds) delete items[id];
484
+ return dropPane({ ...state, items }, pane.id);
485
+ }
486
+ case `${P}resizeSplit`:
487
+ return { ...state, layout: resizeSplit(state.layout, action.payload.splitId, action.payload.sizes) };
488
+ case `${P}setOpen`:
489
+ return action.payload.open === state.isOpen ? state : { ...state, isOpen: action.payload.open, isFullscreen: action.payload.open ? state.isFullscreen : false };
490
+ case `${P}toggle`:
491
+ return { ...state, isOpen: !state.isOpen, isFullscreen: state.isOpen ? false : state.isFullscreen };
492
+ case `${P}setFullscreen`:
493
+ return { ...state, isFullscreen: action.payload.fullscreen, isOpen: action.payload.fullscreen ? true : state.isOpen };
494
+ case `${P}setWidth`:
495
+ return { ...state, width: clampWidth(action.payload.width) };
496
+ case `${P}hydrate`: {
497
+ const snapshot = action.payload.snapshot ? sanitizeCanvasSnapshot(action.payload.snapshot) : null;
498
+ if (!snapshot) return { ...state, hydrated: true };
499
+ let next = snapshot;
500
+ for (const item of Object.values(state.items)) {
501
+ const pane = paneOf(state, item.id);
502
+ next = { ...next, items: { ...next.items, [item.id]: item } };
503
+ if (!paneOf(next, item.id)) next = insertInto(next, next.focusedPaneId, item.id, pane?.activeItemId === item.id);
504
+ }
505
+ return { ...next, isOpen: next.isOpen || state.isOpen, hydrated: true };
506
+ }
507
+ case `${P}reset`:
508
+ return { ...createInitialCanvasState(), hydrated: true };
509
+ default:
510
+ return state;
511
+ }
512
+ }
513
+
514
+ // src/core/store.ts
515
+ function createCanvasStore(initial) {
516
+ let state = initial ?? createInitialCanvasState();
517
+ const listeners = /* @__PURE__ */ new Set();
518
+ return {
519
+ getState: () => state,
520
+ dispatch(action) {
521
+ const next = canvasReducer(state, action);
522
+ if (next === state) return;
523
+ state = next;
524
+ for (const listener of [...listeners]) listener();
525
+ },
526
+ subscribe(listener) {
527
+ listeners.add(listener);
528
+ return () => listeners.delete(listener);
529
+ }
530
+ };
531
+ }
532
+ function bindCanvasToReduxStore(store, select) {
533
+ return {
534
+ getState: () => select(store.getState()),
535
+ dispatch: (action) => {
536
+ store.dispatch(action);
537
+ },
538
+ subscribe: (listener) => store.subscribe(listener)
539
+ };
540
+ }
541
+
542
+ // src/core/persistence.ts
543
+ var CANVAS_STORAGE_KEY = "ai-matrx.canvas.v1";
544
+ function createLocalStorageCanvasPersistence(key = CANVAS_STORAGE_KEY, storage = typeof window === "undefined" ? null : safeLocalStorage()) {
545
+ return {
546
+ load() {
547
+ if (!storage) throw new Error("Canvas storage is unavailable; allow browser storage or provide a persistence port.");
548
+ const raw = storage.getItem(key);
549
+ if (!raw) return null;
550
+ return JSON.parse(raw);
551
+ },
552
+ save(snapshot) {
553
+ if (!storage) throw new Error("Canvas storage is unavailable; allow browser storage or provide a persistence port.");
554
+ storage.setItem(key, JSON.stringify(snapshot));
555
+ }
556
+ };
557
+ }
558
+ function safeLocalStorage() {
559
+ try {
560
+ const probe = "__mxc_probe__";
561
+ window.localStorage.setItem(probe, "1");
562
+ window.localStorage.removeItem(probe);
563
+ return window.localStorage;
564
+ } catch {
565
+ return null;
566
+ }
567
+ }
568
+ function toPersistableSnapshot(state, isRestorable) {
569
+ const keep = new Set(
570
+ Object.values(state.items).filter((item) => isRestorable(item.kind)).map((item) => item.id)
571
+ );
572
+ if (keep.size === Object.keys(state.items).length) return state;
573
+ const items = Object.fromEntries(Object.entries(state.items).filter(([id]) => keep.has(id)));
574
+ let layout = state.layout;
575
+ const panes = {};
576
+ for (const pane of Object.values(state.panes)) {
577
+ const itemIds = pane.itemIds.filter((id) => keep.has(id));
578
+ if (itemIds.length === 0 && pane.itemIds.length > 0) {
579
+ const pruned = removePane(layout, pane.id);
580
+ if (pruned) {
581
+ layout = pruned;
582
+ continue;
583
+ }
584
+ }
585
+ panes[pane.id] = {
586
+ ...pane,
587
+ itemIds,
588
+ activeItemId: pane.activeItemId && keep.has(pane.activeItemId) ? pane.activeItemId : itemIds[0] ?? null
589
+ };
590
+ }
591
+ const focusedPaneId = panes[state.focusedPaneId] ? state.focusedPaneId : Object.values(panes)[0]?.id ?? state.focusedPaneId;
592
+ return { ...state, items, panes, layout, focusedPaneId };
593
+ }
594
+
595
+ // src/core/controller.ts
596
+ var consoleCanvasErrorSink = (report) => {
597
+ console.error(`[@ai-matrx/canvas] ${report.code}: ${report.message}`, report.detail ?? "");
598
+ };
599
+ function createCanvasController(options) {
600
+ const { store, persistence = null } = options;
601
+ const onError = options.onError ?? consoleCanvasErrorSink;
602
+ const isRestorable = options.isRestorable ?? (() => true);
603
+ const dispatch = (action) => store.dispatch(action);
604
+ let presentations = 0;
605
+ const presentationListeners = /* @__PURE__ */ new Set();
606
+ const notifyPresentation = () => {
607
+ for (const listener of [...presentationListeners]) listener();
608
+ };
609
+ const controller = {
610
+ store,
611
+ getState: () => store.getState(),
612
+ open(input) {
613
+ if (options.isKnownKind && !options.isKnownKind(input.kind)) {
614
+ onError({ code: "unknown-kind", message: `No canvas kind "${input.kind}" is registered.`, detail: input });
615
+ return null;
616
+ }
617
+ if (input.data !== void 0) {
618
+ const bad = findNonJson(input.data);
619
+ if (bad) {
620
+ onError({
621
+ code: "non-json-data",
622
+ message: `Canvas data for "${input.kind}" must be plain JSON; ${bad} is not.`,
623
+ detail: { kind: input.kind, key: input.key }
624
+ });
625
+ return null;
626
+ }
627
+ }
628
+ dispatch(canvasActions.open(input));
629
+ return canvasItemId(input.kind, input.key);
630
+ },
631
+ update(itemId, patch) {
632
+ if (patch.data !== void 0) {
633
+ const bad = findNonJson(patch.data);
634
+ if (bad) {
635
+ onError({ code: "non-json-data", message: `Canvas update for ${itemId} must be plain JSON; ${bad} is not.` });
636
+ return false;
637
+ }
638
+ }
639
+ if (!store.getState().items[itemId]) return false;
640
+ dispatch(canvasActions.update(itemId, patch));
641
+ return true;
642
+ },
643
+ rekey(itemId, key) {
644
+ const item = store.getState().items[itemId];
645
+ if (!item) return null;
646
+ dispatch(canvasActions.rekey(itemId, key));
647
+ return canvasItemId(item.kind, key);
648
+ },
649
+ close: (itemId) => dispatch(canvasActions.closeItem(itemId)),
650
+ closeOthers: (itemId) => dispatch(canvasActions.closeOthers(itemId)),
651
+ activate: (itemId) => dispatch(canvasActions.activate(itemId)),
652
+ focusPane: (paneId) => dispatch(canvasActions.focusPane(paneId)),
653
+ moveItem: (itemId, toPaneId, index) => dispatch(canvasActions.moveItem(itemId, toPaneId, index)),
654
+ splitPane: (paneId, orientation, moveItemId) => dispatch(canvasActions.splitPane(paneId, orientation, moveItemId)),
655
+ closePane: (paneId) => dispatch(canvasActions.closePane(paneId)),
656
+ resizeSplit: (splitId, sizes) => dispatch(canvasActions.resizeSplit(splitId, sizes)),
657
+ show: () => dispatch(canvasActions.setOpen(true)),
658
+ hide: () => dispatch(canvasActions.setOpen(false)),
659
+ toggle: () => dispatch(canvasActions.toggle()),
660
+ setFullscreen: (fullscreen) => dispatch(canvasActions.setFullscreen(fullscreen)),
661
+ setWidth: (width) => dispatch(canvasActions.setWidth(width)),
662
+ has: (kind, key) => canvasItemId(kind, key) in store.getState().items,
663
+ isPresented: () => presentations > 0,
664
+ registerPresentation() {
665
+ presentations += 1;
666
+ notifyPresentation();
667
+ let done = false;
668
+ return () => {
669
+ if (done) return;
670
+ done = true;
671
+ presentations -= 1;
672
+ notifyPresentation();
673
+ };
674
+ },
675
+ subscribePresentation(listener) {
676
+ presentationListeners.add(listener);
677
+ return () => presentationListeners.delete(listener);
678
+ },
679
+ start() {
680
+ let disposed = false;
681
+ let timer = null;
682
+ let lastSaved = null;
683
+ const save = () => {
684
+ timer = null;
685
+ const state = store.getState();
686
+ if (!persistence || !state.hydrated || state === lastSaved) return;
687
+ lastSaved = state;
688
+ try {
689
+ void Promise.resolve(persistence.save(toPersistableSnapshot(state, isRestorable))).catch(
690
+ (error) => onError({ code: "persistence-save", message: "Saving the canvas layout failed.", detail: error })
691
+ );
692
+ } catch (error) {
693
+ onError({ code: "persistence-save", message: "Saving the canvas layout failed.", detail: error });
694
+ }
695
+ };
696
+ let seen = store.getState();
697
+ const unsubscribe = store.subscribe(() => {
698
+ if (!persistence || disposed) return;
699
+ const current = store.getState();
700
+ if (current === seen) return;
701
+ seen = current;
702
+ if (timer) clearTimeout(timer);
703
+ timer = setTimeout(save, options.saveDelayMs ?? 250);
704
+ });
705
+ if (store.getState().hydrated) {
706
+ } else if (!persistence) {
707
+ dispatch(canvasActions.hydrate(null));
708
+ } else {
709
+ Promise.resolve().then(() => persistence.load()).then(
710
+ (snapshot) => {
711
+ if (disposed) return;
712
+ const openedThisSession = store.getState().isOpen;
713
+ const validSnapshot = snapshot === null ? null : sanitizeCanvasSnapshot(snapshot);
714
+ if (snapshot !== null && validSnapshot === null) {
715
+ onError({ code: "persistence-load", message: "The saved canvas layout is invalid. Clear the saved layout or repair the persistence port." });
716
+ }
717
+ dispatch(canvasActions.hydrate(validSnapshot));
718
+ if (!openedThisSession && options.mayRestoreOpen && !options.mayRestoreOpen() && store.getState().isOpen) {
719
+ dispatch(canvasActions.setOpen(false));
720
+ }
721
+ },
722
+ (error) => {
723
+ onError({ code: "persistence-load", message: "The saved canvas layout could not be read.", detail: error });
724
+ if (!disposed) dispatch(canvasActions.hydrate(null));
725
+ }
726
+ );
727
+ }
728
+ return () => {
729
+ disposed = true;
730
+ unsubscribe();
731
+ if (timer) {
732
+ clearTimeout(timer);
733
+ save();
734
+ }
735
+ };
736
+ }
737
+ };
738
+ return controller;
739
+ }
740
+
741
+ // src/core/selectors.ts
742
+ var selectCanvasIsOpen = (s) => s.isOpen;
743
+ var selectCanvasIsFullscreen = (s) => s.isFullscreen;
744
+ var selectCanvasWidth = (s) => s.width;
745
+ var selectCanvasLayout = (s) => s.layout;
746
+ var selectCanvasFocusedPaneId = (s) => s.focusedPaneId;
747
+ var selectCanvasIsHydrated = (s) => s.hydrated;
748
+ var selectCanvasItemCount = (s) => Object.keys(s.items).length;
749
+ var selectCanvasPane = (s, paneId) => s.panes[paneId];
750
+ var selectCanvasItem = (s, itemId) => s.items[itemId];
751
+ function selectCanvasPaneCount(s) {
752
+ return listPaneIds(s.layout).length;
753
+ }
754
+ function selectCanvasActiveItem(s) {
755
+ const pane = s.panes[s.focusedPaneId];
756
+ return pane?.activeItemId ? s.items[pane.activeItemId] ?? null : null;
757
+ }
758
+ //# sourceMappingURL=index.cjs.map