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