@ai-matrx/canvas 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  createContext,
7
7
  useContext,
8
8
  useEffect as useEffect2,
9
+ useMemo,
9
10
  useRef as useRef2,
10
11
  useState as useState2,
11
12
  useSyncExternalStore as useSyncExternalStore2
@@ -13,6 +14,8 @@ import {
13
14
 
14
15
  // src/core/types.ts
15
16
  var CANVAS_MAIN_WINDOW = "main";
17
+ var CANVAS_LAUNCHER_KIND = "canvas.launcher";
18
+ var CANVAS_HISTORY_LIMIT = 50;
16
19
  var CANVAS_MIN_WIDTH = 360;
17
20
  var CANVAS_DEFAULT_WIDTH = 640;
18
21
  var CANVAS_MIN_SPLIT_FRACTION = 0.12;
@@ -171,6 +174,10 @@ var canvasActions = {
171
174
  }),
172
175
  /** Gives an item a new identity in place (e.g. a draft that was just saved and now has an id). */
173
176
  rekey: (itemId, key) => ({ type: `${P}rekey`, payload: { itemId, key } }),
177
+ /** Replaces a tab's content with another item in place, remembering what it showed (Back returns). */
178
+ navigate: (itemId, to) => ({ type: `${P}navigate`, payload: { itemId, to, now: Date.now() } }),
179
+ /** Returns a tab to what it showed before its last navigate. */
180
+ back: (itemId) => ({ type: `${P}back`, payload: { itemId, now: Date.now() } }),
174
181
  closeItem: (itemId) => ({ type: `${P}closeItem`, payload: { itemId } }),
175
182
  closeOthers: (itemId) => ({ type: `${P}closeOthers`, payload: { itemId } }),
176
183
  activate: (itemId) => ({ type: `${P}activate`, payload: { itemId } }),
@@ -367,6 +374,63 @@ function sendHome(state, itemId, home, activate) {
367
374
  next = insertInto(next, homeFrame.focusedPaneId, itemId, activate);
368
375
  return activate ? withFrame(next, target, { focusedPaneId: homeFrame.focusedPaneId }) : next;
369
376
  }
377
+ function newItem(kind, key, title, data, now, history) {
378
+ const id = canvasItemId(kind, key);
379
+ return {
380
+ id,
381
+ kind,
382
+ key,
383
+ title,
384
+ data,
385
+ openedAt: now,
386
+ updatedAt: now,
387
+ instance: `${now.toString(36)}:${id}`,
388
+ ...history && history.length > 0 ? { history: history.slice(-CANVAS_HISTORY_LIMIT) } : {}
389
+ };
390
+ }
391
+ function historyEntry(item) {
392
+ return { kind: item.kind, key: item.key, title: item.title, data: item.data };
393
+ }
394
+ function replaceInPlace(state, current, next) {
395
+ const pane = paneOf(state, current.id);
396
+ if (!pane) return state;
397
+ const items = { ...state.items, [next.id]: next };
398
+ delete items[current.id];
399
+ const withItems = { ...state, items };
400
+ const replaced = withPane(withItems, {
401
+ ...pane,
402
+ itemIds: pane.itemIds.map((id) => id === current.id ? next.id : id),
403
+ activeItemId: pane.activeItemId === current.id ? next.id : pane.activeItemId
404
+ });
405
+ return focus(replaced, pane.id);
406
+ }
407
+ function focusExisting(state, itemId) {
408
+ const pane = paneOf(state, itemId);
409
+ if (!pane) return state;
410
+ const next = focus(withPane(state, { ...pane, activeItemId: itemId }), pane.id);
411
+ const windowId = canvasWindowOfPane(next, pane.id);
412
+ return windowId ? withFrame(next, windowId, { isOpen: true }) : next;
413
+ }
414
+ function sanitizeHistory(raw) {
415
+ if (!Array.isArray(raw)) return [];
416
+ const out = [];
417
+ for (const entry of raw) {
418
+ if (!entry || typeof entry !== "object") continue;
419
+ const e = entry;
420
+ if (typeof e.kind !== "string" || typeof e.key !== "string") continue;
421
+ out.push({ kind: e.kind, key: e.key, title: typeof e.title === "string" ? e.title : null, data: e.data ?? null });
422
+ }
423
+ return out.slice(-CANVAS_HISTORY_LIMIT);
424
+ }
425
+ function sanitizeItem(item) {
426
+ const history = sanitizeHistory(item.history);
427
+ const { history: _history, instance, ...rest } = item;
428
+ return {
429
+ ...rest,
430
+ ...typeof instance === "string" && instance ? { instance } : {},
431
+ ...history.length > 0 ? { history } : {}
432
+ };
433
+ }
370
434
  function clampWidth(width) {
371
435
  return Number.isFinite(width) ? Math.max(CANVAS_MIN_WIDTH, Math.round(width)) : null;
372
436
  }
@@ -414,7 +478,7 @@ function sanitizeCanvasSnapshot(raw) {
414
478
  const items = {};
415
479
  for (const [id, item] of Object.entries(s.items)) {
416
480
  if (item && typeof item.kind === "string" && typeof item.key === "string" && canvasItemId(item.kind, item.key) === id) {
417
- items[id] = item;
481
+ items[id] = sanitizeItem(item);
418
482
  }
419
483
  }
420
484
  const placed = /* @__PURE__ */ new Set();
@@ -488,7 +552,7 @@ function canvasReducer(state = createInitialCanvasState(), action) {
488
552
  next = insertInto(next, frame2.focusedPaneId, itemId, true);
489
553
  }
490
554
  } else {
491
- const item = { id: itemId, kind, key, title: title ?? null, data: data ?? null, openedAt: now, updatedAt: now };
555
+ const item = newItem(kind, key, title ?? null, data ?? null, now);
492
556
  next = ensureWindow({ ...next, items: { ...next.items, [itemId]: item } }, revealIn);
493
557
  const frame2 = canvasWindowFrame(next, revealIn) ?? next;
494
558
  let paneId = frame2.focusedPaneId;
@@ -537,6 +601,35 @@ function canvasReducer(state = createInitialCanvasState(), action) {
537
601
  }
538
602
  return { ...state, items, panes };
539
603
  }
604
+ case `${P}navigate`: {
605
+ const { itemId, to, now } = action.payload;
606
+ const current = state.items[itemId];
607
+ if (!current || !paneOf(state, itemId)) return state;
608
+ const targetId = canvasItemId(to.kind, to.key);
609
+ if (targetId === itemId) {
610
+ const refresh = { type: `${P}update`, payload: { itemId, data: to.data, title: to.title, now } };
611
+ return canvasReducer(state, refresh);
612
+ }
613
+ if (state.items[targetId]) {
614
+ const focused = focusExisting(state, targetId);
615
+ return current.kind === CANVAS_LAUNCHER_KIND ? canvasReducer(focused, canvasActions.closeItem(itemId)) : focused;
616
+ }
617
+ const history = [...current.history ?? [], historyEntry(current)];
618
+ return replaceInPlace(state, current, newItem(to.kind, to.key, to.title ?? null, to.data ?? null, now, history));
619
+ }
620
+ case `${P}back`: {
621
+ const { itemId, now } = action.payload;
622
+ const current = state.items[itemId];
623
+ const history = current?.history ?? [];
624
+ const entry = history[history.length - 1];
625
+ if (!current || !entry || !paneOf(state, itemId)) return state;
626
+ const targetId = canvasItemId(entry.kind, entry.key);
627
+ if (targetId === itemId) return state;
628
+ if (state.items[targetId]) {
629
+ return focusExisting(canvasReducer(state, canvasActions.closeItem(itemId)), targetId);
630
+ }
631
+ return replaceInPlace(state, current, newItem(entry.kind, entry.key, entry.title, entry.data, now, history.slice(0, -1)));
632
+ }
540
633
  case `${P}closeItem`: {
541
634
  const { itemId } = action.payload;
542
635
  if (!state.items[itemId]) return state;
@@ -790,7 +883,8 @@ var CANVAS_WEB_LAYOUT_RULES = {
790
883
  defaultWidth: CANVAS_DEFAULT_WIDTH,
791
884
  minWidth: CANVAS_MIN_WIDTH,
792
885
  centreMinWidth: 420,
793
- stackSplit: 0.5,
886
+ // Split down 70/30 like the desktop: what opens below (notifications, a feed) is the smaller pane.
887
+ stackSplit: 0.7,
794
888
  sideSplit: 0.5,
795
889
  paneMinWidth: 0
796
890
  };
@@ -915,7 +1009,8 @@ function createCanvasController(options) {
915
1009
  presentationListeners: /* @__PURE__ */ new Set(),
916
1010
  views: /* @__PURE__ */ new Map(),
917
1011
  started: /* @__PURE__ */ new Map(),
918
- sessionStop: null
1012
+ sessionStop: null,
1013
+ launcherSeq: 0
919
1014
  };
920
1015
  return view(shared, canvasWindowId(options.windowId ?? CANVAS_MAIN_WINDOW));
921
1016
  }
@@ -930,6 +1025,36 @@ function view(shared, windowId) {
930
1025
  for (const listener of [...shared.presentationListeners]) listener();
931
1026
  };
932
1027
  const shareFor = (orientation) => orientation === "vertical" ? rules.stackSplit : rules.sideSplit;
