@ai-matrx/canvas 0.1.0 → 0.2.0

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