1028
+ const acceptable = (input) => {
1029
+ if (input.kind !== CANVAS_LAUNCHER_KIND && options.isKnownKind && !options.isKnownKind(input.kind)) {
1030
+ onError({ code: "unknown-kind", message: `No canvas kind "${input.kind}" is registered.`, detail: input });
1031
+ return false;
1032
+ }
1033
+ if (input.data !== void 0) {
1034
+ const bad = findNonJson(input.data);
1035
+ if (bad) {
1036
+ onError({
1037
+ code: "non-json-data",
1038
+ message: `Canvas data for "${input.kind}" must be plain JSON; ${bad} is not.`,
1039
+ detail: { kind: input.kind, key: input.key }
1040
+ });
1041
+ return false;
1042
+ }
1043
+ }
1044
+ return true;
1045
+ };
1046
+ const preferredTargetFor = (input) => {
1047
+ const preferred = options.preferredTarget?.(input.kind);
1048
+ if (!preferred || preferred === "focused") return "focused";
1049
+ const state = store.getState();
1050
+ if (canvasItemId(input.kind, input.key) in state.items) return "focused";
1051
+ const frame2 = canvasWindowFrame(state, input.windowId ?? windowId);
1052
+ if (!frame2?.isOpen) return "focused";
1053
+ const panes = listPaneIds(frame2.layout);
1054
+ const holding = panes.find((paneId) => state.panes[paneId]?.itemIds.some((id) => state.items[id]?.kind === input.kind));
1055
+ if (holding) return { paneId: holding };
1056
+ return state.panes[frame2.focusedPaneId]?.activeItemId ? preferred : "focused";
1057
+ };
933
1058
  const controller = {
934
1059
  store,
935
1060
  windowId,
@@ -937,24 +1062,11 @@ function view(shared, windowId) {
937
1062
  forWindow: (id) => view(shared, canvasWindowId(id)),
938
1063
  getState: () => store.getState(),
939
1064
  open(input) {
940
- if (options.isKnownKind && !options.isKnownKind(input.kind)) {
941
- onError({ code: "unknown-kind", message: `No canvas kind "${input.kind}" is registered.`, detail: input });
942
- return null;
943
- }
944
- if (input.data !== void 0) {
945
- const bad = findNonJson(input.data);
946
- if (bad) {
947
- onError({
948
- code: "non-json-data",
949
- message: `Canvas data for "${input.kind}" must be plain JSON; ${bad} is not.`,
950
- detail: { kind: input.kind, key: input.key }
951
- });
952
- return null;
953
- }
954
- }
955
- const target = input.target ?? "focused";
1065
+ if (!acceptable(input)) return null;
1066
+ const target = input.target ?? preferredTargetFor(input);
956
1067
  const splitShare = target === "split-down" ? rules.stackSplit : target === "split-right" ? rules.sideSplit : void 0;
957
- const resolved = input.windowId || windowId === CANVAS_MAIN_WINDOW ? input : { ...input, windowId };
1068
+ const withTarget = input.target === target ? input : { ...input, target };
1069
+ const resolved = withTarget.windowId || windowId === CANVAS_MAIN_WINDOW ? withTarget : { ...withTarget, windowId };
958
1070
  dispatch(canvasActions.open(resolved, splitShare === void 0 ? void 0 : { splitShare }));
959
1071
  return canvasItemId(input.kind, input.key);
960
1072
  },
@@ -970,6 +1082,34 @@ function view(shared, windowId) {
970
1082
  dispatch(canvasActions.update(itemId, patch));
971
1083
  return true;
972
1084
  },
1085
+ navigate(itemId, to) {
1086
+ if (!store.getState().items[itemId] || !acceptable(to)) return null;
1087
+ dispatch(canvasActions.navigate(itemId, to));
1088
+ const shown = canvasItemId(to.kind, to.key);
1089
+ return shown in store.getState().items ? shown : null;
1090
+ },
1091
+ back(itemId) {
1092
+ const history = store.getState().items[itemId]?.history ?? [];
1093
+ const entry = history[history.length - 1];
1094
+ if (!entry) return null;
1095
+ dispatch(canvasActions.back(itemId));
1096
+ return canvasItemId(entry.kind, entry.key);
1097
+ },
1098
+ newTab(paneId) {
1099
+ const state = store.getState();
1100
+ const frame2 = canvasWindowFrame(state, windowId);
1101
+ const target = paneId && state.panes[paneId] ? paneId : frame2?.focusedPaneId ?? state.focusedPaneId;
1102
+ const existing = state.panes[target]?.itemIds.find((id) => state.items[id]?.kind === CANVAS_LAUNCHER_KIND);
1103
+ if (existing) {
1104
+ dispatch(canvasActions.activate(existing));
1105
+ dispatch(canvasActions.setOpen(true, canvasWindowOfItem(store.getState(), existing) ?? windowId));
1106
+ return existing;
1107
+ }
1108
+ shared.launcherSeq += 1;
1109
+ const key = `${Date.now().toString(36)}-${shared.launcherSeq}`;
1110
+ dispatch(canvasActions.open({ kind: CANVAS_LAUNCHER_KIND, key, target: { paneId: target }, ...windowId === CANVAS_MAIN_WINDOW ? {} : { windowId } }));
1111
+ return canvasItemId(CANVAS_LAUNCHER_KIND, key);
1112
+ },
973
1113
  rekey(itemId, key) {
974
1114
  const item = store.getState().items[itemId];
975
1115
  if (!item) return null;
@@ -1219,6 +1359,32 @@ function selectCanvasWindowItemCount(s, windowId) {
1219
1359
  return listPaneIds(layout).reduce((sum, paneId) => sum + (s.panes[paneId]?.itemIds.length ?? 0), 0);
1220
1360
  }
1221
1361
  var NO_WINDOWS = Object.freeze({});
1362
+ var NO_HISTORY = Object.freeze([]);
1363
+ var selectCanvasCanGoBack = (s, itemId) => (s.items[itemId]?.history?.length ?? 0) > 0;
1364
+
1365
+ // src/core/presentation.ts
1366
+ var CANVAS_NARROW_WIDTH = 480;
1367
+ var SQUARE_BAND = 1.2;
1368
+ function canvasOrientation(width, height) {
1369
+ if (!(width > 0) || !(height > 0)) return "square";
1370
+ const ratio = width / height;
1371
+ if (ratio >= SQUARE_BAND) return "landscape";
1372
+ if (ratio <= 1 / SQUARE_BAND) return "portrait";
1373
+ return "square";
1374
+ }
1375
+ function canvasPresentation(input) {
1376
+ const width = Math.max(0, Math.round(input.width));
1377
+ const height = Math.max(0, Math.round(input.height));
1378
+ return {
1379
+ host: "canvas",
1380
+ width,
1381
+ height,
1382
+ orientation: canvasOrientation(width, height),
1383
+ isNarrow: width > 0 && width < CANVAS_NARROW_WIDTH,
1384
+ isFullscreen: input.isFullscreen,
1385
+ paneCount: Math.max(1, input.paneCount)
1386
+ };
1387
+ }
1222
1388
 
1223
1389
  // src/core/toggleKind.ts
1224
1390
  var CANVAS_DEFAULT_KEY = "default";
@@ -1420,6 +1586,31 @@ function getCanvasKindsVersion() {
1420
1586
 
1421
1587
  // src/react/provider.tsx
1422
1588
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1589
+ function createLauncherEntryStore() {
1590
+ let entries = [];
1591
+ const listeners = /* @__PURE__ */ new Set();
1592
+ const emit = () => {
1593
+ for (const listener of [...listeners]) listener();
1594
+ };
1595
+ return {
1596
+ add(entry) {
1597
+ entries = [...entries, entry];
1598
+ emit();
1599
+ let done = false;
1600
+ return () => {
1601
+ if (done) return;
1602
+ done = true;
1603
+ entries = entries.filter((e) => e !== entry);
1604
+ emit();
1605
+ };
1606
+ },
1607
+ list: () => entries,
1608
+ subscribe(listener) {
1609
+ listeners.add(listener);
1610
+ return () => listeners.delete(listener);
1611
+ }
1612
+ };
1613
+ }
1423
1614
  var PHONE_MAX_WIDTH = 767;
1424
1615
  var isPhoneViewport = () => typeof window !== "undefined" && window.innerWidth <= PHONE_MAX_WIDTH;
1425
1616
  var isMacPlatform = () => typeof navigator !== "undefined" && /mac|iphone|ipad/i.test(navigator.userAgentData?.platform ?? navigator.platform ?? "");
@@ -1448,11 +1639,20 @@ function CanvasProvider({
1448
1639
  layoutRules,
1449
1640
  regionWidth = null,
1450
1641
  tabStrip = "auto",
1451
- ...hostPorts
1642
+ itemActions,
1643
+ popOut,
1644
+ dockBack,
1645
+ showMenu,
1646
+ overlay,
1647
+ onNativeBodyFrame
1452
1648
  }) {
1649
+ const hostPorts = useMemo(
1650
+ () => ({ itemActions, popOut, dockBack, showMenu, overlay, onNativeBodyFrame }),
1651
+ [itemActions, popOut, dockBack, showMenu, overlay, onNativeBodyFrame]
1652
+ );
1453
1653
  const portsRef = useRef2(hostPorts);
1454
1654
  portsRef.current = hostPorts;
1455
- const [{ controller, layer }] = useState2(() => ({
1655
+ const [{ controller, layer, launcherEntries }] = useState2(() => ({
1456
1656
  controller: createCanvasController({
1457
1657
  store: store ?? createCanvasStore(),
1458
1658
  persistence: persistence === void 0 ? createLocalStorageCanvasPersistence() : persistence,
@@ -1462,9 +1662,11 @@ function CanvasProvider({
1462
1662
  windowId: windowIdProp ?? CANVAS_MAIN_WINDOW,
1463
1663
  layoutRules,
1464
1664
  windowPorts: () => portsRef.current,
1465
- sessions: sessionHooksFor
1665
+ sessions: sessionHooksFor,
1666
+ preferredTarget: (kind) => getCanvasKind(kind)?.preferredTarget
1466
1667
  }),
1467
- layer: createCanvasLayer(() => portsRef.current)
1668
+ layer: createCanvasLayer(() => portsRef.current),
1669
+ launcherEntries: createLauncherEntryStore()
1468
1670
  }));
1469
1671
  const windowId = controller.windowId;
1470
1672
  useEffect2(() => controller.start(), [controller]);
@@ -1489,7 +1691,10 @@ function CanvasProvider({
1489
1691
  window.addEventListener("keydown", onKey);
1490
1692
  return () => window.removeEventListener("keydown", onKey);
1491
1693
  }, [controller, hotkeys, windowId]);
1492
- const value = { controller, windowId, ports: hostPorts, layer, regionWidth, tabStrip };
1694
+ const value = useMemo(
1695
+ () => ({ controller, windowId, ports: hostPorts, layer, regionWidth, tabStrip, launcherEntries }),
1696
+ [controller, windowId, hostPorts, layer, regionWidth, tabStrip, launcherEntries]
1697
+ );
1493
1698
  return /* @__PURE__ */ jsxs(CanvasContext.Provider, { value, children: [
1494
1699
  children,
1495
1700
  hostPorts.overlay ? null : /* @__PURE__ */ jsx2(CanvasDomOverlay, { layer })
@@ -1497,8 +1702,10 @@ function CanvasProvider({
1497
1702
  }
1498
1703
  function CanvasWindowScope({ windowId, children }) {
1499
1704
  const parent = useCanvasContext();
1500
- const id = canvasWindowId(windowId);
1501
- const value = { ...parent, windowId: id, controller: parent.controller.forWindow(id) };
1705
+ const value = useMemo(() => {
1706
+ const id = canvasWindowId(windowId);
1707
+ return { ...parent, windowId: id, controller: parent.controller.forWindow(id) };
1708
+ }, [parent, windowId]);
1502
1709
  return /* @__PURE__ */ jsx2(CanvasContext.Provider, { value, children });
1503
1710
  }
1504
1711
  function useCanvasContext() {
@@ -1576,27 +1783,54 @@ function useCanvasKind(id) {
1576
1783
  useSyncExternalStore2(subscribeCanvasKinds, getCanvasKindsVersion, getCanvasKindsVersion);
1577
1784
  return getCanvasKind(id);
1578
1785
  }
1786
+ var NO_ENTRIES = Object.freeze([]);
1787
+ var noEntries = () => NO_ENTRIES;
1788
+ function useCanvasLauncherEntry(entry) {
1789
+ const store = useContext(CanvasContext)?.launcherEntries;
1790
+ const fingerprint = JSON.stringify([entry.kind, entry.key, entry.title ?? null, entry.data ?? null]);
1791
+ const icon = entry.icon;
1792
+ useEffect2(() => {
1793
+ if (!store) return void 0;
1794
+ const [kind, key, title, data] = JSON.parse(fingerprint);
1795
+ return store.add({ kind, key, title, data, ...icon ? { icon } : {} });
1796
+ }, [store, fingerprint, icon]);
1797
+ }
1798
+ function useCanvasLauncherEntries() {
1799
+ const store = useContext(CanvasContext)?.launcherEntries;
1800
+ const all = useSyncExternalStore2(store ? store.subscribe : NO_SUBSCRIPTION, store ? store.list : noEntries, store ? store.list : noEntries);
1801
+ const seen = /* @__PURE__ */ new Set();
1802
+ const out = [];
1803
+ for (let i = all.length - 1; i >= 0; i -= 1) {
1804
+ const entry = all[i];
1805
+ if (!entry || seen.has(`${entry.kind}::${entry.key}`)) continue;
1806
+ seen.add(`${entry.kind}::${entry.key}`);
1807
+ out.unshift(entry);
1808
+ }
1809
+ return out;
1810
+ }
1579
1811
 
1580
1812
  // src/react/CanvasColumn.tsx
1581
1813
  import {
1582
- useEffect as useEffect5,
1583
- useLayoutEffect as useLayoutEffect3,
1584
- useRef as useRef5,
1585
- useState as useState4,
1586
- useSyncExternalStore as useSyncExternalStore3
1814
+ useEffect as useEffect7,
1815
+ useLayoutEffect as useLayoutEffect4,
1816
+ useRef as useRef7,
1817
+ useState as useState6,
1818
+ useSyncExternalStore as useSyncExternalStore4
1587
1819
  } from "react";
1588
1820
  import { TapTargetButton, TapTargetButtonTransparent } from "@ai-matrx/tap-target";
1589
1821
 
1590
- // src/react/CanvasPane.tsx
1822
+ // src/react/bodies.tsx
1591
1823
  import {
1592
1824
  Component,
1593
1825
  Suspense,
1594
1826
  lazy,
1595
- useEffect as useEffect4,
1827
+ memo,
1828
+ useEffect as useEffect5,
1596
1829
  useLayoutEffect as useLayoutEffect2,
1597
- useRef as useRef4
1830
+ useMemo as useMemo2,
1831
+ useRef as useRef5,
1832
+ useState as useState4
1598
1833
  } from "react";
1599
- import * as Menu from "@radix-ui/react-dropdown-menu";
1600
1834
 
1601
1835
  // src/react/chrome.tsx
1602
1836
  import {
@@ -1696,6 +1930,12 @@ var DockIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
1696
1930
  /* @__PURE__ */ jsx3("path", { d: "M21 3 12 12" }),
1697
1931
  /* @__PURE__ */ jsx3("path", { d: "M12 6v6h6" })
1698
1932
  ] });
1933
+ var LayoutGridIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
1934
+ /* @__PURE__ */ jsx3("rect", { width: "7", height: "7", x: "3", y: "3", rx: "1" }),
1935
+ /* @__PURE__ */ jsx3("rect", { width: "7", height: "7", x: "14", y: "3", rx: "1" }),
1936
+ /* @__PURE__ */ jsx3("rect", { width: "7", height: "7", x: "14", y: "14", rx: "1" }),
1937
+ /* @__PURE__ */ jsx3("rect", { width: "7", height: "7", x: "3", y: "14", rx: "1" })
1938
+ ] });
1699
1939
 
1700
1940
  // src/react/chrome.tsx
1701
1941
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -1946,10 +2186,225 @@ function BrowserBar({ url, canGoBack, canGoForward, onBack, onForward, onReload,
1946
2186
  ] });
1947
2187
  }
1948
2188
 
1949
- // src/react/CanvasPane.tsx
1950
- import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1951
- var DRAG_MIME = "application/x-matrx-canvas-item";
1952
- var useIsoLayoutEffect = typeof window === "undefined" ? useEffect4 : useLayoutEffect2;
2189
+ // src/react/dnd.ts
2190
+ import { useEffect as useEffect4, useRef as useRef4 } from "react";
2191
+ var CANVAS_DRAG_MIME = "application/x-matrx-canvas-item";
2192
+ function useTabDrag(itemId, title) {
2193
+ const { layer } = useCanvasInternals();
2194
+ const active = useRef4([]);
2195
+ const end = () => {
2196
+ for (const dispose of active.current.splice(0)) dispose();
2197
+ };
2198
+ useEffect4(() => end, []);
2199
+ return {
2200
+ draggable: true,
2201
+ onDragStart: (event) => {
2202
+ event.dataTransfer.setData(CANVAS_DRAG_MIME, itemId);
2203
+ event.dataTransfer.effectAllowed = "move";
2204
+ end();
2205
+ active.current.push(layer.beginBlocking(), layer.show({ type: "tab-drag", itemId, title }));
2206
+ },
2207
+ onDragEnd: end
2208
+ };
2209
+ }
2210
+ function usePaneDrop(paneId, getOutline) {
2211
+ const canvas = useCanvas();
2212
+ const { layer } = useCanvasInternals();
2213
+ const drop = useRef4(null);
2214
+ useEffect4(() => () => drop.current?.(), []);
2215
+ const hide = () => {
2216
+ drop.current?.();
2217
+ drop.current = null;
2218
+ };
2219
+ return {
2220
+ onDragOver: (event) => {
2221
+ if (!event.dataTransfer.types.includes(CANVAS_DRAG_MIME)) return;
2222
+ event.preventDefault();
2223
+ event.dataTransfer.dropEffect = "move";
2224
+ const outline = getOutline();
2225
+ if (!drop.current && outline) {
2226
+ drop.current = layer.show({ type: "drop-indicator", rect: toCanvasRect(outline.getBoundingClientRect()), paneId });
2227
+ }
2228
+ },
2229
+ onDragLeave: (event) => {
2230
+ const next = event.relatedTarget;
2231
+ if (next && event.currentTarget.contains(next)) return;
2232
+ hide();
2233
+ },
2234
+ onDrop: (event) => {
2235
+ hide();
2236
+ const itemId = event.dataTransfer.getData(CANVAS_DRAG_MIME);
2237
+ if (!itemId) return;
2238
+ event.preventDefault();
2239
+ canvas.moveItem(itemId, paneId);
2240
+ }
2241
+ };
2242
+ }
2243
+
2244
+ // src/react/launcher.tsx
2245
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2246
+ function useChoices() {
2247
+ const entries = useCanvasLauncherEntries();
2248
+ const kinds = useCanvasKinds();
2249
+ const byId = new Map(kinds.map((kind) => [kind.id, kind]));
2250
+ const contextual = [];
2251
+ for (const entry of entries) {
2252
+ const kind = byId.get(entry.kind);
2253
+ if (!kind) continue;
2254
+ contextual.push({
2255
+ id: `ctx:${canvasItemId(entry.kind, entry.key)}`,
2256
+ label: entry.title ?? kind.label,
2257
+ icon: entry.icon ?? kind.icon,
2258
+ input: { kind: entry.kind, key: entry.key, title: entry.title ?? null, ...entry.data !== void 0 ? { data: entry.data } : {} }
2259
+ });
2260
+ }
2261
+ const global = [];
2262
+ for (const kind of kinds) {
2263
+ const launcher = kind.launcher;
2264
+ if (!launcher) continue;
2265
+ global.push({
2266
+ id: `kind:${kind.id}`,
2267
+ label: launcher.title ?? kind.label,
2268
+ icon: kind.icon,
2269
+ input: { kind: kind.id, key: launcher.key, data: launcher.data, title: launcher.title ?? null }
2270
+ });
2271
+ }
2272
+ return { contextual, global };
2273
+ }
2274
+ function LauncherGrid({ onChoose }) {
2275
+ const { contextual, global } = useChoices();
2276
+ if (contextual.length === 0 && global.length === 0) return /* @__PURE__ */ jsx5(CanvasPanelEmpty, { children: "Nothing to open" });
2277
+ const section = (label, choices) => choices.length === 0 ? null : /* @__PURE__ */ jsxs4("section", { className: "mxc-launcher-section", "aria-label": label ?? "Open", children: [
2278
+ label ? /* @__PURE__ */ jsx5("h3", { className: "mxc-launcher-heading", children: label }) : null,
2279
+ /* @__PURE__ */ jsx5("div", { className: "mxc-launcher-grid", children: choices.map((choice) => {
2280
+ const Icon = choice.icon;
2281
+ return /* @__PURE__ */ jsxs4("button", { type: "button", className: "mxc-launcher-item", onClick: () => onChoose(choice.input), children: [
2282
+ /* @__PURE__ */ jsx5(Icon, { className: "mxc-launcher-icon" }),
2283
+ /* @__PURE__ */ jsx5("span", { children: choice.label })
2284
+ ] }, choice.id);
2285
+ }) })
2286
+ ] });
2287
+ return /* @__PURE__ */ jsxs4("div", { className: "mxc-launcher", children: [
2288
+ section(contextual.length > 0 ? "On this page" : null, contextual),
2289
+ section(contextual.length > 0 ? "Open" : null, global)
2290
+ ] });
2291
+ }
2292
+ function LauncherBody({ item, canvas }) {
2293
+ return /* @__PURE__ */ jsx5(LauncherGrid, { onChoose: (input) => void canvas.navigate(item.id, input) });
2294
+ }
2295
+ function EmptyPaneLauncher({ paneId }) {
2296
+ const canvas = useCanvas();
2297
+ return /* @__PURE__ */ jsx5(
2298
+ LauncherGrid,
2299
+ {
2300
+ onChoose: (input) => {
2301
+ const tab = canvas.newTab(paneId);
2302
+ canvas.navigate(tab, input);
2303
+ }
2304
+ }
2305
+ );
2306
+ }
2307
+ var CANVAS_LAUNCHER_KIND_DEFINITION = {
2308
+ id: CANVAS_LAUNCHER_KIND,
2309
+ label: "New tab",
2310
+ icon: LayoutGridIcon,
2311
+ component: LauncherBody
2312
+ };
2313
+ function useItemKind(kindId) {
2314
+ const registered = useCanvasKind(kindId);
2315
+ return kindId === CANVAS_LAUNCHER_KIND ? CANVAS_LAUNCHER_KIND_DEFINITION : registered;
2316
+ }
2317
+
2318
+ // src/react/presentation.tsx
2319
+ import { createContext as createContext2, useContext as useContext2 } from "react";
2320
+ var CanvasPresentationContext = createContext2(null);
2321
+ function useCanvasPresentation() {
2322
+ return useContext2(CanvasPresentationContext);
2323
+ }
2324
+
2325
+ // src/react/slots.ts
2326
+ import { createContext as createContext3, useContext as useContext3, useSyncExternalStore as useSyncExternalStore3 } from "react";
2327
+ var ZERO = Object.freeze({ width: 0, height: 0 });
2328
+ function createCanvasSlots() {
2329
+ const slots = /* @__PURE__ */ new Map();
2330
+ const sections = /* @__PURE__ */ new Map();
2331
+ const sizes = /* @__PURE__ */ new Map();
2332
+ const listeners = /* @__PURE__ */ new Set();
2333
+ let observer = null;
2334
+ const emit = () => {
2335
+ for (const listener of [...listeners]) listener();
2336
+ };
2337
+ const read = (paneId, element) => {
2338
+ const box = element.getBoundingClientRect();
2339
+ const width = Math.round(box.width);
2340
+ const height = Math.round(box.height);
2341
+ const previous = sizes.get(paneId);
2342
+ if (previous && previous.width === width && previous.height === height) return false;
2343
+ sizes.set(paneId, { width, height });
2344
+ return true;
2345
+ };
2346
+ const measureAll = () => {
2347
+ let changed = false;
2348
+ for (const [paneId, element] of slots) changed = read(paneId, element) || changed;
2349
+ return changed;
2350
+ };
2351
+ const watch = () => {
2352
+ if (observer || typeof ResizeObserver === "undefined") return observer;
2353
+ observer = new ResizeObserver(() => {
2354
+ measureAll();
2355
+ emit();
2356
+ });
2357
+ return observer;
2358
+ };
2359
+ return {
2360
+ set(paneId, element) {
2361
+ if (slots.get(paneId) === element) return;
2362
+ const previous = slots.get(paneId);
2363
+ if (previous) observer?.unobserve(previous);
2364
+ slots.set(paneId, element);
2365
+ watch()?.observe(element);
2366
+ read(paneId, element);
2367
+ emit();
2368
+ },
2369
+ unset(paneId, element) {
2370
+ const current = slots.get(paneId);
2371
+ if (!current || element && current !== element) return;
2372
+ observer?.unobserve(current);
2373
+ slots.delete(paneId);
2374
+ sizes.delete(paneId);
2375
+ emit();
2376
+ },
2377
+ setSection(paneId, element) {
2378
+ if (element) sections.set(paneId, element);
2379
+ else sections.delete(paneId);
2380
+ },
2381
+ slot: (paneId) => slots.get(paneId) ?? null,
2382
+ section: (paneId) => sections.get(paneId) ?? null,
2383
+ size: (paneId) => sizes.get(paneId) ?? ZERO,
2384
+ measure() {
2385
+ if (measureAll()) emit();
2386
+ },
2387
+ subscribe(listener) {
2388
+ listeners.add(listener);
2389
+ return () => listeners.delete(listener);
2390
+ }
2391
+ };
2392
+ }
2393
+ var CanvasSlotsContext = createContext3(null);
2394
+ function useCanvasSlots() {
2395
+ return useContext3(CanvasSlotsContext);
2396
+ }
2397
+ function useSlotSize(slots, paneId) {
2398
+ return useSyncExternalStore3(
2399
+ slots.subscribe,
2400
+ () => slots.size(paneId),
2401
+ () => ZERO
2402
+ );
2403
+ }
2404
+
2405
+ // src/react/bodies.tsx
2406
+ import { Fragment, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2407
+ var useIsoLayoutEffect = typeof window === "undefined" ? useEffect5 : useLayoutEffect2;
1953
2408
  var LAZY = /* @__PURE__ */ Symbol.for("ai-matrx.canvas.lazy");
1954
2409
  function kindComponent(kind) {
1955
2410
  if (kind.component) return kind.component;
@@ -1960,8 +2415,313 @@ function kindComponent(kind) {
1960
2415
  function resetKindComponent(kind) {
1961
2416
  if (kind) delete kind[LAZY];
1962
2417
  }
2418
+ function usePanePresentation(slots, paneId, isFullscreen, paneCount) {
2419
+ const size = useSlotSize(slots, paneId);
2420
+ return useMemo2(
2421
+ () => canvasPresentation({ width: size.width, height: size.height, isFullscreen, paneCount }),
2422
+ [size.width, size.height, isFullscreen, paneCount]
2423
+ );
2424
+ }
2425
+ var hostKey = (item) => item.instance ?? item.id;
2426
+ var SEP2 = "";
2427
+ var ROW = "";
2428
+ function layerSignature(state, windowId, onlyPane) {
2429
+ const frame2 = canvasWindowFrame(state, windowId);
2430
+ if (!frame2) return "";
2431
+ const rows = [];
2432
+ const paneIds = listPaneIds(frame2.layout);
2433
+ for (const paneId of onlyPane ? [onlyPane] : paneIds) {
2434
+ const pane = state.panes[paneId];
2435
+ for (const itemId of pane?.itemIds ?? []) {
2436
+ const item = state.items[itemId];
2437
+ if (item) rows.push({ item, row: [hostKey(item), itemId, paneId, pane?.activeItemId === itemId ? "1" : "0"].join(SEP2) });
2438
+ }
2439
+ }
2440
+ rows.sort((a, b) => a.item.openedAt - b.item.openedAt || hostKey(a.item).localeCompare(hostKey(b.item)));
2441
+ const head = [frame2.isOpen ? "1" : "0", frame2.isFullscreen ? "1" : "0", frame2.focusedPaneId, String(paneIds.length)].join(SEP2);
2442
+ return [head, ...rows.map((r) => r.row)].join(ROW);
2443
+ }
2444
+ function parseSignature(signature) {
2445
+ if (!signature) return null;
2446
+ const [head = "", ...rows] = signature.split(ROW);
2447
+ const [open, full, focusedPaneId = "", count = "1"] = head.split(SEP2);
2448
+ return {
2449
+ isOpen: open === "1",
2450
+ isFullscreen: full === "1",
2451
+ focusedPaneId,
2452
+ paneCount: Number(count) || 1,
2453
+ placed: rows.map((row) => {
2454
+ const [hostId = "", itemId = "", paneId = "", active] = row.split(SEP2);
2455
+ return { hostId, itemId, paneId, active: active === "1" };
2456
+ })
2457
+ };
2458
+ }
2459
+ function createPlacement(getRoot, slots, afterPlace) {
2460
+ const hosts = /* @__PURE__ */ new Map();
2461
+ let placing = false;
2462
+ const place = () => {
2463
+ const root = getRoot();
2464
+ if (!root || placing) return;
2465
+ placing = true;
2466
+ try {
2467
+ slots.measure();
2468
+ const base = root.getBoundingClientRect();
2469
+ for (const { element, paneId } of hosts.values()) {
2470
+ const slot = slots.slot(paneId);
2471
+ if (!slot) {
2472
+ element.dataset.placed = "false";
2473
+ continue;
2474
+ }
2475
+ const box = slot.getBoundingClientRect();
2476
+ const style = element.style;
2477
+ style.left = `${box.left - base.left}px`;
2478
+ style.top = `${box.top - base.top}px`;
2479
+ style.width = `${box.width}px`;
2480
+ style.height = `${box.height}px`;
2481
+ element.dataset.placed = "true";
2482
+ }
2483
+ } finally {
2484
+ placing = false;
2485
+ }
2486
+ afterPlace();
2487
+ };
2488
+ return {
2489
+ register(key, entry) {
2490
+ hosts.set(key, entry);
2491
+ return () => {
2492
+ if (hosts.get(key) === entry) hosts.delete(key);
2493
+ };
2494
+ },
2495
+ place
2496
+ };
2497
+ }
2498
+ function CanvasBodyLayer() {
2499
+ const { windowId, layer } = useCanvasInternals();
2500
+ const slots = useCanvasSlots();
2501
+ const signature = useCanvasState((s) => layerSignature(s, windowId));
2502
+ const root = useRef5(null);
2503
+ const [placement] = useState4(
2504
+ () => slots ? createPlacement(() => root.current, slots, () => layer.native.remeasure()) : null
2505
+ );
2506
+ useIsoLayoutEffect(() => placement?.place());
2507
+ useIsoLayoutEffect(() => slots && placement ? slots.subscribe(placement.place) : void 0, [slots, placement]);
2508
+ const shape = parseSignature(signature);
2509
+ if (!slots || !placement || !shape) return null;
2510
+ return /* @__PURE__ */ jsx6("div", { ref: root, className: "mxc-bodies", "data-mxc-bodies": windowId, children: shape.placed.map(({ hostId, itemId, paneId, active }) => /* @__PURE__ */ jsx6(
2511
+ BodyHost,
2512
+ {
2513
+ hostId,
2514
+ itemId,
2515
+ paneId,
2516
+ visible: shape.isOpen && active,
2517
+ focused: active && shape.focusedPaneId === paneId,
2518
+ isFullscreen: shape.isFullscreen,
2519
+ paneCount: shape.paneCount,
2520
+ slots,
2521
+ placement
2522
+ },
2523
+ hostId
2524
+ )) });
2525
+ }
2526
+ function CanvasPaneInlineBodies({ paneId, slots }) {
2527
+ const { windowId } = useCanvasInternals();
2528
+ const shape = parseSignature(useCanvasState((s) => layerSignature(s, windowId, paneId)));
2529
+ if (!shape) return null;
2530
+ return /* @__PURE__ */ jsx6(Fragment, { children: shape.placed.map(({ hostId, itemId, active }) => /* @__PURE__ */ jsx6(
2531
+ BodyHost,
2532
+ {
2533
+ hostId,
2534
+ itemId,
2535
+ paneId,
2536
+ visible: active,
2537
+ focused: active && shape.focusedPaneId === paneId,
2538
+ isFullscreen: shape.isFullscreen,
2539
+ paneCount: shape.paneCount,
2540
+ slots,
2541
+ placement: null
2542
+ },
2543
+ hostId
2544
+ )) });
2545
+ }
2546
+ var BodyHost = memo(function BodyHost2({
2547
+ hostId,
2548
+ itemId,
2549
+ paneId,
2550
+ visible,
2551
+ focused,
2552
+ isFullscreen,
2553
+ paneCount,
2554
+ slots,
2555
+ placement
2556
+ }) {
2557
+ const canvas = useCanvas();
2558
+ const { windowId, ports } = useCanvasInternals();
2559
+ const item = useCanvasState((s) => s.items[itemId]);
2560
+ const kind = useItemKind(item?.kind ?? "");
2561
+ const element = useRef5(null);
2562
+ const [everShown, setEverShown] = useState4(visible);
2563
+ if (visible && !everShown) setEverShown(true);
2564
+ const mounted = kind?.unmountWhenHidden ? visible : everShown;
2565
+ const presentation = usePanePresentation(slots, paneId, isFullscreen, paneCount);
2566
+ const drop = usePaneDrop(paneId, () => slots.section(paneId));
2567
+ useIsoLayoutEffect(() => {
2568
+ const el = element.current;
2569
+ if (!placement || !el) return void 0;
2570
+ return placement.register(hostId, { element: el, paneId });
2571
+ }, [placement, hostId, paneId]);
2572
+ if (!item) return null;
2573
+ return /* @__PURE__ */ jsx6(
2574
+ "div",
2575
+ {
2576
+ ref: element,
2577
+ className: "mxc-item-host",
2578
+ "data-inline": placement ? void 0 : "",
2579
+ "data-placed": placement ? "false" : void 0,
2580
+ "data-visible": visible ? "true" : "false",
2581
+ "data-kind": item.kind,
2582
+ "data-item": item.id,
2583
+ "data-pane": paneId,
2584
+ inert: !visible,
2585
+ "aria-hidden": visible ? void 0 : true,
2586
+ onPointerDownCapture: () => {
2587
+ if (!focused) canvas.focusPane(paneId);
2588
+ },
2589
+ ...drop,
2590
+ children: mounted ? /* @__PURE__ */ jsx6(CanvasPresentationContext.Provider, { value: presentation, children: /* @__PURE__ */ jsx6("div", { className: "mxc-item", "data-kind": item.kind, children: /* @__PURE__ */ jsx6(
2591
+ ItemContent,
2592
+ {
2593
+ item,
2594
+ kind,
2595
+ paneId,
2596
+ windowId,
2597
+ focused,
2598
+ visible,
2599
+ presentation,
2600
+ canvas,
2601
+ hasNativePort: !!ports.onNativeBodyFrame
2602
+ }
2603
+ ) }) }) : null
2604
+ }
2605
+ );
2606
+ });
2607
+ function ItemContent({
2608
+ item,
2609
+ kind,
2610
+ paneId,
2611
+ windowId,
2612
+ focused,
2613
+ visible,
2614
+ presentation,
2615
+ canvas,
2616
+ hasNativePort
2617
+ }) {
2618
+ let unavailable = null;
2619
+ try {
2620
+ unavailable = kind?.unavailable?.(item.data, item) ?? null;
2621
+ } catch (error) {
2622
+ console.error(`[@ai-matrx/canvas] "${item.kind}" unavailable() threw`, error);
2623
+ }
2624
+ if (!kind) return /* @__PURE__ */ jsx6(ItemProblem, { message: "This item can no longer be shown.", onClose: () => canvas.close(item.id) });
2625
+ if (unavailable) return /* @__PURE__ */ jsx6(CanvasPanelEmpty, { children: unavailable });
2626
+ const rendered = kindComponent(kind) ? /* @__PURE__ */ jsx6(Rendered, { kind, item, paneId, isFocused: focused, isVisible: visible, canvas, windowId, presentation }) : null;
2627
+ if (kind.body === "native") {
2628
+ return hasNativePort ? /* @__PURE__ */ jsx6(NativeBody, { itemId: item.id, windowId, visible, children: rendered }) : /* @__PURE__ */ jsx6(CanvasPanelEmpty, { children: "Opens in the desktop app" });
2629
+ }
2630
+ return rendered;
2631
+ }
2632
+ var Rendered = memo(function Rendered2({
2633
+ kind,
2634
+ item,
2635
+ paneId,
2636
+ isFocused,
2637
+ isVisible,
2638
+ canvas,
2639
+ windowId,
2640
+ presentation
2641
+ }) {
2642
+ const props = { item, data: item.data, paneId, isFocused, isVisible, canvas, windowId, presentation };
2643
+ return /* @__PURE__ */ jsx6(ItemBoundary, { item, onClose: () => canvas.close(item.id), onRetry: () => resetKindComponent(kind), children: /* @__PURE__ */ jsx6(Suspense, { fallback: /* @__PURE__ */ jsx6("div", { className: "mxc-loading", "aria-busy": "true" }), children: /* @__PURE__ */ jsx6(KindRender, { kind, props }) }) });
2644
+ });
2645
+ function KindRender({ kind, props }) {
2646
+ const Render = kindComponent(kind);
2647
+ if (!Render) throw new Error(`[@ai-matrx/canvas] kind "${kind.id}" has nothing to render.`);
2648
+ return /* @__PURE__ */ jsx6(Render, { ...props });
2649
+ }
2650
+ function NativeBody({ itemId, windowId, visible, children }) {
2651
+ const { layer } = useCanvasInternals();
2652
+ const ref = useRef5(null);
2653
+ const measure = useRef5(() => void 0);
2654
+ measure.current = () => {
2655
+ const el = ref.current;
2656
+ const box = visible && el?.isConnected ? el.getBoundingClientRect() : null;
2657
+ if (!box || box.width <= 0 || box.height <= 0) {
2658
+ layer.native.report(itemId, null, { visible: false, windowId });
2659
+ return;
2660
+ }
2661
+ const rect = { ...toCanvasRect(box), devicePixelRatio: window.devicePixelRatio || 1 };
2662
+ layer.native.report(itemId, rect, { visible: !layer.isBlocked(), windowId });
2663
+ };
2664
+ useIsoLayoutEffect(() => measure.current());
2665
+ useIsoLayoutEffect(() => {
2666
+ const el = ref.current;
2667
+ const run = () => measure.current();
2668
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(run);
2669
+ if (el) observer?.observe(el);
2670
+ const unregister = layer.native.register(run);
2671
+ window.addEventListener("resize", run);
2672
+ return () => {
2673
+ observer?.disconnect();
2674
+ unregister();
2675
+ window.removeEventListener("resize", run);
2676
+ layer.native.report(itemId, null, { visible: false, windowId });
2677
+ };
2678
+ }, [itemId, windowId, layer]);
2679
+ return /* @__PURE__ */ jsx6("div", { ref, className: "mxc-native-body", "data-native-body": itemId, children });
2680
+ }
2681
+ function ItemProblem({ message, onClose, onRetry }) {
2682
+ return /* @__PURE__ */ jsxs5("div", { className: "mxc-problem", role: "alert", children: [
2683
+ /* @__PURE__ */ jsx6(AlertIcon, {}),
2684
+ /* @__PURE__ */ jsx6("p", { children: message }),
2685
+ /* @__PURE__ */ jsxs5("div", { className: "mxc-problem-actions", children: [
2686
+ onRetry ? /* @__PURE__ */ jsx6("button", { type: "button", className: "mxc-text-button", onClick: onRetry, children: "Try again" }) : null,
2687
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mxc-text-button", onClick: onClose, children: "Close tab" })
2688
+ ] })
2689
+ ] });
2690
+ }
2691
+ var ItemBoundary = class extends Component {
2692
+ state = { error: null };
2693
+ static getDerivedStateFromError(error) {
2694
+ return { error };
2695
+ }
2696
+ componentDidCatch(error, info) {
2697
+ console.error(`[@ai-matrx/canvas] "${this.props.item.kind}" failed to render`, error, info.componentStack);
2698
+ }
2699
+ render() {
2700
+ if (this.state.error) {
2701
+ return /* @__PURE__ */ jsx6(
2702
+ ItemProblem,
2703
+ {
2704
+ message: "This item failed to load.",
2705
+ onClose: this.props.onClose,
2706
+ onRetry: () => {
2707
+ this.props.onRetry();
2708
+ this.setState({ error: null });
2709
+ }
2710
+ }
2711
+ );
2712
+ }
2713
+ return this.props.children;
2714
+ }
2715
+ };
2716
+
2717
+ // src/react/CanvasPane.tsx
2718
+ import { Component as Component2, useEffect as useEffect6, useLayoutEffect as useLayoutEffect3, useRef as useRef6, useState as useState5 } from "react";
2719
+ import * as Menu from "@radix-ui/react-dropdown-menu";
2720
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2721
+ var useIsoLayoutEffect2 = typeof window === "undefined" ? useEffect6 : useLayoutEffect3;
1963
2722
  function itemTitle(item, kind) {
1964
2723
  if (item.title) return item.title;
2724
+ if (item.kind === CANVAS_LAUNCHER_KIND) return "New tab";
1965
2725
  if (kind?.title) {
1966
2726
  try {
1967
2727
  const title = kind.title(item.data, item);
@@ -1972,70 +2732,46 @@ function itemTitle(item, kind) {
1972
2732
  return kind?.label ?? item.kind;
1973
2733
  }
1974
2734
  var isMac = () => typeof navigator !== "undefined" && /mac|iphone|ipad/i.test(navigator.platform ?? "");
1975
- function useTabDrag(itemId, title) {
1976
- const { layer } = useCanvasInternals();
1977
- const active = useRef4([]);
1978
- const end = () => {
1979
- for (const dispose of active.current.splice(0)) dispose();
1980
- };
1981
- useEffect4(() => end, []);
1982
- return {
1983
- draggable: true,
1984
- onDragStart: (event) => {
1985
- event.dataTransfer.setData(DRAG_MIME, itemId);
1986
- event.dataTransfer.effectAllowed = "move";
1987
- end();
1988
- active.current.push(layer.beginBlocking(), layer.show({ type: "tab-drag", itemId, title }));
1989
- },
1990
- onDragEnd: end
1991
- };
1992
- }
1993
2735
  function CanvasPaneView({ paneId }) {
1994
2736
  const canvas = useCanvas();
1995
- const { windowId, layer, ports, tabStrip } = useCanvasInternals();
2737
+ const { windowId, tabStrip } = useCanvasInternals();
2738
+ const columnSlots = useCanvasSlots();
2739
+ const [ownSlots] = useState5(createCanvasSlots);
2740
+ const slots = columnSlots ?? ownSlots;
1996
2741
  const pane = useCanvasState((s) => s.panes[paneId]);
1997
- const items = useCanvasState((s) => s.items);
2742
+ const active = useCanvasState((s) => {
2743
+ const activeId = s.panes[paneId]?.activeItemId;
2744
+ return activeId ? s.items[activeId] : void 0;
2745
+ });
1998
2746
  const isFullscreen = useCanvasState((s) => selectCanvasIsFullscreen(s, windowId));
2747
+ const isOpen = useCanvasState((s) => selectCanvasIsOpen(s, windowId));
1999
2748
  const focused = useCanvasState((s) => selectCanvasFocusedPaneId(s, windowId) === paneId);
2000
2749
  const paneCount = useCanvasState((s) => selectCanvasPaneCount(s, windowId));
2001
2750
  const role = useCanvasState((s) => windowId === CANVAS_MAIN_WINDOW ? "main" : s.windows[windowId]?.role ?? "window");
2002
- const active = pane?.activeItemId ? items[pane.activeItemId] : void 0;
2003
- const kind = useCanvasKind(active?.kind ?? "");
2004
- const drop = useRef4(null);
2005
- const sectionRef = useRef4(null);
2006
- useEffect4(() => () => drop.current?.(), []);
2751
+ const canGoBack = useCanvasState((s) => active ? selectCanvasCanGoBack(s, active.id) : false);
2752
+ const kind = useItemKind(active?.kind ?? "");
2753
+ const sectionRef = useRef6(null);
2754
+ const bodyRef = useRef6(null);
2755
+ const presentation = usePanePresentation(slots, paneId, isFullscreen, paneCount);
2756
+ const drop = usePaneDrop(paneId, () => sectionRef.current);
2757
+ useIsoLayoutEffect2(() => {
2758
+ const body = bodyRef.current;
2759
+ const section = sectionRef.current;
2760
+ if (body) slots.set(paneId, body);
2761
+ slots.setSection(paneId, section);
2762
+ return () => {
2763
+ slots.unset(paneId, body);
2764
+ if (slots.section(paneId) === section) slots.setSection(paneId, null);
2765
+ };
2766
+ }, [slots, paneId]);
2007
2767
  if (!pane) return null;
2008
- const kindProps = active && kind ? { item: active, data: active.data, paneId, isFocused: focused, canvas, windowId } : null;
2768
+ const kindProps = active && kind ? { item: active, data: active.data, paneId, isFocused: focused, isVisible: isOpen, canvas, windowId, presentation } : null;
2009
2769
  const Leading = kind?.HeaderLeading;
2010
2770
  const Action = kind?.HeaderAction;
2011
2771
  const Toolbar = kind?.Toolbar;
2012
2772
  const single = tabStrip === "auto" && pane.itemIds.length === 1 && active;
2013
- const hideDrop = () => {
2014
- drop.current?.();
2015
- drop.current = null;
2016
- };
2017
- const onDragOver = (event) => {
2018
- if (!event.dataTransfer.types.includes(DRAG_MIME)) return;
2019
- event.preventDefault();
2020
- event.dataTransfer.dropEffect = "move";
2021
- if (!drop.current && sectionRef.current) {
2022
- drop.current = layer.show({ type: "drop-indicator", rect: toCanvasRect(sectionRef.current.getBoundingClientRect()), paneId });
2023
- }
2024
- };
2025
- const onDragLeave = (event) => {
2026
- const next = event.relatedTarget;
2027
- if (next && sectionRef.current?.contains(next)) return;
2028
- hideDrop();
2029
- };
2030
- const onDrop = (event) => {
2031
- hideDrop();
2032
- const itemId = event.dataTransfer.getData(DRAG_MIME);
2033
- if (!itemId) return;
2034
- event.preventDefault();
2035
- canvas.moveItem(itemId, paneId);
2036
- };
2037
2773
  const closeLabel = role === "pop-out" && paneCount === 1 ? "Close window" : paneCount > 1 ? "Close pane" : "Close canvas";
2038
- return /* @__PURE__ */ jsxs4(
2774
+ return /* @__PURE__ */ jsxs6(
2039
2775
  CanvasPanel,
2040
2776
  {
2041
2777
  ref: sectionRef,
@@ -2044,10 +2780,14 @@ function CanvasPaneView({ paneId }) {
2044
2780
  title: active ? itemTitle(active, kind) : "Canvas",
2045
2781
  focused,
2046
2782
  "data-mxc-pane": paneId,
2047
- header: single && active ? /* @__PURE__ */ jsx5(SingleTitle, { item: active, kind }) : pane.itemIds.length > 0 ? /* @__PURE__ */ jsx5("div", { className: "mxc-tabs", role: "tablist", "aria-label": "Canvas tabs", children: pane.itemIds.map((itemId, index) => /* @__PURE__ */ jsx5(ItemTab, { itemId, paneId, index, active: itemId === pane.activeItemId }, itemId)) }) : /* @__PURE__ */ jsx5("span", { className: "mxc-panel-title", children: "New tab" }),
2048
- leading: Leading && kindProps ? /* @__PURE__ */ jsx5(Leading, { ...kindProps }) : null,
2049
- actions: Action && kindProps ? /* @__PURE__ */ jsx5(Action, { ...kindProps }) : null,
2050
- renderMenu: () => /* @__PURE__ */ jsx5(PaneMenu, { paneId, windowId, activeItem: active, kind, kindProps, tabCount: pane.itemIds.length, role }),
2783
+ header: /* @__PURE__ */ jsxs6(Fragment2, { children: [
2784
+ canGoBack && active ? /* @__PURE__ */ jsx7(CanvasHeaderButton, { className: "mxc-back", label: "Back", onClick: () => void canvas.back(active.id), children: /* @__PURE__ */ jsx7(ArrowLeftIcon, {}) }) : null,
2785
+ single && active ? /* @__PURE__ */ jsx7(SingleTitle, { item: active, kind }) : pane.itemIds.length > 0 ? /* @__PURE__ */ jsx7(TabStrip, { paneId, itemIds: pane.itemIds, activeItemId: pane.activeItemId }) : /* @__PURE__ */ jsx7("span", { className: "mxc-panel-title", children: "New tab" }),
2786
+ /* @__PURE__ */ jsx7(CanvasHeaderButton, { className: "mxc-new-tab", label: "New tab", onClick: () => void canvas.newTab(paneId), children: /* @__PURE__ */ jsx7(PlusIcon, {}) })
2787
+ ] }),
2788
+ leading: Leading && kindProps ? /* @__PURE__ */ jsx7(Leading, { ...kindProps }) : null,
2789
+ actions: Action && kindProps ? /* @__PURE__ */ jsx7(Action, { ...kindProps }) : null,
2790
+ renderMenu: () => /* @__PURE__ */ jsx7(PaneMenu, { paneId, windowId, activeItem: active, kind, kindProps, tabCount: pane.itemIds.length, role }),
2051
2791
  expanded: isFullscreen,
2052
2792
  onToggleExpand: () => canvas.setFullscreen(!isFullscreen),
2053
2793
  expandShortcut: canvasShortcutHint("canvas.toggleFullscreen", isMac()),
@@ -2057,22 +2797,15 @@ function CanvasPaneView({ paneId }) {
2057
2797
  onPointerDownCapture: () => {
2058
2798
  if (!focused) canvas.focusPane(paneId);
2059
2799
  },
2060
- onDragOver,
2061
- onDragLeave,
2062
- onDrop,
2800
+ ...drop,
2063
2801
  children: [
2064
- Toolbar && kindProps ? /* @__PURE__ */ jsx5("div", { className: "mxc-item-toolbar", "data-kind": active?.kind, children: /* @__PURE__ */ jsx5(ToolbarBoundary, { kindId: kind?.id ?? "", children: /* @__PURE__ */ jsx5(Toolbar, { ...kindProps }) }) }) : null,
2065
- pane.itemIds.length === 0 ? /* @__PURE__ */ jsx5(Launcher, { paneId }) : pane.itemIds.map((itemId) => {
2066
- const item = items[itemId];
2067
- if (!item) return null;
2068
- const isActive = itemId === pane.activeItemId;
2069
- return /* @__PURE__ */ jsx5(ItemBody, { item, paneId, windowId, isActive, isFocused: focused && isActive, hasNativePort: !!ports.onNativeBodyFrame }, itemId);
2070
- })
2802
+ Toolbar && kindProps ? /* @__PURE__ */ jsx7("div", { className: "mxc-item-toolbar", "data-kind": active?.kind, children: /* @__PURE__ */ jsx7(ToolbarBoundary, { kindId: kind?.id ?? "", children: /* @__PURE__ */ jsx7(Toolbar, { ...kindProps }) }) }) : null,
2803
+ /* @__PURE__ */ jsx7("div", { ref: bodyRef, className: "mxc-pane-slot", "data-mxc-slot": paneId, children: pane.itemIds.length === 0 ? /* @__PURE__ */ jsx7(EmptyPaneLauncher, { paneId }) : columnSlots ? null : /* @__PURE__ */ jsx7(CanvasPaneInlineBodies, { paneId, slots }) })
2071
2804
  ]
2072
2805
  }
2073
2806
  );
2074
2807
  }
2075
- var ToolbarBoundary = class extends Component {
2808
+ var ToolbarBoundary = class extends Component2 {
2076
2809
  state = { failed: false };
2077
2810
  static getDerivedStateFromError() {
2078
2811
  return { failed: true };
@@ -2085,41 +2818,71 @@ var ToolbarBoundary = class extends Component {
2085
2818
  }
2086
2819
  };
2087
2820
  function SingleTitle({ item, kind }) {
2821
+ const canvas = useCanvas();
2088
2822
  const title = itemTitle(item, kind);
2089
2823
  const drag = useTabDrag(item.id, title);
2090
2824
  const Icon = kind?.icon ?? AlertIcon;
2091
- return /* @__PURE__ */ jsxs4("span", { className: "mxc-panel-title", "data-draggable": "", title, ...drag, children: [
2092
- /* @__PURE__ */ jsx5(Icon, { className: "mxc-title-icon" }),
2093
- /* @__PURE__ */ jsx5("span", { className: "mxc-tab-title", children: title })
2825
+ return /* @__PURE__ */ jsxs6("span", { className: "mxc-panel-title mxc-single-title", "data-draggable": "", title, ...drag, children: [
2826
+ /* @__PURE__ */ jsx7(Icon, { className: "mxc-title-icon" }),
2827
+ /* @__PURE__ */ jsx7("span", { className: "mxc-tab-title", children: title }),
2828
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "mxc-tab-close", "data-slot": "browser-tab-close", "aria-label": `Close ${title}`, onClick: () => canvas.close(item.id), children: /* @__PURE__ */ jsx7(CloseIcon, {}) })
2094
2829
  ] });
2095
2830
  }
2831
+ function TabStrip({ paneId, itemIds, activeItemId }) {
2832
+ const strip = useRef6(null);
2833
+ useIsoLayoutEffect2(() => {
2834
+ const el = strip.current;
2835
+ if (!el) return void 0;
2836
+ const reveal = () => {
2837
+ const tab = el.querySelector('[role="tab"][aria-selected="true"]');
2838
+ if (!tab) return;
2839
+ const left = tab.offsetLeft;
2840
+ const right = left + tab.offsetWidth;
2841
+ if (left < el.scrollLeft) el.scrollLeft = left;
2842
+ else if (right > el.scrollLeft + el.clientWidth) el.scrollLeft = right - el.clientWidth;
2843
+ };
2844
+ reveal();
2845
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(reveal);
2846
+ observer?.observe(el);
2847
+ return () => observer?.disconnect();
2848
+ }, [activeItemId, itemIds.length]);
2849
+ return /* @__PURE__ */ jsx7(
2850
+ "div",
2851
+ {
2852
+ ref: strip,
2853
+ className: "mxc-tabs",
2854
+ role: "tablist",
2855
+ "aria-label": "Canvas tabs",
2856
+ onWheel: (event) => {
2857
+ const el = strip.current;
2858
+ if (el && Math.abs(event.deltaY) > Math.abs(event.deltaX)) el.scrollLeft += event.deltaY;
2859
+ },
2860
+ children: itemIds.map((itemId, index) => /* @__PURE__ */ jsx7(ItemTab, { itemId, paneId, index, active: itemId === activeItemId }, itemId))
2861
+ }
2862
+ );
2863
+ }
2096
2864
  function ItemTab({ itemId, paneId, index, active }) {
2097
2865
  const canvas = useCanvas();
2098
2866
  const item = useCanvasState((s) => s.items[itemId]);
2099
- const kind = useCanvasKind(item?.kind ?? "");
2100
- const ref = useRef4(null);
2867
+ const kind = useItemKind(item?.kind ?? "");
2101
2868
  const title = item ? itemTitle(item, kind) : "";
2102
2869
  const drag = useTabDrag(itemId, title);
2103
- useEffect4(() => {
2104
- if (active) ref.current?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
2105
- }, [active]);
2106
2870
  if (!item) return null;
2107
2871
  const Icon = kind?.icon ?? AlertIcon;
2108
- return /* @__PURE__ */ jsx5(
2872
+ return /* @__PURE__ */ jsx7(
2109
2873
  CanvasTab,
2110
2874
  {
2111
- ref,
2112
2875
  title,
2113
- icon: /* @__PURE__ */ jsx5(Icon, {}),
2876
+ icon: /* @__PURE__ */ jsx7(Icon, {}),
2114
2877
  active,
2115
2878
  onSelect: () => canvas.activate(itemId),
2116
2879
  onClose: () => canvas.close(itemId),
2117
2880
  ...drag,
2118
2881
  onDragOver: (event) => {
2119
- if (event.dataTransfer.types.includes(DRAG_MIME)) event.preventDefault();
2882
+ if (event.dataTransfer.types.includes(CANVAS_DRAG_MIME)) event.preventDefault();
2120
2883
  },
2121
2884
  onDrop: (event) => {
2122
- const dragged = event.dataTransfer.getData(DRAG_MIME);
2885
+ const dragged = event.dataTransfer.getData(CANVAS_DRAG_MIME);
2123
2886
  if (!dragged) return;
2124
2887
  event.preventDefault();
2125
2888
  if (dragged !== itemId) canvas.moveItem(dragged, paneId, index);
@@ -2142,16 +2905,16 @@ function PaneMenu({
2142
2905
  const canvas = useCanvas();
2143
2906
  const { ports, layer } = useCanvasInternals();
2144
2907
  const moveId = tabCount > 1 ? activeItem?.id : void 0;
2145
- const blocking = useRef4(null);
2146
- useEffect4(() => () => blocking.current?.(), []);
2908
+ const blocking = useRef6(null);
2909
+ useEffect6(() => () => blocking.current?.(), []);
2147
2910
  const kindItems = kind?.menuItems && kindProps ? safeMenu(() => kind.menuItems?.(kindProps)) : [];
2148
2911
  const hostItems = activeItem && ports.itemActions ? safeMenu(() => ports.itemActions?.(activeItem, kind)) : [];
2149
2912
  const sections = [
2150
2913
  [...kindItems, ...hostItems].map((entry) => ({ ...entry, id: `item:${entry.id}` })),
2151
2914
  [
2152
- { id: "split-right", label: "Split right", icon: /* @__PURE__ */ jsx5(SplitRightIcon, {}), onSelect: () => canvas.splitPane(paneId, "horizontal", moveId) },
2153
- { id: "split-down", label: "Split down", icon: /* @__PURE__ */ jsx5(SplitDownIcon, {}), onSelect: () => canvas.splitPane(paneId, "vertical", moveId) },
2154
- ...activeItem && role === "pop-out" ? [{ id: "dock-back", label: "Dock back", icon: /* @__PURE__ */ jsx5(DockIcon, {}), onSelect: () => void canvas.dockBack(activeItem.id) }] : activeItem && ports.popOut ? [{ id: "pop-out", label: "Pop out", icon: /* @__PURE__ */ jsx5(PopOutIcon, {}), onSelect: () => void canvas.popOut(activeItem.id) }] : []
2915
+ { id: "split-right", label: "Split right", icon: /* @__PURE__ */ jsx7(SplitRightIcon, {}), onSelect: () => canvas.splitPane(paneId, "horizontal", moveId) },
2916
+ { id: "split-down", label: "Split down", icon: /* @__PURE__ */ jsx7(SplitDownIcon, {}), onSelect: () => canvas.splitPane(paneId, "vertical", moveId) },
2917
+ ...activeItem && role === "pop-out" ? [{ id: "dock-back", label: "Dock back", icon: /* @__PURE__ */ jsx7(DockIcon, {}), onSelect: () => void canvas.dockBack(activeItem.id) }] : activeItem && ports.popOut ? [{ id: "pop-out", label: "Pop out", icon: /* @__PURE__ */ jsx7(PopOutIcon, {}), onSelect: () => void canvas.popOut(activeItem.id) }] : []
2155
2918
  ],
2156
2919
  activeItem ? [
2157
2920
  { id: "close-tab", label: "Close tab", onSelect: () => canvas.close(activeItem.id) },
@@ -2169,7 +2932,7 @@ function PaneMenu({
2169
2932
  handlers.set(action.id, action.onSelect);
2170
2933
  }
2171
2934
  });
2172
- return /* @__PURE__ */ jsx5(
2935
+ return /* @__PURE__ */ jsx7(
2173
2936
  CanvasMenuTrigger,
2174
2937
  {
2175
2938
  onClick: (event) => {
@@ -2184,7 +2947,7 @@ function PaneMenu({
2184
2947
  }
2185
2948
  );
2186
2949
  }
2187
- return /* @__PURE__ */ jsxs4(
2950
+ return /* @__PURE__ */ jsxs6(
2188
2951
  Menu.Root,
2189
2952
  {
2190
2953
  onOpenChange: (open) => {
@@ -2192,16 +2955,16 @@ function PaneMenu({
2192
2955
  blocking.current = open ? layer.beginBlocking() : null;
2193
2956
  },
2194
2957
  children: [
2195
- /* @__PURE__ */ jsx5(Menu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx5(CanvasMenuTrigger, {}) }),
2196
- /* @__PURE__ */ jsx5(Menu.Portal, { children: /* @__PURE__ */ jsx5(Menu.Content, { align: "end", sideOffset: 4, className: "mxc-menu", children: sections.map((section, i) => /* @__PURE__ */ jsx5(MenuSection, { first: i === 0, actions: section }, i)) }) })
2958
+ /* @__PURE__ */ jsx7(Menu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx7(CanvasMenuTrigger, {}) }),
2959
+ /* @__PURE__ */ jsx7(Menu.Portal, { children: /* @__PURE__ */ jsx7(Menu.Content, { align: "end", sideOffset: 4, className: "mxc-menu", children: sections.map((section, i) => /* @__PURE__ */ jsx7(MenuSection, { first: i === 0, actions: section }, i)) }) })
2197
2960
  ]
2198
2961
  }
2199
2962
  );
2200
2963
  }
2201
2964
  function MenuSection({ first, actions }) {
2202
- return /* @__PURE__ */ jsxs4(Fragment, { children: [
2203
- first ? null : /* @__PURE__ */ jsx5(Menu.Separator, { className: "mxc-menu-separator" }),
2204
- actions.map((action) => /* @__PURE__ */ jsxs4(Menu.Item, { onSelect: action.onSelect, className: "mxc-menu-item", "data-destructive": action.destructive ? "" : void 0, children: [
2965
+ return /* @__PURE__ */ jsxs6(Fragment2, { children: [
2966
+ first ? null : /* @__PURE__ */ jsx7(Menu.Separator, { className: "mxc-menu-separator" }),
2967
+ actions.map((action) => /* @__PURE__ */ jsxs6(Menu.Item, { onSelect: action.onSelect, className: "mxc-menu-item", "data-destructive": action.destructive ? "" : void 0, children: [
2205
2968
  action.icon,
2206
2969
  action.label
2207
2970
  ] }, action.id))
@@ -2215,150 +2978,16 @@ function safeMenu(build) {
2215
2978
  return [];
2216
2979
  }
2217
2980
  }
2218
- function ItemBody({
2219
- item,
2220
- paneId,
2221
- windowId,
2222
- isActive,
2223
- isFocused,
2224
- hasNativePort
2225
- }) {
2226
- const canvas = useCanvas();
2227
- const kind = useCanvasKind(item.kind);
2228
- if (!isActive && !kind?.keepAlive) return null;
2229
- const props = { item, data: item.data, paneId, isFocused, canvas, windowId };
2230
- let unavailable = null;
2231
- try {
2232
- unavailable = kind?.unavailable?.(item.data, item) ?? null;
2233
- } catch (error) {
2234
- console.error(`[@ai-matrx/canvas] "${item.kind}" unavailable() threw`, error);
2235
- }
2236
- let body;
2237
- if (!kind) {
2238
- body = /* @__PURE__ */ jsx5(ItemProblem, { message: "This item can no longer be shown.", onClose: () => canvas.close(item.id) });
2239
- } else if (unavailable) {
2240
- body = /* @__PURE__ */ jsx5(CanvasPanelEmpty, { children: unavailable });
2241
- } else if (kind.body === "native") {
2242
- body = hasNativePort ? /* @__PURE__ */ jsx5(NativeBody, { itemId: item.id, windowId, children: kindComponent(kind) ? /* @__PURE__ */ jsx5(Rendered, { kind, item, props }) : null }) : /* @__PURE__ */ jsx5(CanvasPanelEmpty, { children: "Opens in the desktop app" });
2243
- } else {
2244
- body = /* @__PURE__ */ jsx5(Rendered, { kind, item, props });
2245
- }
2246
- return /* @__PURE__ */ jsx5("div", { className: "mxc-item", hidden: !isActive, "data-kind": item.kind, children: body });
2247
- }
2248
- function Rendered({ kind, item, props }) {
2249
- const canvas = useCanvas();
2250
- return /* @__PURE__ */ jsx5(ItemBoundary, { item, onClose: () => canvas.close(item.id), onRetry: () => resetKindComponent(kind), children: /* @__PURE__ */ jsx5(Suspense, { fallback: /* @__PURE__ */ jsx5("div", { className: "mxc-loading", "aria-busy": "true" }), children: /* @__PURE__ */ jsx5(KindRender, { kind, props }) }) });
2251
- }
2252
- function KindRender({ kind, props }) {
2253
- const Render = kindComponent(kind);
2254
- if (!Render) throw new Error(`[@ai-matrx/canvas] kind "${kind.id}" has nothing to render.`);
2255
- return /* @__PURE__ */ jsx5(Render, { ...props });
2256
- }
2257
- function NativeBody({ itemId, windowId, children }) {
2258
- const { layer } = useCanvasInternals();
2259
- const ref = useRef4(null);
2260
- const measure = useRef4(() => void 0);
2261
- measure.current = () => {
2262
- const el = ref.current;
2263
- const box = el?.isConnected ? el.getBoundingClientRect() : null;
2264
- if (!box || box.width <= 0 || box.height <= 0) {
2265
- layer.native.report(itemId, null, { visible: false, windowId });
2266
- return;
2267
- }
2268
- const rect = { ...toCanvasRect(box), devicePixelRatio: window.devicePixelRatio || 1 };
2269
- layer.native.report(itemId, rect, { visible: !layer.isBlocked(), windowId });
2270
- };
2271
- useIsoLayoutEffect(() => measure.current());
2272
- useIsoLayoutEffect(() => {
2273
- const el = ref.current;
2274
- const run = () => measure.current();
2275
- const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(run);
2276
- if (el) observer?.observe(el);
2277
- const unregister = layer.native.register(run);
2278
- window.addEventListener("resize", run);
2279
- return () => {
2280
- observer?.disconnect();
2281
- unregister();
2282
- window.removeEventListener("resize", run);
2283
- layer.native.report(itemId, null, { visible: false, windowId });
2284
- };
2285
- }, [itemId, windowId, layer]);
2286
- return /* @__PURE__ */ jsx5("div", { ref, className: "mxc-native-body", "data-native-body": itemId, children });
2287
- }
2288
- function ItemProblem({ message, onClose, onRetry }) {
2289
- return /* @__PURE__ */ jsxs4("div", { className: "mxc-problem", role: "alert", children: [
2290
- /* @__PURE__ */ jsx5(AlertIcon, {}),
2291
- /* @__PURE__ */ jsx5("p", { children: message }),
2292
- /* @__PURE__ */ jsxs4("div", { className: "mxc-problem-actions", children: [
2293
- onRetry ? /* @__PURE__ */ jsx5("button", { type: "button", className: "mxc-text-button", onClick: onRetry, children: "Try again" }) : null,
2294
- /* @__PURE__ */ jsx5("button", { type: "button", className: "mxc-text-button", onClick: onClose, children: "Close tab" })
2295
- ] })
2296
- ] });
2297
- }
2298
- var ItemBoundary = class extends Component {
2299
- state = { error: null };
2300
- static getDerivedStateFromError(error) {
2301
- return { error };
2302
- }
2303
- componentDidCatch(error, info) {
2304
- console.error(`[@ai-matrx/canvas] "${this.props.item.kind}" failed to render`, error, info.componentStack);
2305
- }
2306
- render() {
2307
- if (this.state.error) {
2308
- return /* @__PURE__ */ jsx5(
2309
- ItemProblem,
2310
- {
2311
- message: "This item failed to load.",
2312
- onClose: this.props.onClose,
2313
- onRetry: () => {
2314
- this.props.onRetry();
2315
- this.setState({ error: null });
2316
- }
2317
- }
2318
- );
2319
- }
2320
- return this.props.children;
2321
- }
2322
- };
2323
- function Launcher({ paneId }) {
2324
- const canvas = useCanvas();
2325
- const kinds = useCanvasKinds().filter((kind) => kind.launcher);
2326
- if (kinds.length === 0) return /* @__PURE__ */ jsx5(CanvasPanelEmpty, { children: "Nothing open" });
2327
- return /* @__PURE__ */ jsx5("div", { className: "mxc-launcher", children: kinds.map((kind) => {
2328
- const Icon = kind.icon;
2329
- const launcher = kind.launcher;
2330
- if (!launcher) return null;
2331
- return /* @__PURE__ */ jsxs4(
2332
- "button",
2333
- {
2334
- type: "button",
2335
- className: "mxc-launcher-item",
2336
- onClick: () => canvas.open({
2337
- kind: kind.id,
2338
- key: launcher.key,
2339
- data: launcher.data,
2340
- title: launcher.title ?? null,
2341
- target: { paneId }
2342
- }),
2343
- children: [
2344
- /* @__PURE__ */ jsx5(Icon, { className: "mxc-launcher-icon" }),
2345
- /* @__PURE__ */ jsx5("span", { children: launcher.title ?? kind.label })
2346
- ]
2347
- },
2348
- kind.id
2349
- );
2350
- }) });
2351
- }
2352
2981
 
2353
2982
  // src/react/CanvasColumn.tsx
2354
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2355
- var useIsoLayoutEffect2 = typeof window === "undefined" ? useEffect5 : useLayoutEffect3;
2983
+ import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2984
+ var useIsoLayoutEffect3 = typeof window === "undefined" ? useEffect7 : useLayoutEffect4;
2356
2985
  function subscribeViewport(listener) {
2357
2986
  window.addEventListener("resize", listener);
2358
2987
  return () => window.removeEventListener("resize", listener);
2359
2988
  }
2360
2989
  function useViewportWidth() {
2361
- return useSyncExternalStore3(subscribeViewport, () => window.innerWidth, () => 0);
2990
+ return useSyncExternalStore4(subscribeViewport, () => window.innerWidth, () => 0);
2362
2991
  }
2363
2992
  function useRegionWidth() {
2364
2993
  const viewport = useViewportWidth();
@@ -2376,28 +3005,66 @@ function useCanvasColumnWidth() {
2376
3005
  return fitCanvasWidth(width, region, controller.rules);
2377
3006
  }
2378
3007
  function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
2379
- const { controller, windowId } = useCanvasInternals();
3008
+ const { controller, windowId, layer } = useCanvasInternals();
2380
3009
  const isOpen = useCanvasState((s) => selectCanvasIsOpen(s, windowId));
2381
3010
  const isFullscreen = useCanvasState((s) => selectCanvasIsFullscreen(s, windowId));
2382
3011
  const width = useCanvasState((s) => selectCanvasWidth(s, windowId));
2383
3012
  const layout = useCanvasState((s) => selectCanvasLayout(s, windowId));
2384
3013
  const region = useRegionWidth();
2385
- const [dragWidth, setDragWidth] = useState4(null);
3014
+ const [dragWidth, setDragWidth] = useState6(null);
3015
+ const [slots] = useState6(createCanvasSlots);
3016
+ const ref = useRef7(null);
2386
3017
  useRegisterCanvasPresence();
2387
- if (!isOpen || !layout) return null;
3018
+ useEffect7(() => {
3019
+ const el = ref.current;
3020
+ if (!el) return void 0;
3021
+ let frame2 = 0;
3022
+ let running = false;
3023
+ const tick = () => {
3024
+ layer.native.remeasure();
3025
+ frame2 = running ? requestAnimationFrame(tick) : 0;
3026
+ };
3027
+ const start = (event) => {
3028
+ if (event.target !== el || running) return;
3029
+ running = true;
3030
+ frame2 = requestAnimationFrame(tick);
3031
+ };
3032
+ const stop = (event) => {
3033
+ if (event.target !== el) return;
3034
+ running = false;
3035
+ if (frame2) cancelAnimationFrame(frame2);
3036
+ frame2 = 0;
3037
+ layer.native.remeasure();
3038
+ };
3039
+ el.addEventListener("transitionrun", start);
3040
+ el.addEventListener("transitionend", stop);
3041
+ el.addEventListener("transitioncancel", stop);
3042
+ return () => {
3043
+ running = false;
3044
+ if (frame2) cancelAnimationFrame(frame2);
3045
+ el.removeEventListener("transitionrun", start);
3046
+ el.removeEventListener("transitionend", stop);
3047
+ el.removeEventListener("transitioncancel", stop);
3048
+ };
3049
+ }, [layer]);
3050
+ if (!layout) return null;
2388
3051
  const fitted = fitCanvasWidth(width, region, controller.rules);
2389
3052
  const shown = dragWidth ?? fitted;
2390
- return /* @__PURE__ */ jsxs5(
3053
+ return /* @__PURE__ */ jsx8(
2391
3054
  "aside",
2392
3055
  {
3056
+ ref,
2393
3057
  className: ["mxc-column", className].filter(Boolean).join(" "),
2394
- "data-fullscreen": isFullscreen ? "" : void 0,
3058
+ "data-open": isOpen ? "" : void 0,
3059
+ "data-fullscreen": isOpen && isFullscreen ? "" : void 0,
2395
3060
  "data-placement": placement,
2396
3061
  "data-mxc-window": windowId,
2397
3062
  "aria-label": "Canvas",
3063
+ "aria-hidden": isOpen ? void 0 : true,
3064
+ inert: !isOpen,
2398
3065
  style: { ...style, ["--mxc-width"]: `${shown}px` },
2399
- children: [
2400
- placement === "docked" && !isFullscreen ? /* @__PURE__ */ jsx6(
3066
+ children: /* @__PURE__ */ jsxs7("div", { className: "mxc-column-inner", children: [
3067
+ placement === "docked" && isOpen && !isFullscreen ? /* @__PURE__ */ jsx8(
2401
3068
  WidthHandle,
2402
3069
  {
2403
3070
  width: fitted,
@@ -2408,14 +3075,17 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
2408
3075
  }
2409
3076
  }
2410
3077
  ) : null,
2411
- /* @__PURE__ */ jsx6("div", { className: "mxc-layout", children: /* @__PURE__ */ jsx6(LayoutNodeView, { node: layout }) })
2412
- ]
3078
+ /* @__PURE__ */ jsxs7(CanvasSlotsContext.Provider, { value: slots, children: [
3079
+ /* @__PURE__ */ jsx8("div", { className: "mxc-layout", children: /* @__PURE__ */ jsx8(LayoutNodeView, { node: layout }) }),
3080
+ /* @__PURE__ */ jsx8(CanvasBodyLayer, {})
3081
+ ] })
3082
+ ] })
2413
3083
  }
2414
3084
  );
2415
3085
  }
2416
3086
  function WidthHandle({ width, region, onDrag }) {
2417
3087
  const canvas = useCanvas();
2418
- const start = useRef5(null);
3088
+ const start = useRef7(null);
2419
3089
  const clamp = (next) => clampDraggedCanvasWidth(next, region, canvas.rules);
2420
3090
  const onPointerDown = (event) => {
2421
3091
  event.preventDefault();
@@ -2435,7 +3105,7 @@ function WidthHandle({ width, region, onDrag }) {
2435
3105
  onDrag(null);
2436
3106
  canvas.setWidth(next);
2437
3107
  };
2438
- return /* @__PURE__ */ jsx6(
3108
+ return /* @__PURE__ */ jsx8(
2439
3109
  "div",
2440
3110
  {
2441
3111
  className: "mxc-width-handle",
@@ -2457,12 +3127,12 @@ function WidthHandle({ width, region, onDrag }) {
2457
3127
  );
2458
3128
  }
2459
3129
  function LayoutNodeView({ node }) {
2460
- if (node.type === "pane") return /* @__PURE__ */ jsx6(CanvasPaneView, { paneId: node.paneId });
2461
- return /* @__PURE__ */ jsx6(SplitView, { node });
3130
+ if (node.type === "pane") return /* @__PURE__ */ jsx8(CanvasPaneView, { paneId: node.paneId });
3131
+ return /* @__PURE__ */ jsx8(SplitView, { node });
2462
3132
  }
2463
3133
  function useMeasuredWidth(ref, enabled) {
2464
- const [width, setWidth] = useState4(0);
2465
- useIsoLayoutEffect2(() => {
3134
+ const [width, setWidth] = useState6(0);
3135
+ useIsoLayoutEffect3(() => {
2466
3136
  const el = ref.current;
2467
3137
  if (!enabled || !el) return;
2468
3138
  setWidth(el.getBoundingClientRect().width);
@@ -2475,8 +3145,8 @@ function useMeasuredWidth(ref, enabled) {
2475
3145
  }
2476
3146
  function SplitView({ node }) {
2477
3147
  const canvas = useCanvas();
2478
- const ref = useRef5(null);
2479
- const [liveSizes, setLiveSizes] = useState4(null);
3148
+ const ref = useRef7(null);
3149
+ const [liveSizes, setLiveSizes] = useState6(null);
2480
3150
  const sizes = liveSizes ?? node.sizes;
2481
3151
  const measuredWidth = useMeasuredWidth(ref, node.orientation === "horizontal" && canvas.rules.paneMinWidth > 0);
2482
3152
  const stacked = node.orientation === "horizontal" && shouldStackSplit(measuredWidth, node.children.length, canvas.rules);
@@ -2516,11 +3186,11 @@ function SplitView({ node }) {
2516
3186
  target.addEventListener("pointerup", up);
2517
3187
  target.addEventListener("pointercancel", up);
2518
3188
  };
2519
- return /* @__PURE__ */ jsx6("div", { ref, className: "mxc-split", "data-orientation": orientation, "data-stacked": stacked ? "" : void 0, children: node.children.map((child, i) => /* @__PURE__ */ jsx6(
3189
+ return /* @__PURE__ */ jsx8("div", { ref, className: "mxc-split", "data-orientation": orientation, "data-stacked": stacked ? "" : void 0, children: node.children.map((child, i) => /* @__PURE__ */ jsx8(
2520
3190
  SplitChild,
2521
3191
  {
2522
3192
  size: sizes[i] ?? 1 / node.children.length,
2523
- handle: i < node.children.length - 1 ? /* @__PURE__ */ jsx6(
3193
+ handle: i < node.children.length - 1 ? /* @__PURE__ */ jsx8(
2524
3194
  "div",
2525
3195
  {
2526
3196
  className: "mxc-split-handle",
@@ -2530,14 +3200,14 @@ function SplitView({ node }) {
2530
3200
  onDoubleClick: () => canvas.resizeSplit(node.id, node.children.map(() => 1 / node.children.length))
2531
3201
  }
2532
3202
  ) : null,
2533
- children: /* @__PURE__ */ jsx6(LayoutNodeView, { node: child })
3203
+ children: /* @__PURE__ */ jsx8(LayoutNodeView, { node: child })
2534
3204
  },
2535
3205
  child.type === "pane" ? child.paneId : child.id
2536
3206
  )) });
2537
3207
  }
2538
3208
  function SplitChild({ size, handle, children }) {
2539
- return /* @__PURE__ */ jsxs5(Fragment2, { children: [
2540
- /* @__PURE__ */ jsx6("div", { className: "mxc-split-child", style: { flexGrow: size, flexBasis: 0 }, children }),
3209
+ return /* @__PURE__ */ jsxs7(Fragment3, { children: [
3210
+ /* @__PURE__ */ jsx8("div", { className: "mxc-split-child", style: { flexGrow: size, flexBasis: 0 }, children }),
2541
3211
  handle
2542
3212
  ] });
2543
3213
  }
@@ -2551,26 +3221,61 @@ function CanvasToggle({
2551
3221
  const count = useCanvasState((s) => selectCanvasWindowItemCount(s, windowId));
2552
3222
  const hint = canvasShortcutHint("canvas.toggle", isMac2());
2553
3223
  const Button = variant === "glass" ? TapTargetButton : TapTargetButtonTransparent;
2554
- return /* @__PURE__ */ jsxs5("span", { className: ["mxc-toggle", className].filter(Boolean).join(" "), "data-open": isOpen ? "" : void 0, children: [
2555
- /* @__PURE__ */ jsx6(
3224
+ return /* @__PURE__ */ jsxs7("span", { className: ["mxc-toggle", className].filter(Boolean).join(" "), "data-open": isOpen ? "" : void 0, children: [
3225
+ /* @__PURE__ */ jsx8(
2556
3226
  Button,
2557
3227
  {
2558
3228
  ariaLabel: isOpen ? "Hide canvas" : "Show canvas",
2559
3229
  tooltip: `${isOpen ? "Hide canvas" : "Show canvas"}${hint ? ` (${hint})` : ""}`,
2560
- icon: /* @__PURE__ */ jsx6(PanelRightIcon, {}),
3230
+ icon: /* @__PURE__ */ jsx8(PanelRightIcon, {}),
2561
3231
  onClick: () => controller.toggle()
2562
3232
  }
2563
3233
  ),
2564
- count > 0 && !isOpen ? /* @__PURE__ */ jsx6("span", { className: "mxc-toggle-badge", children: count }) : null
3234
+ count > 0 && !isOpen ? /* @__PURE__ */ jsx8("span", { className: "mxc-toggle-badge", children: count }) : null
2565
3235
  ] });
2566
3236
  }
3237
+ function CanvasKindIcons({
3238
+ kinds,
3239
+ variant = "transparent",
3240
+ className,
3241
+ label = "Canvas tools"
3242
+ }) {
3243
+ return /* @__PURE__ */ jsx8("span", { role: "group", "aria-label": label, className: ["mxc-kind-icons", className].filter(Boolean).join(" "), children: kinds.map((entry) => {
3244
+ const icon = typeof entry === "string" ? { kind: entry } : entry;
3245
+ return /* @__PURE__ */ jsx8(KindIconButton, { icon, variant }, `${icon.kind}::${icon.key ?? CANVAS_DEFAULT_KEY}`);
3246
+ }) });
3247
+ }
3248
+ function KindIconButton({ icon, variant }) {
3249
+ const kind = useCanvasKind(icon.kind);
3250
+ const toggle = useCanvasKindToggle({
3251
+ kind: icon.kind,
3252
+ key: icon.key,
3253
+ data: icon.data,
3254
+ title: icon.title,
3255
+ whenVisible: icon.whenVisible
3256
+ });
3257
+ if (!kind) return null;
3258
+ const Icon = kind.icon;
3259
+ const Button = variant === "glass" ? TapTargetButton : TapTargetButtonTransparent;
3260
+ const name = icon.label ?? kind.label;
3261
+ return /* @__PURE__ */ jsx8(
3262
+ Button,
3263
+ {
3264
+ ariaLabel: name,
3265
+ tooltip: name,
3266
+ icon: /* @__PURE__ */ jsx8(Icon, {}),
3267
+ pressed: toggle.isVisible,
3268
+ onClick: () => void toggle.toggle()
3269
+ }
3270
+ );
3271
+ }
2567
3272
  function CanvasFrame({ children, className }) {
2568
3273
  const { windowId } = useCanvasInternals();
2569
3274
  const isFullscreen = useCanvasState((s) => selectCanvasIsFullscreen(s, windowId));
2570
3275
  const isOpen = useCanvasState((s) => selectCanvasIsOpen(s, windowId));
2571
- return /* @__PURE__ */ jsxs5("div", { className: ["mxc-frame", className].filter(Boolean).join(" "), "data-canvas-open": isOpen ? "" : void 0, children: [
2572
- /* @__PURE__ */ jsx6("div", { className: "mxc-frame-main", hidden: isOpen && isFullscreen, children }),
2573
- /* @__PURE__ */ jsx6(CanvasColumn, {})
3276
+ return /* @__PURE__ */ jsxs7("div", { className: ["mxc-frame", className].filter(Boolean).join(" "), "data-canvas-open": isOpen ? "" : void 0, children: [
3277
+ /* @__PURE__ */ jsx8("div", { className: "mxc-frame-main", hidden: isOpen && isFullscreen, children }),
3278
+ /* @__PURE__ */ jsx8(CanvasColumn, {})
2574
3279
  ] });
2575
3280
  }
2576
3281
  export {
@@ -2579,6 +3284,7 @@ export {
2579
3284
  CanvasColumn,
2580
3285
  CanvasFrame,
2581
3286
  CanvasHeaderButton,
3287
+ CanvasKindIcons,
2582
3288
  CanvasMenuTrigger,
2583
3289
  CanvasPaneView,
2584
3290
  CanvasPanel,
@@ -2601,7 +3307,10 @@ export {
2601
3307
  useCanvasKind,
2602
3308
  useCanvasKindToggle,
2603
3309
  useCanvasKinds,
3310
+ useCanvasLauncherEntries,
3311
+ useCanvasLauncherEntry,
2604
3312
  useCanvasNativeBodies,
3313
+ useCanvasPresentation,
2605
3314
  useCanvasState,
2606
3315
  useCanvasWindowId,
2607
3316
  useOptionalCanvas,