@ai-matrx/canvas 0.4.0 → 0.5.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.js CHANGED
@@ -13,6 +13,8 @@ import {
13
13
 
14
14
  // src/core/types.ts
15
15
  var CANVAS_MAIN_WINDOW = "main";
16
+ var CANVAS_LAUNCHER_KIND = "canvas.launcher";
17
+ var CANVAS_HISTORY_LIMIT = 50;
16
18
  var CANVAS_MIN_WIDTH = 360;
17
19
  var CANVAS_DEFAULT_WIDTH = 640;
18
20
  var CANVAS_MIN_SPLIT_FRACTION = 0.12;
@@ -171,6 +173,10 @@ var canvasActions = {
171
173
  }),
172
174
  /** Gives an item a new identity in place (e.g. a draft that was just saved and now has an id). */
173
175
  rekey: (itemId, key) => ({ type: `${P}rekey`, payload: { itemId, key } }),
176
+ /** Replaces a tab's content with another item in place, remembering what it showed (Back returns). */
177
+ navigate: (itemId, to) => ({ type: `${P}navigate`, payload: { itemId, to, now: Date.now() } }),
178
+ /** Returns a tab to what it showed before its last navigate. */
179
+ back: (itemId) => ({ type: `${P}back`, payload: { itemId, now: Date.now() } }),
174
180
  closeItem: (itemId) => ({ type: `${P}closeItem`, payload: { itemId } }),
175
181
  closeOthers: (itemId) => ({ type: `${P}closeOthers`, payload: { itemId } }),
176
182
  activate: (itemId) => ({ type: `${P}activate`, payload: { itemId } }),
@@ -367,6 +373,63 @@ function sendHome(state, itemId, home, activate) {
367
373
  next = insertInto(next, homeFrame.focusedPaneId, itemId, activate);
368
374
  return activate ? withFrame(next, target, { focusedPaneId: homeFrame.focusedPaneId }) : next;
369
375
  }
376
+ function newItem(kind, key, title, data, now, history) {
377
+ const id = canvasItemId(kind, key);
378
+ return {
379
+ id,
380
+ kind,
381
+ key,
382
+ title,
383
+ data,
384
+ openedAt: now,
385
+ updatedAt: now,
386
+ instance: `${now.toString(36)}:${id}`,
387
+ ...history && history.length > 0 ? { history: history.slice(-CANVAS_HISTORY_LIMIT) } : {}
388
+ };
389
+ }
390
+ function historyEntry(item) {
391
+ return { kind: item.kind, key: item.key, title: item.title, data: item.data };
392
+ }
393
+ function replaceInPlace(state, current, next) {
394
+ const pane = paneOf(state, current.id);
395
+ if (!pane) return state;
396
+ const items = { ...state.items, [next.id]: next };
397
+ delete items[current.id];
398
+ const withItems = { ...state, items };
399
+ const replaced = withPane(withItems, {
400
+ ...pane,
401
+ itemIds: pane.itemIds.map((id) => id === current.id ? next.id : id),
402
+ activeItemId: pane.activeItemId === current.id ? next.id : pane.activeItemId
403
+ });
404
+ return focus(replaced, pane.id);
405
+ }
406
+ function focusExisting(state, itemId) {
407
+ const pane = paneOf(state, itemId);
408
+ if (!pane) return state;
409
+ const next = focus(withPane(state, { ...pane, activeItemId: itemId }), pane.id);
410
+ const windowId = canvasWindowOfPane(next, pane.id);
411
+ return windowId ? withFrame(next, windowId, { isOpen: true }) : next;
412
+ }
413
+ function sanitizeHistory(raw) {
414
+ if (!Array.isArray(raw)) return [];
415
+ const out = [];
416
+ for (const entry of raw) {
417
+ if (!entry || typeof entry !== "object") continue;
418
+ const e = entry;
419
+ if (typeof e.kind !== "string" || typeof e.key !== "string") continue;
420
+ out.push({ kind: e.kind, key: e.key, title: typeof e.title === "string" ? e.title : null, data: e.data ?? null });
421
+ }
422
+ return out.slice(-CANVAS_HISTORY_LIMIT);
423
+ }
424
+ function sanitizeItem(item) {
425
+ const history = sanitizeHistory(item.history);
426
+ const { history: _history, instance, ...rest } = item;
427
+ return {
428
+ ...rest,
429
+ ...typeof instance === "string" && instance ? { instance } : {},
430
+ ...history.length > 0 ? { history } : {}
431
+ };
432
+ }
370
433
  function clampWidth(width) {
371
434
  return Number.isFinite(width) ? Math.max(CANVAS_MIN_WIDTH, Math.round(width)) : null;
372
435
  }
@@ -414,7 +477,7 @@ function sanitizeCanvasSnapshot(raw) {
414
477
  const items = {};
415
478
  for (const [id, item] of Object.entries(s.items)) {
416
479
  if (item && typeof item.kind === "string" && typeof item.key === "string" && canvasItemId(item.kind, item.key) === id) {
417
- items[id] = item;
480
+ items[id] = sanitizeItem(item);
418
481
  }
419
482
  }
420
483
  const placed = /* @__PURE__ */ new Set();
@@ -488,7 +551,7 @@ function canvasReducer(state = createInitialCanvasState(), action) {
488
551
  next = insertInto(next, frame2.focusedPaneId, itemId, true);
489
552
  }
490
553
  } else {
491
- const item = { id: itemId, kind, key, title: title ?? null, data: data ?? null, openedAt: now, updatedAt: now };
554
+ const item = newItem(kind, key, title ?? null, data ?? null, now);
492
555
  next = ensureWindow({ ...next, items: { ...next.items, [itemId]: item } }, revealIn);
493
556
  const frame2 = canvasWindowFrame(next, revealIn) ?? next;
494
557
  let paneId = frame2.focusedPaneId;
@@ -537,6 +600,35 @@ function canvasReducer(state = createInitialCanvasState(), action) {
537
600
  }
538
601
  return { ...state, items, panes };
539
602
  }
603
+ case `${P}navigate`: {
604
+ const { itemId, to, now } = action.payload;
605
+ const current = state.items[itemId];
606
+ if (!current || !paneOf(state, itemId)) return state;
607
+ const targetId = canvasItemId(to.kind, to.key);
608
+ if (targetId === itemId) {
609
+ const refresh = { type: `${P}update`, payload: { itemId, data: to.data, title: to.title, now } };
610
+ return canvasReducer(state, refresh);
611
+ }
612
+ if (state.items[targetId]) {
613
+ const focused = focusExisting(state, targetId);
614
+ return current.kind === CANVAS_LAUNCHER_KIND ? canvasReducer(focused, canvasActions.closeItem(itemId)) : focused;
615
+ }
616
+ const history = [...current.history ?? [], historyEntry(current)];
617
+ return replaceInPlace(state, current, newItem(to.kind, to.key, to.title ?? null, to.data ?? null, now, history));
618
+ }
619
+ case `${P}back`: {
620
+ const { itemId, now } = action.payload;
621
+ const current = state.items[itemId];
622
+ const history = current?.history ?? [];
623
+ const entry = history[history.length - 1];
624
+ if (!current || !entry || !paneOf(state, itemId)) return state;
625
+ const targetId = canvasItemId(entry.kind, entry.key);
626
+ if (targetId === itemId) return state;
627
+ if (state.items[targetId]) {
628
+ return focusExisting(canvasReducer(state, canvasActions.closeItem(itemId)), targetId);
629
+ }
630
+ return replaceInPlace(state, current, newItem(entry.kind, entry.key, entry.title, entry.data, now, history.slice(0, -1)));
631
+ }
540
632
  case `${P}closeItem`: {
541
633
  const { itemId } = action.payload;
542
634
  if (!state.items[itemId]) return state;
@@ -790,7 +882,8 @@ var CANVAS_WEB_LAYOUT_RULES = {
790
882
  defaultWidth: CANVAS_DEFAULT_WIDTH,
791
883
  minWidth: CANVAS_MIN_WIDTH,
792
884
  centreMinWidth: 420,
793
- stackSplit: 0.5,
885
+ // Split down 70/30 like the desktop: what opens below (notifications, a feed) is the smaller pane.
886
+ stackSplit: 0.7,
794
887
  sideSplit: 0.5,
795
888
  paneMinWidth: 0
796
889
  };
@@ -915,7 +1008,8 @@ function createCanvasController(options) {
915
1008
  presentationListeners: /* @__PURE__ */ new Set(),
916
1009
  views: /* @__PURE__ */ new Map(),
917
1010
  started: /* @__PURE__ */ new Map(),
918
- sessionStop: null
1011
+ sessionStop: null,
1012
+ launcherSeq: 0
919
1013
  };
920
1014
  return view(shared, canvasWindowId(options.windowId ?? CANVAS_MAIN_WINDOW));
921
1015
  }
@@ -930,6 +1024,36 @@ function view(shared, windowId) {
930
1024
  for (const listener of [...shared.presentationListeners]) listener();
931
1025
  };
932
1026
  const shareFor = (orientation) => orientation === "vertical" ? rules.stackSplit : rules.sideSplit;
1027
+ const acceptable = (input) => {
1028
+ if (input.kind !== CANVAS_LAUNCHER_KIND && options.isKnownKind && !options.isKnownKind(input.kind)) {
1029
+ onError({ code: "unknown-kind", message: `No canvas kind "${input.kind}" is registered.`, detail: input });
1030
+ return false;
1031
+ }
1032
+ if (input.data !== void 0) {
1033
+ const bad = findNonJson(input.data);
1034
+ if (bad) {
1035
+ onError({
1036
+ code: "non-json-data",
1037
+ message: `Canvas data for "${input.kind}" must be plain JSON; ${bad} is not.`,
1038
+ detail: { kind: input.kind, key: input.key }
1039
+ });
1040
+ return false;
1041
+ }
1042
+ }
1043
+ return true;
1044
+ };
1045
+ const preferredTargetFor = (input) => {
1046
+ const preferred = options.preferredTarget?.(input.kind);
1047
+ if (!preferred || preferred === "focused") return "focused";
1048
+ const state = store.getState();
1049
+ if (canvasItemId(input.kind, input.key) in state.items) return "focused";
1050
+ const frame2 = canvasWindowFrame(state, input.windowId ?? windowId);
1051
+ if (!frame2?.isOpen) return "focused";
1052
+ const panes = listPaneIds(frame2.layout);
1053
+ const holding = panes.find((paneId) => state.panes[paneId]?.itemIds.some((id) => state.items[id]?.kind === input.kind));
1054
+ if (holding) return { paneId: holding };
1055
+ return state.panes[frame2.focusedPaneId]?.activeItemId ? preferred : "focused";
1056
+ };
933
1057
  const controller = {
934
1058
  store,
935
1059
  windowId,
@@ -937,24 +1061,11 @@ function view(shared, windowId) {
937
1061
  forWindow: (id) => view(shared, canvasWindowId(id)),
938
1062
  getState: () => store.getState(),
939
1063
  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";
1064
+ if (!acceptable(input)) return null;
1065
+ const target = input.target ?? preferredTargetFor(input);
956
1066
  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 };
1067
+ const withTarget = input.target === target ? input : { ...input, target };
1068
+ const resolved = withTarget.windowId || windowId === CANVAS_MAIN_WINDOW ? withTarget : { ...withTarget, windowId };
958
1069
  dispatch(canvasActions.open(resolved, splitShare === void 0 ? void 0 : { splitShare }));
959
1070
  return canvasItemId(input.kind, input.key);
960
1071
  },
@@ -970,6 +1081,34 @@ function view(shared, windowId) {
970
1081
  dispatch(canvasActions.update(itemId, patch));
971
1082
  return true;
972
1083
  },
1084
+ navigate(itemId, to) {
1085
+ if (!store.getState().items[itemId] || !acceptable(to)) return null;
1086
+ dispatch(canvasActions.navigate(itemId, to));
1087
+ const shown = canvasItemId(to.kind, to.key);
1088
+ return shown in store.getState().items ? shown : null;
1089
+ },
1090
+ back(itemId) {
1091
+ const history = store.getState().items[itemId]?.history ?? [];
1092
+ const entry = history[history.length - 1];
1093
+ if (!entry) return null;
1094
+ dispatch(canvasActions.back(itemId));
1095
+ return canvasItemId(entry.kind, entry.key);
1096
+ },
1097
+ newTab(paneId) {
1098
+ const state = store.getState();
1099
+ const frame2 = canvasWindowFrame(state, windowId);
1100
+ const target = paneId && state.panes[paneId] ? paneId : frame2?.focusedPaneId ?? state.focusedPaneId;
1101
+ const existing = state.panes[target]?.itemIds.find((id) => state.items[id]?.kind === CANVAS_LAUNCHER_KIND);
1102
+ if (existing) {
1103
+ dispatch(canvasActions.activate(existing));
1104
+ dispatch(canvasActions.setOpen(true, canvasWindowOfItem(store.getState(), existing) ?? windowId));
1105
+ return existing;
1106
+ }
1107
+ shared.launcherSeq += 1;
1108
+ const key = `${Date.now().toString(36)}-${shared.launcherSeq}`;
1109
+ dispatch(canvasActions.open({ kind: CANVAS_LAUNCHER_KIND, key, target: { paneId: target }, ...windowId === CANVAS_MAIN_WINDOW ? {} : { windowId } }));
1110
+ return canvasItemId(CANVAS_LAUNCHER_KIND, key);
1111
+ },
973
1112
  rekey(itemId, key) {
974
1113
  const item = store.getState().items[itemId];
975
1114
  if (!item) return null;
@@ -1219,6 +1358,32 @@ function selectCanvasWindowItemCount(s, windowId) {
1219
1358
  return listPaneIds(layout).reduce((sum, paneId) => sum + (s.panes[paneId]?.itemIds.length ?? 0), 0);
1220
1359
  }
1221
1360
  var NO_WINDOWS = Object.freeze({});
1361
+ var NO_HISTORY = Object.freeze([]);
1362
+ var selectCanvasCanGoBack = (s, itemId) => (s.items[itemId]?.history?.length ?? 0) > 0;
1363
+
1364
+ // src/core/presentation.ts
1365
+ var CANVAS_NARROW_WIDTH = 480;
1366
+ var SQUARE_BAND = 1.2;
1367
+ function canvasOrientation(width, height) {
1368
+ if (!(width > 0) || !(height > 0)) return "square";
1369
+ const ratio = width / height;
1370
+ if (ratio >= SQUARE_BAND) return "landscape";
1371
+ if (ratio <= 1 / SQUARE_BAND) return "portrait";
1372
+ return "square";
1373
+ }
1374
+ function canvasPresentation(input) {
1375
+ const width = Math.max(0, Math.round(input.width));
1376
+ const height = Math.max(0, Math.round(input.height));
1377
+ return {
1378
+ host: "canvas",
1379
+ width,
1380
+ height,
1381
+ orientation: canvasOrientation(width, height),
1382
+ isNarrow: width > 0 && width < CANVAS_NARROW_WIDTH,
1383
+ isFullscreen: input.isFullscreen,
1384
+ paneCount: Math.max(1, input.paneCount)
1385
+ };
1386
+ }
1222
1387
 
1223
1388
  // src/core/toggleKind.ts
1224
1389
  var CANVAS_DEFAULT_KEY = "default";
@@ -1420,6 +1585,31 @@ function getCanvasKindsVersion() {
1420
1585
 
1421
1586
  // src/react/provider.tsx
1422
1587
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1588
+ function createLauncherEntryStore() {
1589
+ let entries = [];
1590
+ const listeners = /* @__PURE__ */ new Set();
1591
+ const emit = () => {
1592
+ for (const listener of [...listeners]) listener();
1593
+ };
1594
+ return {
1595
+ add(entry) {
1596
+ entries = [...entries, entry];
1597
+ emit();
1598
+ let done = false;
1599
+ return () => {
1600
+ if (done) return;
1601
+ done = true;
1602
+ entries = entries.filter((e) => e !== entry);
1603
+ emit();
1604
+ };
1605
+ },
1606
+ list: () => entries,
1607
+ subscribe(listener) {
1608
+ listeners.add(listener);
1609
+ return () => listeners.delete(listener);
1610
+ }
1611
+ };
1612
+ }
1423
1613
  var PHONE_MAX_WIDTH = 767;
1424
1614
  var isPhoneViewport = () => typeof window !== "undefined" && window.innerWidth <= PHONE_MAX_WIDTH;
1425
1615
  var isMacPlatform = () => typeof navigator !== "undefined" && /mac|iphone|ipad/i.test(navigator.userAgentData?.platform ?? navigator.platform ?? "");
@@ -1452,7 +1642,7 @@ function CanvasProvider({
1452
1642
  }) {
1453
1643
  const portsRef = useRef2(hostPorts);
1454
1644
  portsRef.current = hostPorts;
1455
- const [{ controller, layer }] = useState2(() => ({
1645
+ const [{ controller, layer, launcherEntries }] = useState2(() => ({
1456
1646
  controller: createCanvasController({
1457
1647
  store: store ?? createCanvasStore(),
1458
1648
  persistence: persistence === void 0 ? createLocalStorageCanvasPersistence() : persistence,
@@ -1462,9 +1652,11 @@ function CanvasProvider({
1462
1652
  windowId: windowIdProp ?? CANVAS_MAIN_WINDOW,
1463
1653
  layoutRules,
1464
1654
  windowPorts: () => portsRef.current,
1465
- sessions: sessionHooksFor
1655
+ sessions: sessionHooksFor,
1656
+ preferredTarget: (kind) => getCanvasKind(kind)?.preferredTarget
1466
1657
  }),
1467
- layer: createCanvasLayer(() => portsRef.current)
1658
+ layer: createCanvasLayer(() => portsRef.current),
1659
+ launcherEntries: createLauncherEntryStore()
1468
1660
  }));
1469
1661
  const windowId = controller.windowId;
1470
1662
  useEffect2(() => controller.start(), [controller]);
@@ -1489,7 +1681,7 @@ function CanvasProvider({
1489
1681
  window.addEventListener("keydown", onKey);
1490
1682
  return () => window.removeEventListener("keydown", onKey);
1491
1683
  }, [controller, hotkeys, windowId]);
1492
- const value = { controller, windowId, ports: hostPorts, layer, regionWidth, tabStrip };
1684
+ const value = { controller, windowId, ports: hostPorts, layer, regionWidth, tabStrip, launcherEntries };
1493
1685
  return /* @__PURE__ */ jsxs(CanvasContext.Provider, { value, children: [
1494
1686
  children,
1495
1687
  hostPorts.overlay ? null : /* @__PURE__ */ jsx2(CanvasDomOverlay, { layer })
@@ -1576,27 +1768,54 @@ function useCanvasKind(id) {
1576
1768
  useSyncExternalStore2(subscribeCanvasKinds, getCanvasKindsVersion, getCanvasKindsVersion);
1577
1769
  return getCanvasKind(id);
1578
1770
  }
1771
+ var NO_ENTRIES = Object.freeze([]);
1772
+ var noEntries = () => NO_ENTRIES;
1773
+ function useCanvasLauncherEntry(entry) {
1774
+ const store = useContext(CanvasContext)?.launcherEntries;
1775
+ const fingerprint = JSON.stringify([entry.kind, entry.key, entry.title ?? null, entry.data ?? null]);
1776
+ const icon = entry.icon;
1777
+ useEffect2(() => {
1778
+ if (!store) return void 0;
1779
+ const [kind, key, title, data] = JSON.parse(fingerprint);
1780
+ return store.add({ kind, key, title, data, ...icon ? { icon } : {} });
1781
+ }, [store, fingerprint, icon]);
1782
+ }
1783
+ function useCanvasLauncherEntries() {
1784
+ const store = useContext(CanvasContext)?.launcherEntries;
1785
+ const all = useSyncExternalStore2(store ? store.subscribe : NO_SUBSCRIPTION, store ? store.list : noEntries, store ? store.list : noEntries);
1786
+ const seen = /* @__PURE__ */ new Set();
1787
+ const out = [];
1788
+ for (let i = all.length - 1; i >= 0; i -= 1) {
1789
+ const entry = all[i];
1790
+ if (!entry || seen.has(`${entry.kind}::${entry.key}`)) continue;
1791
+ seen.add(`${entry.kind}::${entry.key}`);
1792
+ out.unshift(entry);
1793
+ }
1794
+ return out;
1795
+ }
1579
1796
 
1580
1797
  // src/react/CanvasColumn.tsx
1581
1798
  import {
1582
- useEffect as useEffect5,
1583
- useLayoutEffect as useLayoutEffect3,
1584
- useRef as useRef5,
1585
- useState as useState4,
1586
- useSyncExternalStore as useSyncExternalStore3
1799
+ useEffect as useEffect7,
1800
+ useLayoutEffect as useLayoutEffect4,
1801
+ useRef as useRef7,
1802
+ useState as useState6,
1803
+ useSyncExternalStore as useSyncExternalStore4
1587
1804
  } from "react";
1588
1805
  import { TapTargetButton, TapTargetButtonTransparent } from "@ai-matrx/tap-target";
1589
1806
 
1590
- // src/react/CanvasPane.tsx
1807
+ // src/react/bodies.tsx
1591
1808
  import {
1592
1809
  Component,
1593
1810
  Suspense,
1594
1811
  lazy,
1595
- useEffect as useEffect4,
1812
+ memo,
1813
+ useEffect as useEffect5,
1596
1814
  useLayoutEffect as useLayoutEffect2,
1597
- useRef as useRef4
1815
+ useMemo,
1816
+ useRef as useRef5,
1817
+ useState as useState4
1598
1818
  } from "react";
1599
- import * as Menu from "@radix-ui/react-dropdown-menu";
1600
1819
 
1601
1820
  // src/react/chrome.tsx
1602
1821
  import {
@@ -1696,6 +1915,12 @@ var DockIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
1696
1915
  /* @__PURE__ */ jsx3("path", { d: "M21 3 12 12" }),
1697
1916
  /* @__PURE__ */ jsx3("path", { d: "M12 6v6h6" })
1698
1917
  ] });
1918
+ var LayoutGridIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
1919
+ /* @__PURE__ */ jsx3("rect", { width: "7", height: "7", x: "3", y: "3", rx: "1" }),
1920
+ /* @__PURE__ */ jsx3("rect", { width: "7", height: "7", x: "14", y: "3", rx: "1" }),
1921
+ /* @__PURE__ */ jsx3("rect", { width: "7", height: "7", x: "14", y: "14", rx: "1" }),
1922
+ /* @__PURE__ */ jsx3("rect", { width: "7", height: "7", x: "3", y: "14", rx: "1" })
1923
+ ] });
1699
1924
 
1700
1925
  // src/react/chrome.tsx
1701
1926
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -1946,10 +2171,225 @@ function BrowserBar({ url, canGoBack, canGoForward, onBack, onForward, onReload,
1946
2171
  ] });
1947
2172
  }
1948
2173
 
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;
2174
+ // src/react/dnd.ts
2175
+ import { useEffect as useEffect4, useRef as useRef4 } from "react";
2176
+ var CANVAS_DRAG_MIME = "application/x-matrx-canvas-item";
2177
+ function useTabDrag(itemId, title) {
2178
+ const { layer } = useCanvasInternals();
2179
+ const active = useRef4([]);
2180
+ const end = () => {
2181
+ for (const dispose of active.current.splice(0)) dispose();
2182
+ };
2183
+ useEffect4(() => end, []);
2184
+ return {
2185
+ draggable: true,
2186
+ onDragStart: (event) => {
2187
+ event.dataTransfer.setData(CANVAS_DRAG_MIME, itemId);
2188
+ event.dataTransfer.effectAllowed = "move";
2189
+ end();
2190
+ active.current.push(layer.beginBlocking(), layer.show({ type: "tab-drag", itemId, title }));
2191
+ },
2192
+ onDragEnd: end
2193
+ };
2194
+ }
2195
+ function usePaneDrop(paneId, getOutline) {
2196
+ const canvas = useCanvas();
2197
+ const { layer } = useCanvasInternals();
2198
+ const drop = useRef4(null);
2199
+ useEffect4(() => () => drop.current?.(), []);
2200
+ const hide = () => {
2201
+ drop.current?.();
2202
+ drop.current = null;
2203
+ };
2204
+ return {
2205
+ onDragOver: (event) => {
2206
+ if (!event.dataTransfer.types.includes(CANVAS_DRAG_MIME)) return;
2207
+ event.preventDefault();
2208
+ event.dataTransfer.dropEffect = "move";
2209
+ const outline = getOutline();
2210
+ if (!drop.current && outline) {
2211
+ drop.current = layer.show({ type: "drop-indicator", rect: toCanvasRect(outline.getBoundingClientRect()), paneId });
2212
+ }
2213
+ },
2214
+ onDragLeave: (event) => {
2215
+ const next = event.relatedTarget;
2216
+ if (next && event.currentTarget.contains(next)) return;
2217
+ hide();
2218
+ },
2219
+ onDrop: (event) => {
2220
+ hide();
2221
+ const itemId = event.dataTransfer.getData(CANVAS_DRAG_MIME);
2222
+ if (!itemId) return;
2223
+ event.preventDefault();
2224
+ canvas.moveItem(itemId, paneId);
2225
+ }
2226
+ };
2227
+ }
2228
+
2229
+ // src/react/launcher.tsx
2230
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2231
+ function useChoices() {
2232
+ const entries = useCanvasLauncherEntries();
2233
+ const kinds = useCanvasKinds();
2234
+ const byId = new Map(kinds.map((kind) => [kind.id, kind]));
2235
+ const contextual = [];
2236
+ for (const entry of entries) {
2237
+ const kind = byId.get(entry.kind);
2238
+ if (!kind) continue;
2239
+ contextual.push({
2240
+ id: `ctx:${canvasItemId(entry.kind, entry.key)}`,
2241
+ label: entry.title ?? kind.label,
2242
+ icon: entry.icon ?? kind.icon,
2243
+ input: { kind: entry.kind, key: entry.key, title: entry.title ?? null, ...entry.data !== void 0 ? { data: entry.data } : {} }
2244
+ });
2245
+ }
2246
+ const global = [];
2247
+ for (const kind of kinds) {
2248
+ const launcher = kind.launcher;
2249
+ if (!launcher) continue;
2250
+ global.push({
2251
+ id: `kind:${kind.id}`,
2252
+ label: launcher.title ?? kind.label,
2253
+ icon: kind.icon,
2254
+ input: { kind: kind.id, key: launcher.key, data: launcher.data, title: launcher.title ?? null }
2255
+ });
2256
+ }
2257
+ return { contextual, global };
2258
+ }
2259
+ function LauncherGrid({ onChoose }) {
2260
+ const { contextual, global } = useChoices();
2261
+ if (contextual.length === 0 && global.length === 0) return /* @__PURE__ */ jsx5(CanvasPanelEmpty, { children: "Nothing to open" });
2262
+ const section = (label, choices) => choices.length === 0 ? null : /* @__PURE__ */ jsxs4("section", { className: "mxc-launcher-section", "aria-label": label ?? "Open", children: [
2263
+ label ? /* @__PURE__ */ jsx5("h3", { className: "mxc-launcher-heading", children: label }) : null,
2264
+ /* @__PURE__ */ jsx5("div", { className: "mxc-launcher-grid", children: choices.map((choice) => {
2265
+ const Icon = choice.icon;
2266
+ return /* @__PURE__ */ jsxs4("button", { type: "button", className: "mxc-launcher-item", onClick: () => onChoose(choice.input), children: [
2267
+ /* @__PURE__ */ jsx5(Icon, { className: "mxc-launcher-icon" }),
2268
+ /* @__PURE__ */ jsx5("span", { children: choice.label })
2269
+ ] }, choice.id);
2270
+ }) })
2271
+ ] });
2272
+ return /* @__PURE__ */ jsxs4("div", { className: "mxc-launcher", children: [
2273
+ section(contextual.length > 0 ? "On this page" : null, contextual),
2274
+ section(contextual.length > 0 ? "Open" : null, global)
2275
+ ] });
2276
+ }
2277
+ function LauncherBody({ item, canvas }) {
2278
+ return /* @__PURE__ */ jsx5(LauncherGrid, { onChoose: (input) => void canvas.navigate(item.id, input) });
2279
+ }
2280
+ function EmptyPaneLauncher({ paneId }) {
2281
+ const canvas = useCanvas();
2282
+ return /* @__PURE__ */ jsx5(
2283
+ LauncherGrid,
2284
+ {
2285
+ onChoose: (input) => {
2286
+ const tab = canvas.newTab(paneId);
2287
+ canvas.navigate(tab, input);
2288
+ }
2289
+ }
2290
+ );
2291
+ }
2292
+ var CANVAS_LAUNCHER_KIND_DEFINITION = {
2293
+ id: CANVAS_LAUNCHER_KIND,
2294
+ label: "New tab",
2295
+ icon: LayoutGridIcon,
2296
+ component: LauncherBody
2297
+ };
2298
+ function useItemKind(kindId) {
2299
+ const registered = useCanvasKind(kindId);
2300
+ return kindId === CANVAS_LAUNCHER_KIND ? CANVAS_LAUNCHER_KIND_DEFINITION : registered;
2301
+ }
2302
+
2303
+ // src/react/presentation.tsx
2304
+ import { createContext as createContext2, useContext as useContext2 } from "react";
2305
+ var CanvasPresentationContext = createContext2(null);
2306
+ function useCanvasPresentation() {
2307
+ return useContext2(CanvasPresentationContext);
2308
+ }
2309
+
2310
+ // src/react/slots.ts
2311
+ import { createContext as createContext3, useContext as useContext3, useSyncExternalStore as useSyncExternalStore3 } from "react";
2312
+ var ZERO = Object.freeze({ width: 0, height: 0 });
2313
+ function createCanvasSlots() {
2314
+ const slots = /* @__PURE__ */ new Map();
2315
+ const sections = /* @__PURE__ */ new Map();
2316
+ const sizes = /* @__PURE__ */ new Map();
2317
+ const listeners = /* @__PURE__ */ new Set();
2318
+ let observer = null;
2319
+ const emit = () => {
2320
+ for (const listener of [...listeners]) listener();
2321
+ };
2322
+ const read = (paneId, element) => {
2323
+ const box = element.getBoundingClientRect();
2324
+ const width = Math.round(box.width);
2325
+ const height = Math.round(box.height);
2326
+ const previous = sizes.get(paneId);
2327
+ if (previous && previous.width === width && previous.height === height) return false;
2328
+ sizes.set(paneId, { width, height });
2329
+ return true;
2330
+ };
2331
+ const measureAll = () => {
2332
+ let changed = false;
2333
+ for (const [paneId, element] of slots) changed = read(paneId, element) || changed;
2334
+ return changed;
2335
+ };
2336
+ const watch = () => {
2337
+ if (observer || typeof ResizeObserver === "undefined") return observer;
2338
+ observer = new ResizeObserver(() => {
2339
+ measureAll();
2340
+ emit();
2341
+ });
2342
+ return observer;
2343
+ };
2344
+ return {
2345
+ set(paneId, element) {
2346
+ if (slots.get(paneId) === element) return;
2347
+ const previous = slots.get(paneId);
2348
+ if (previous) observer?.unobserve(previous);
2349
+ slots.set(paneId, element);
2350
+ watch()?.observe(element);
2351
+ read(paneId, element);
2352
+ emit();
2353
+ },
2354
+ unset(paneId, element) {
2355
+ const current = slots.get(paneId);
2356
+ if (!current || element && current !== element) return;
2357
+ observer?.unobserve(current);
2358
+ slots.delete(paneId);
2359
+ sizes.delete(paneId);
2360
+ emit();
2361
+ },
2362
+ setSection(paneId, element) {
2363
+ if (element) sections.set(paneId, element);
2364
+ else sections.delete(paneId);
2365
+ },
2366
+ slot: (paneId) => slots.get(paneId) ?? null,
2367
+ section: (paneId) => sections.get(paneId) ?? null,
2368
+ size: (paneId) => sizes.get(paneId) ?? ZERO,
2369
+ measure() {
2370
+ if (measureAll()) emit();
2371
+ },
2372
+ subscribe(listener) {
2373
+ listeners.add(listener);
2374
+ return () => listeners.delete(listener);
2375
+ }
2376
+ };
2377
+ }
2378
+ var CanvasSlotsContext = createContext3(null);
2379
+ function useCanvasSlots() {
2380
+ return useContext3(CanvasSlotsContext);
2381
+ }
2382
+ function useSlotSize(slots, paneId) {
2383
+ return useSyncExternalStore3(
2384
+ slots.subscribe,
2385
+ () => slots.size(paneId),
2386
+ () => ZERO
2387
+ );
2388
+ }
2389
+
2390
+ // src/react/bodies.tsx
2391
+ import { Fragment, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2392
+ var useIsoLayoutEffect = typeof window === "undefined" ? useEffect5 : useLayoutEffect2;
1953
2393
  var LAZY = /* @__PURE__ */ Symbol.for("ai-matrx.canvas.lazy");
1954
2394
  function kindComponent(kind) {
1955
2395
  if (kind.component) return kind.component;
@@ -1960,8 +2400,294 @@ function kindComponent(kind) {
1960
2400
  function resetKindComponent(kind) {
1961
2401
  if (kind) delete kind[LAZY];
1962
2402
  }
2403
+ function usePanePresentation(slots, paneId, isFullscreen, paneCount) {
2404
+ const size = useSlotSize(slots, paneId);
2405
+ return useMemo(
2406
+ () => canvasPresentation({ width: size.width, height: size.height, isFullscreen, paneCount }),
2407
+ [size.width, size.height, isFullscreen, paneCount]
2408
+ );
2409
+ }
2410
+ function placedItems(state, windowId, onlyPane) {
2411
+ const frame2 = canvasWindowFrame(state, windowId);
2412
+ if (!frame2) return [];
2413
+ const out = [];
2414
+ for (const paneId of onlyPane ? [onlyPane] : listPaneIds(frame2.layout)) {
2415
+ const pane = state.panes[paneId];
2416
+ for (const itemId of pane?.itemIds ?? []) {
2417
+ const item = state.items[itemId];
2418
+ if (item) out.push({ item, paneId, active: pane?.activeItemId === itemId });
2419
+ }
2420
+ }
2421
+ return out.sort((a, b) => a.item.openedAt - b.item.openedAt || hostKey(a.item).localeCompare(hostKey(b.item)));
2422
+ }
2423
+ var hostKey = (item) => item.instance ?? item.id;
2424
+ function createPlacement(getRoot, slots, afterPlace) {
2425
+ const hosts = /* @__PURE__ */ new Map();
2426
+ let placing = false;
2427
+ const place = () => {
2428
+ const root = getRoot();
2429
+ if (!root || placing) return;
2430
+ placing = true;
2431
+ try {
2432
+ slots.measure();
2433
+ const base = root.getBoundingClientRect();
2434
+ for (const { element, paneId } of hosts.values()) {
2435
+ const slot = slots.slot(paneId);
2436
+ if (!slot) {
2437
+ element.dataset.placed = "false";
2438
+ continue;
2439
+ }
2440
+ const box = slot.getBoundingClientRect();
2441
+ const style = element.style;
2442
+ style.left = `${box.left - base.left}px`;
2443
+ style.top = `${box.top - base.top}px`;
2444
+ style.width = `${box.width}px`;
2445
+ style.height = `${box.height}px`;
2446
+ element.dataset.placed = "true";
2447
+ }
2448
+ } finally {
2449
+ placing = false;
2450
+ }
2451
+ afterPlace();
2452
+ };
2453
+ return {
2454
+ register(key, entry) {
2455
+ hosts.set(key, entry);
2456
+ return () => {
2457
+ if (hosts.get(key) === entry) hosts.delete(key);
2458
+ };
2459
+ },
2460
+ place
2461
+ };
2462
+ }
2463
+ function CanvasBodyLayer() {
2464
+ const { windowId, layer } = useCanvasInternals();
2465
+ const slots = useCanvasSlots();
2466
+ const state = useCanvasState((s) => s);
2467
+ const root = useRef5(null);
2468
+ const [placement] = useState4(
2469
+ () => slots ? createPlacement(() => root.current, slots, () => layer.native.remeasure()) : null
2470
+ );
2471
+ useIsoLayoutEffect(() => placement?.place());
2472
+ useIsoLayoutEffect(() => slots && placement ? slots.subscribe(placement.place) : void 0, [slots, placement]);
2473
+ if (!slots || !placement) return null;
2474
+ const frame2 = canvasWindowFrame(state, windowId);
2475
+ if (!frame2) return null;
2476
+ const paneCount = listPaneIds(frame2.layout).length;
2477
+ return /* @__PURE__ */ jsx6("div", { ref: root, className: "mxc-bodies", "data-mxc-bodies": windowId, children: placedItems(state, windowId).map(({ item, paneId, active }) => /* @__PURE__ */ jsx6(
2478
+ BodyHost,
2479
+ {
2480
+ hostId: hostKey(item),
2481
+ item,
2482
+ paneId,
2483
+ visible: frame2.isOpen && active,
2484
+ focused: active && frame2.focusedPaneId === paneId,
2485
+ isFullscreen: frame2.isFullscreen,
2486
+ paneCount,
2487
+ slots,
2488
+ placement
2489
+ },
2490
+ hostKey(item)
2491
+ )) });
2492
+ }
2493
+ function CanvasPaneInlineBodies({ paneId, slots }) {
2494
+ const { windowId } = useCanvasInternals();
2495
+ const state = useCanvasState((s) => s);
2496
+ const frame2 = canvasWindowFrame(state, windowId);
2497
+ const paneCount = frame2 ? listPaneIds(frame2.layout).length : 1;
2498
+ return /* @__PURE__ */ jsx6(Fragment, { children: placedItems(state, windowId, paneId).map(({ item, active }) => /* @__PURE__ */ jsx6(
2499
+ BodyHost,
2500
+ {
2501
+ hostId: hostKey(item),
2502
+ item,
2503
+ paneId,
2504
+ visible: active,
2505
+ focused: active && frame2?.focusedPaneId === paneId,
2506
+ isFullscreen: frame2?.isFullscreen ?? false,
2507
+ paneCount,
2508
+ slots,
2509
+ placement: null
2510
+ },
2511
+ hostKey(item)
2512
+ )) });
2513
+ }
2514
+ function BodyHost({
2515
+ hostId,
2516
+ item,
2517
+ paneId,
2518
+ visible,
2519
+ focused,
2520
+ isFullscreen,
2521
+ paneCount,
2522
+ slots,
2523
+ placement
2524
+ }) {
2525
+ const canvas = useCanvas();
2526
+ const { windowId, ports } = useCanvasInternals();
2527
+ const kind = useItemKind(item.kind);
2528
+ const element = useRef5(null);
2529
+ const [everShown, setEverShown] = useState4(visible);
2530
+ if (visible && !everShown) setEverShown(true);
2531
+ const mounted = kind?.unmountWhenHidden ? visible : everShown;
2532
+ const presentation = usePanePresentation(slots, paneId, isFullscreen, paneCount);
2533
+ const drop = usePaneDrop(paneId, () => slots.section(paneId));
2534
+ useIsoLayoutEffect(() => {
2535
+ const el = element.current;
2536
+ if (!placement || !el) return void 0;
2537
+ return placement.register(hostId, { element: el, paneId });
2538
+ }, [placement, hostId, paneId]);
2539
+ return /* @__PURE__ */ jsx6(
2540
+ "div",
2541
+ {
2542
+ ref: element,
2543
+ className: "mxc-item-host",
2544
+ "data-inline": placement ? void 0 : "",
2545
+ "data-placed": placement ? "false" : void 0,
2546
+ "data-visible": visible ? "true" : "false",
2547
+ "data-kind": item.kind,
2548
+ "data-item": item.id,
2549
+ "data-pane": paneId,
2550
+ inert: !visible,
2551
+ "aria-hidden": visible ? void 0 : true,
2552
+ onPointerDownCapture: () => {
2553
+ if (!focused) canvas.focusPane(paneId);
2554
+ },
2555
+ ...drop,
2556
+ children: mounted ? /* @__PURE__ */ jsx6(CanvasPresentationContext.Provider, { value: presentation, children: /* @__PURE__ */ jsx6("div", { className: "mxc-item", "data-kind": item.kind, children: /* @__PURE__ */ jsx6(
2557
+ ItemContent,
2558
+ {
2559
+ item,
2560
+ kind,
2561
+ paneId,
2562
+ windowId,
2563
+ focused,
2564
+ visible,
2565
+ presentation,
2566
+ canvas,
2567
+ hasNativePort: !!ports.onNativeBodyFrame
2568
+ }
2569
+ ) }) }) : null
2570
+ }
2571
+ );
2572
+ }
2573
+ function ItemContent({
2574
+ item,
2575
+ kind,
2576
+ paneId,
2577
+ windowId,
2578
+ focused,
2579
+ visible,
2580
+ presentation,
2581
+ canvas,
2582
+ hasNativePort
2583
+ }) {
2584
+ let unavailable = null;
2585
+ try {
2586
+ unavailable = kind?.unavailable?.(item.data, item) ?? null;
2587
+ } catch (error) {
2588
+ console.error(`[@ai-matrx/canvas] "${item.kind}" unavailable() threw`, error);
2589
+ }
2590
+ if (!kind) return /* @__PURE__ */ jsx6(ItemProblem, { message: "This item can no longer be shown.", onClose: () => canvas.close(item.id) });
2591
+ if (unavailable) return /* @__PURE__ */ jsx6(CanvasPanelEmpty, { children: unavailable });
2592
+ const rendered = kindComponent(kind) ? /* @__PURE__ */ jsx6(Rendered, { kind, item, paneId, isFocused: focused, isVisible: visible, canvas, windowId, presentation }) : null;
2593
+ if (kind.body === "native") {
2594
+ return hasNativePort ? /* @__PURE__ */ jsx6(NativeBody, { itemId: item.id, windowId, visible, children: rendered }) : /* @__PURE__ */ jsx6(CanvasPanelEmpty, { children: "Opens in the desktop app" });
2595
+ }
2596
+ return rendered;
2597
+ }
2598
+ var Rendered = memo(function Rendered2({
2599
+ kind,
2600
+ item,
2601
+ paneId,
2602
+ isFocused,
2603
+ isVisible,
2604
+ canvas,
2605
+ windowId,
2606
+ presentation
2607
+ }) {
2608
+ const props = { item, data: item.data, paneId, isFocused, isVisible, canvas, windowId, presentation };
2609
+ 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 }) }) });
2610
+ });
2611
+ function KindRender({ kind, props }) {
2612
+ const Render = kindComponent(kind);
2613
+ if (!Render) throw new Error(`[@ai-matrx/canvas] kind "${kind.id}" has nothing to render.`);
2614
+ return /* @__PURE__ */ jsx6(Render, { ...props });
2615
+ }
2616
+ function NativeBody({ itemId, windowId, visible, children }) {
2617
+ const { layer } = useCanvasInternals();
2618
+ const ref = useRef5(null);
2619
+ const measure = useRef5(() => void 0);
2620
+ measure.current = () => {
2621
+ const el = ref.current;
2622
+ const box = visible && el?.isConnected ? el.getBoundingClientRect() : null;
2623
+ if (!box || box.width <= 0 || box.height <= 0) {
2624
+ layer.native.report(itemId, null, { visible: false, windowId });
2625
+ return;
2626
+ }
2627
+ const rect = { ...toCanvasRect(box), devicePixelRatio: window.devicePixelRatio || 1 };
2628
+ layer.native.report(itemId, rect, { visible: !layer.isBlocked(), windowId });
2629
+ };
2630
+ useIsoLayoutEffect(() => measure.current());
2631
+ useIsoLayoutEffect(() => {
2632
+ const el = ref.current;
2633
+ const run = () => measure.current();
2634
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(run);
2635
+ if (el) observer?.observe(el);
2636
+ const unregister = layer.native.register(run);
2637
+ window.addEventListener("resize", run);
2638
+ return () => {
2639
+ observer?.disconnect();
2640
+ unregister();
2641
+ window.removeEventListener("resize", run);
2642
+ layer.native.report(itemId, null, { visible: false, windowId });
2643
+ };
2644
+ }, [itemId, windowId, layer]);
2645
+ return /* @__PURE__ */ jsx6("div", { ref, className: "mxc-native-body", "data-native-body": itemId, children });
2646
+ }
2647
+ function ItemProblem({ message, onClose, onRetry }) {
2648
+ return /* @__PURE__ */ jsxs5("div", { className: "mxc-problem", role: "alert", children: [
2649
+ /* @__PURE__ */ jsx6(AlertIcon, {}),
2650
+ /* @__PURE__ */ jsx6("p", { children: message }),
2651
+ /* @__PURE__ */ jsxs5("div", { className: "mxc-problem-actions", children: [
2652
+ onRetry ? /* @__PURE__ */ jsx6("button", { type: "button", className: "mxc-text-button", onClick: onRetry, children: "Try again" }) : null,
2653
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mxc-text-button", onClick: onClose, children: "Close tab" })
2654
+ ] })
2655
+ ] });
2656
+ }
2657
+ var ItemBoundary = class extends Component {
2658
+ state = { error: null };
2659
+ static getDerivedStateFromError(error) {
2660
+ return { error };
2661
+ }
2662
+ componentDidCatch(error, info) {
2663
+ console.error(`[@ai-matrx/canvas] "${this.props.item.kind}" failed to render`, error, info.componentStack);
2664
+ }
2665
+ render() {
2666
+ if (this.state.error) {
2667
+ return /* @__PURE__ */ jsx6(
2668
+ ItemProblem,
2669
+ {
2670
+ message: "This item failed to load.",
2671
+ onClose: this.props.onClose,
2672
+ onRetry: () => {
2673
+ this.props.onRetry();
2674
+ this.setState({ error: null });
2675
+ }
2676
+ }
2677
+ );
2678
+ }
2679
+ return this.props.children;
2680
+ }
2681
+ };
2682
+
2683
+ // src/react/CanvasPane.tsx
2684
+ import { Component as Component2, useEffect as useEffect6, useLayoutEffect as useLayoutEffect3, useRef as useRef6, useState as useState5 } from "react";
2685
+ import * as Menu from "@radix-ui/react-dropdown-menu";
2686
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2687
+ var useIsoLayoutEffect2 = typeof window === "undefined" ? useEffect6 : useLayoutEffect3;
1963
2688
  function itemTitle(item, kind) {
1964
2689
  if (item.title) return item.title;
2690
+ if (item.kind === CANVAS_LAUNCHER_KIND) return "New tab";
1965
2691
  if (kind?.title) {
1966
2692
  try {
1967
2693
  const title = kind.title(item.data, item);
@@ -1972,70 +2698,44 @@ function itemTitle(item, kind) {
1972
2698
  return kind?.label ?? item.kind;
1973
2699
  }
1974
2700
  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
2701
  function CanvasPaneView({ paneId }) {
1994
2702
  const canvas = useCanvas();
1995
- const { windowId, layer, ports, tabStrip } = useCanvasInternals();
2703
+ const { windowId, tabStrip } = useCanvasInternals();
2704
+ const columnSlots = useCanvasSlots();
2705
+ const [ownSlots] = useState5(createCanvasSlots);
2706
+ const slots = columnSlots ?? ownSlots;
1996
2707
  const pane = useCanvasState((s) => s.panes[paneId]);
1997
2708
  const items = useCanvasState((s) => s.items);
1998
2709
  const isFullscreen = useCanvasState((s) => selectCanvasIsFullscreen(s, windowId));
2710
+ const isOpen = useCanvasState((s) => selectCanvasIsOpen(s, windowId));
1999
2711
  const focused = useCanvasState((s) => selectCanvasFocusedPaneId(s, windowId) === paneId);
2000
2712
  const paneCount = useCanvasState((s) => selectCanvasPaneCount(s, windowId));
2001
2713
  const role = useCanvasState((s) => windowId === CANVAS_MAIN_WINDOW ? "main" : s.windows[windowId]?.role ?? "window");
2002
2714
  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?.(), []);
2715
+ const canGoBack = useCanvasState((s) => active ? selectCanvasCanGoBack(s, active.id) : false);
2716
+ const kind = useItemKind(active?.kind ?? "");
2717
+ const sectionRef = useRef6(null);
2718
+ const bodyRef = useRef6(null);
2719
+ const presentation = usePanePresentation(slots, paneId, isFullscreen, paneCount);
2720
+ const drop = usePaneDrop(paneId, () => sectionRef.current);
2721
+ useIsoLayoutEffect2(() => {
2722
+ const body = bodyRef.current;
2723
+ const section = sectionRef.current;
2724
+ if (body) slots.set(paneId, body);
2725
+ slots.setSection(paneId, section);
2726
+ return () => {
2727
+ slots.unset(paneId, body);
2728
+ if (slots.section(paneId) === section) slots.setSection(paneId, null);
2729
+ };
2730
+ }, [slots, paneId]);
2007
2731
  if (!pane) return null;
2008
- const kindProps = active && kind ? { item: active, data: active.data, paneId, isFocused: focused, canvas, windowId } : null;
2732
+ const kindProps = active && kind ? { item: active, data: active.data, paneId, isFocused: focused, isVisible: isOpen, canvas, windowId, presentation } : null;
2009
2733
  const Leading = kind?.HeaderLeading;
2010
2734
  const Action = kind?.HeaderAction;
2011
2735
  const Toolbar = kind?.Toolbar;
2012
2736
  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
2737
  const closeLabel = role === "pop-out" && paneCount === 1 ? "Close window" : paneCount > 1 ? "Close pane" : "Close canvas";
2038
- return /* @__PURE__ */ jsxs4(
2738
+ return /* @__PURE__ */ jsxs6(
2039
2739
  CanvasPanel,
2040
2740
  {
2041
2741
  ref: sectionRef,
@@ -2044,10 +2744,14 @@ function CanvasPaneView({ paneId }) {
2044
2744
  title: active ? itemTitle(active, kind) : "Canvas",
2045
2745
  focused,
2046
2746
  "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 }),
2747
+ header: /* @__PURE__ */ jsxs6(Fragment2, { children: [
2748
+ canGoBack && active ? /* @__PURE__ */ jsx7(CanvasHeaderButton, { className: "mxc-back", label: "Back", onClick: () => void canvas.back(active.id), children: /* @__PURE__ */ jsx7(ArrowLeftIcon, {}) }) : null,
2749
+ 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" }),
2750
+ /* @__PURE__ */ jsx7(CanvasHeaderButton, { className: "mxc-new-tab", label: "New tab", onClick: () => void canvas.newTab(paneId), children: /* @__PURE__ */ jsx7(PlusIcon, {}) })
2751
+ ] }),
2752
+ leading: Leading && kindProps ? /* @__PURE__ */ jsx7(Leading, { ...kindProps }) : null,
2753
+ actions: Action && kindProps ? /* @__PURE__ */ jsx7(Action, { ...kindProps }) : null,
2754
+ renderMenu: () => /* @__PURE__ */ jsx7(PaneMenu, { paneId, windowId, activeItem: active, kind, kindProps, tabCount: pane.itemIds.length, role }),
2051
2755
  expanded: isFullscreen,
2052
2756
  onToggleExpand: () => canvas.setFullscreen(!isFullscreen),
2053
2757
  expandShortcut: canvasShortcutHint("canvas.toggleFullscreen", isMac()),
@@ -2057,22 +2761,15 @@ function CanvasPaneView({ paneId }) {
2057
2761
  onPointerDownCapture: () => {
2058
2762
  if (!focused) canvas.focusPane(paneId);
2059
2763
  },
2060
- onDragOver,
2061
- onDragLeave,
2062
- onDrop,
2764
+ ...drop,
2063
2765
  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
- })
2766
+ 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,
2767
+ /* @__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
2768
  ]
2072
2769
  }
2073
2770
  );
2074
2771
  }
2075
- var ToolbarBoundary = class extends Component {
2772
+ var ToolbarBoundary = class extends Component2 {
2076
2773
  state = { failed: false };
2077
2774
  static getDerivedStateFromError() {
2078
2775
  return { failed: true };
@@ -2085,41 +2782,71 @@ var ToolbarBoundary = class extends Component {
2085
2782
  }
2086
2783
  };
2087
2784
  function SingleTitle({ item, kind }) {
2785
+ const canvas = useCanvas();
2088
2786
  const title = itemTitle(item, kind);
2089
2787
  const drag = useTabDrag(item.id, title);
2090
2788
  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 })
2789
+ return /* @__PURE__ */ jsxs6("span", { className: "mxc-panel-title mxc-single-title", "data-draggable": "", title, ...drag, children: [
2790
+ /* @__PURE__ */ jsx7(Icon, { className: "mxc-title-icon" }),
2791
+ /* @__PURE__ */ jsx7("span", { className: "mxc-tab-title", children: title }),
2792
+ /* @__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
2793
  ] });
2095
2794
  }
2795
+ function TabStrip({ paneId, itemIds, activeItemId }) {
2796
+ const strip = useRef6(null);
2797
+ useIsoLayoutEffect2(() => {
2798
+ const el = strip.current;
2799
+ if (!el) return void 0;
2800
+ const reveal = () => {
2801
+ const tab = el.querySelector('[role="tab"][aria-selected="true"]');
2802
+ if (!tab) return;
2803
+ const left = tab.offsetLeft - el.offsetLeft;
2804
+ const right = left + tab.offsetWidth;
2805
+ if (left < el.scrollLeft) el.scrollLeft = left;
2806
+ else if (right > el.scrollLeft + el.clientWidth) el.scrollLeft = right - el.clientWidth;
2807
+ };
2808
+ reveal();
2809
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(reveal);
2810
+ observer?.observe(el);
2811
+ return () => observer?.disconnect();
2812
+ }, [activeItemId, itemIds.length]);
2813
+ return /* @__PURE__ */ jsx7(
2814
+ "div",
2815
+ {
2816
+ ref: strip,
2817
+ className: "mxc-tabs",
2818
+ role: "tablist",
2819
+ "aria-label": "Canvas tabs",
2820
+ onWheel: (event) => {
2821
+ const el = strip.current;
2822
+ if (el && Math.abs(event.deltaY) > Math.abs(event.deltaX)) el.scrollLeft += event.deltaY;
2823
+ },
2824
+ children: itemIds.map((itemId, index) => /* @__PURE__ */ jsx7(ItemTab, { itemId, paneId, index, active: itemId === activeItemId }, itemId))
2825
+ }
2826
+ );
2827
+ }
2096
2828
  function ItemTab({ itemId, paneId, index, active }) {
2097
2829
  const canvas = useCanvas();
2098
2830
  const item = useCanvasState((s) => s.items[itemId]);
2099
- const kind = useCanvasKind(item?.kind ?? "");
2100
- const ref = useRef4(null);
2831
+ const kind = useItemKind(item?.kind ?? "");
2101
2832
  const title = item ? itemTitle(item, kind) : "";
2102
2833
  const drag = useTabDrag(itemId, title);
2103
- useEffect4(() => {
2104
- if (active) ref.current?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
2105
- }, [active]);
2106
2834
  if (!item) return null;
2107
2835
  const Icon = kind?.icon ?? AlertIcon;
2108
- return /* @__PURE__ */ jsx5(
2836
+ return /* @__PURE__ */ jsx7(
2109
2837
  CanvasTab,
2110
2838
  {
2111
- ref,
2112
2839
  title,
2113
- icon: /* @__PURE__ */ jsx5(Icon, {}),
2840
+ icon: /* @__PURE__ */ jsx7(Icon, {}),
2114
2841
  active,
2115
2842
  onSelect: () => canvas.activate(itemId),
2116
2843
  onClose: () => canvas.close(itemId),
2117
2844
  ...drag,
2118
2845
  onDragOver: (event) => {
2119
- if (event.dataTransfer.types.includes(DRAG_MIME)) event.preventDefault();
2846
+ if (event.dataTransfer.types.includes(CANVAS_DRAG_MIME)) event.preventDefault();
2120
2847
  },
2121
2848
  onDrop: (event) => {
2122
- const dragged = event.dataTransfer.getData(DRAG_MIME);
2849
+ const dragged = event.dataTransfer.getData(CANVAS_DRAG_MIME);
2123
2850
  if (!dragged) return;
2124
2851
  event.preventDefault();
2125
2852
  if (dragged !== itemId) canvas.moveItem(dragged, paneId, index);
@@ -2142,16 +2869,16 @@ function PaneMenu({
2142
2869
  const canvas = useCanvas();
2143
2870
  const { ports, layer } = useCanvasInternals();
2144
2871
  const moveId = tabCount > 1 ? activeItem?.id : void 0;
2145
- const blocking = useRef4(null);
2146
- useEffect4(() => () => blocking.current?.(), []);
2872
+ const blocking = useRef6(null);
2873
+ useEffect6(() => () => blocking.current?.(), []);
2147
2874
  const kindItems = kind?.menuItems && kindProps ? safeMenu(() => kind.menuItems?.(kindProps)) : [];
2148
2875
  const hostItems = activeItem && ports.itemActions ? safeMenu(() => ports.itemActions?.(activeItem, kind)) : [];
2149
2876
  const sections = [
2150
2877
  [...kindItems, ...hostItems].map((entry) => ({ ...entry, id: `item:${entry.id}` })),
2151
2878
  [
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) }] : []
2879
+ { id: "split-right", label: "Split right", icon: /* @__PURE__ */ jsx7(SplitRightIcon, {}), onSelect: () => canvas.splitPane(paneId, "horizontal", moveId) },
2880
+ { id: "split-down", label: "Split down", icon: /* @__PURE__ */ jsx7(SplitDownIcon, {}), onSelect: () => canvas.splitPane(paneId, "vertical", moveId) },
2881
+ ...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
2882
  ],
2156
2883
  activeItem ? [
2157
2884
  { id: "close-tab", label: "Close tab", onSelect: () => canvas.close(activeItem.id) },
@@ -2169,7 +2896,7 @@ function PaneMenu({
2169
2896
  handlers.set(action.id, action.onSelect);
2170
2897
  }
2171
2898
  });
2172
- return /* @__PURE__ */ jsx5(
2899
+ return /* @__PURE__ */ jsx7(
2173
2900
  CanvasMenuTrigger,
2174
2901
  {
2175
2902
  onClick: (event) => {
@@ -2184,7 +2911,7 @@ function PaneMenu({
2184
2911
  }
2185
2912
  );
2186
2913
  }
2187
- return /* @__PURE__ */ jsxs4(
2914
+ return /* @__PURE__ */ jsxs6(
2188
2915
  Menu.Root,
2189
2916
  {
2190
2917
  onOpenChange: (open) => {
@@ -2192,16 +2919,16 @@ function PaneMenu({
2192
2919
  blocking.current = open ? layer.beginBlocking() : null;
2193
2920
  },
2194
2921
  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)) }) })
2922
+ /* @__PURE__ */ jsx7(Menu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx7(CanvasMenuTrigger, {}) }),
2923
+ /* @__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
2924
  ]
2198
2925
  }
2199
2926
  );
2200
2927
  }
2201
2928
  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: [
2929
+ return /* @__PURE__ */ jsxs6(Fragment2, { children: [
2930
+ first ? null : /* @__PURE__ */ jsx7(Menu.Separator, { className: "mxc-menu-separator" }),
2931
+ actions.map((action) => /* @__PURE__ */ jsxs6(Menu.Item, { onSelect: action.onSelect, className: "mxc-menu-item", "data-destructive": action.destructive ? "" : void 0, children: [
2205
2932
  action.icon,
2206
2933
  action.label
2207
2934
  ] }, action.id))
@@ -2215,150 +2942,16 @@ function safeMenu(build) {
2215
2942
  return [];
2216
2943
  }
2217
2944
  }
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
2945
 
2353
2946
  // 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;
2947
+ import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2948
+ var useIsoLayoutEffect3 = typeof window === "undefined" ? useEffect7 : useLayoutEffect4;
2356
2949
  function subscribeViewport(listener) {
2357
2950
  window.addEventListener("resize", listener);
2358
2951
  return () => window.removeEventListener("resize", listener);
2359
2952
  }
2360
2953
  function useViewportWidth() {
2361
- return useSyncExternalStore3(subscribeViewport, () => window.innerWidth, () => 0);
2954
+ return useSyncExternalStore4(subscribeViewport, () => window.innerWidth, () => 0);
2362
2955
  }
2363
2956
  function useRegionWidth() {
2364
2957
  const viewport = useViewportWidth();
@@ -2376,28 +2969,66 @@ function useCanvasColumnWidth() {
2376
2969
  return fitCanvasWidth(width, region, controller.rules);
2377
2970
  }
2378
2971
  function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
2379
- const { controller, windowId } = useCanvasInternals();
2972
+ const { controller, windowId, layer } = useCanvasInternals();
2380
2973
  const isOpen = useCanvasState((s) => selectCanvasIsOpen(s, windowId));
2381
2974
  const isFullscreen = useCanvasState((s) => selectCanvasIsFullscreen(s, windowId));
2382
2975
  const width = useCanvasState((s) => selectCanvasWidth(s, windowId));
2383
2976
  const layout = useCanvasState((s) => selectCanvasLayout(s, windowId));
2384
2977
  const region = useRegionWidth();
2385
- const [dragWidth, setDragWidth] = useState4(null);
2978
+ const [dragWidth, setDragWidth] = useState6(null);
2979
+ const [slots] = useState6(createCanvasSlots);
2980
+ const ref = useRef7(null);
2386
2981
  useRegisterCanvasPresence();
2387
- if (!isOpen || !layout) return null;
2982
+ useEffect7(() => {
2983
+ const el = ref.current;
2984
+ if (!el) return void 0;
2985
+ let frame2 = 0;
2986
+ let running = false;
2987
+ const tick = () => {
2988
+ layer.native.remeasure();
2989
+ frame2 = running ? requestAnimationFrame(tick) : 0;
2990
+ };
2991
+ const start = (event) => {
2992
+ if (event.target !== el || running) return;
2993
+ running = true;
2994
+ frame2 = requestAnimationFrame(tick);
2995
+ };
2996
+ const stop = (event) => {
2997
+ if (event.target !== el) return;
2998
+ running = false;
2999
+ if (frame2) cancelAnimationFrame(frame2);
3000
+ frame2 = 0;
3001
+ layer.native.remeasure();
3002
+ };
3003
+ el.addEventListener("transitionrun", start);
3004
+ el.addEventListener("transitionend", stop);
3005
+ el.addEventListener("transitioncancel", stop);
3006
+ return () => {
3007
+ running = false;
3008
+ if (frame2) cancelAnimationFrame(frame2);
3009
+ el.removeEventListener("transitionrun", start);
3010
+ el.removeEventListener("transitionend", stop);
3011
+ el.removeEventListener("transitioncancel", stop);
3012
+ };
3013
+ }, [layer]);
3014
+ if (!layout) return null;
2388
3015
  const fitted = fitCanvasWidth(width, region, controller.rules);
2389
3016
  const shown = dragWidth ?? fitted;
2390
- return /* @__PURE__ */ jsxs5(
3017
+ return /* @__PURE__ */ jsx8(
2391
3018
  "aside",
2392
3019
  {
3020
+ ref,
2393
3021
  className: ["mxc-column", className].filter(Boolean).join(" "),
2394
- "data-fullscreen": isFullscreen ? "" : void 0,
3022
+ "data-open": isOpen ? "" : void 0,
3023
+ "data-fullscreen": isOpen && isFullscreen ? "" : void 0,
2395
3024
  "data-placement": placement,
2396
3025
  "data-mxc-window": windowId,
2397
3026
  "aria-label": "Canvas",
3027
+ "aria-hidden": isOpen ? void 0 : true,
3028
+ inert: !isOpen,
2398
3029
  style: { ...style, ["--mxc-width"]: `${shown}px` },
2399
- children: [
2400
- placement === "docked" && !isFullscreen ? /* @__PURE__ */ jsx6(
3030
+ children: /* @__PURE__ */ jsxs7("div", { className: "mxc-column-inner", children: [
3031
+ placement === "docked" && isOpen && !isFullscreen ? /* @__PURE__ */ jsx8(
2401
3032
  WidthHandle,
2402
3033
  {
2403
3034
  width: fitted,
@@ -2408,14 +3039,17 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
2408
3039
  }
2409
3040
  }
2410
3041
  ) : null,
2411
- /* @__PURE__ */ jsx6("div", { className: "mxc-layout", children: /* @__PURE__ */ jsx6(LayoutNodeView, { node: layout }) })
2412
- ]
3042
+ /* @__PURE__ */ jsxs7(CanvasSlotsContext.Provider, { value: slots, children: [
3043
+ /* @__PURE__ */ jsx8("div", { className: "mxc-layout", children: /* @__PURE__ */ jsx8(LayoutNodeView, { node: layout }) }),
3044
+ /* @__PURE__ */ jsx8(CanvasBodyLayer, {})
3045
+ ] })
3046
+ ] })
2413
3047
  }
2414
3048
  );
2415
3049
  }
2416
3050
  function WidthHandle({ width, region, onDrag }) {
2417
3051
  const canvas = useCanvas();
2418
- const start = useRef5(null);
3052
+ const start = useRef7(null);
2419
3053
  const clamp = (next) => clampDraggedCanvasWidth(next, region, canvas.rules);
2420
3054
  const onPointerDown = (event) => {
2421
3055
  event.preventDefault();
@@ -2435,7 +3069,7 @@ function WidthHandle({ width, region, onDrag }) {
2435
3069
  onDrag(null);
2436
3070
  canvas.setWidth(next);
2437
3071
  };
2438
- return /* @__PURE__ */ jsx6(
3072
+ return /* @__PURE__ */ jsx8(
2439
3073
  "div",
2440
3074
  {
2441
3075
  className: "mxc-width-handle",
@@ -2457,12 +3091,12 @@ function WidthHandle({ width, region, onDrag }) {
2457
3091
  );
2458
3092
  }
2459
3093
  function LayoutNodeView({ node }) {
2460
- if (node.type === "pane") return /* @__PURE__ */ jsx6(CanvasPaneView, { paneId: node.paneId });
2461
- return /* @__PURE__ */ jsx6(SplitView, { node });
3094
+ if (node.type === "pane") return /* @__PURE__ */ jsx8(CanvasPaneView, { paneId: node.paneId });
3095
+ return /* @__PURE__ */ jsx8(SplitView, { node });
2462
3096
  }
2463
3097
  function useMeasuredWidth(ref, enabled) {
2464
- const [width, setWidth] = useState4(0);
2465
- useIsoLayoutEffect2(() => {
3098
+ const [width, setWidth] = useState6(0);
3099
+ useIsoLayoutEffect3(() => {
2466
3100
  const el = ref.current;
2467
3101
  if (!enabled || !el) return;
2468
3102
  setWidth(el.getBoundingClientRect().width);
@@ -2475,8 +3109,8 @@ function useMeasuredWidth(ref, enabled) {
2475
3109
  }
2476
3110
  function SplitView({ node }) {
2477
3111
  const canvas = useCanvas();
2478
- const ref = useRef5(null);
2479
- const [liveSizes, setLiveSizes] = useState4(null);
3112
+ const ref = useRef7(null);
3113
+ const [liveSizes, setLiveSizes] = useState6(null);
2480
3114
  const sizes = liveSizes ?? node.sizes;
2481
3115
  const measuredWidth = useMeasuredWidth(ref, node.orientation === "horizontal" && canvas.rules.paneMinWidth > 0);
2482
3116
  const stacked = node.orientation === "horizontal" && shouldStackSplit(measuredWidth, node.children.length, canvas.rules);
@@ -2516,11 +3150,11 @@ function SplitView({ node }) {
2516
3150
  target.addEventListener("pointerup", up);
2517
3151
  target.addEventListener("pointercancel", up);
2518
3152
  };
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(
3153
+ 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
3154
  SplitChild,
2521
3155
  {
2522
3156
  size: sizes[i] ?? 1 / node.children.length,
2523
- handle: i < node.children.length - 1 ? /* @__PURE__ */ jsx6(
3157
+ handle: i < node.children.length - 1 ? /* @__PURE__ */ jsx8(
2524
3158
  "div",
2525
3159
  {
2526
3160
  className: "mxc-split-handle",
@@ -2530,14 +3164,14 @@ function SplitView({ node }) {
2530
3164
  onDoubleClick: () => canvas.resizeSplit(node.id, node.children.map(() => 1 / node.children.length))
2531
3165
  }
2532
3166
  ) : null,
2533
- children: /* @__PURE__ */ jsx6(LayoutNodeView, { node: child })
3167
+ children: /* @__PURE__ */ jsx8(LayoutNodeView, { node: child })
2534
3168
  },
2535
3169
  child.type === "pane" ? child.paneId : child.id
2536
3170
  )) });
2537
3171
  }
2538
3172
  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 }),
3173
+ return /* @__PURE__ */ jsxs7(Fragment3, { children: [
3174
+ /* @__PURE__ */ jsx8("div", { className: "mxc-split-child", style: { flexGrow: size, flexBasis: 0 }, children }),
2541
3175
  handle
2542
3176
  ] });
2543
3177
  }
@@ -2551,26 +3185,61 @@ function CanvasToggle({
2551
3185
  const count = useCanvasState((s) => selectCanvasWindowItemCount(s, windowId));
2552
3186
  const hint = canvasShortcutHint("canvas.toggle", isMac2());
2553
3187
  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(
3188
+ return /* @__PURE__ */ jsxs7("span", { className: ["mxc-toggle", className].filter(Boolean).join(" "), "data-open": isOpen ? "" : void 0, children: [
3189
+ /* @__PURE__ */ jsx8(
2556
3190
  Button,
2557
3191
  {
2558
3192
  ariaLabel: isOpen ? "Hide canvas" : "Show canvas",
2559
3193
  tooltip: `${isOpen ? "Hide canvas" : "Show canvas"}${hint ? ` (${hint})` : ""}`,
2560
- icon: /* @__PURE__ */ jsx6(PanelRightIcon, {}),
3194
+ icon: /* @__PURE__ */ jsx8(PanelRightIcon, {}),
2561
3195
  onClick: () => controller.toggle()
2562
3196
  }
2563
3197
  ),
2564
- count > 0 && !isOpen ? /* @__PURE__ */ jsx6("span", { className: "mxc-toggle-badge", children: count }) : null
3198
+ count > 0 && !isOpen ? /* @__PURE__ */ jsx8("span", { className: "mxc-toggle-badge", children: count }) : null
2565
3199
  ] });
2566
3200
  }
3201
+ function CanvasKindIcons({
3202
+ kinds,
3203
+ variant = "transparent",
3204
+ className,
3205
+ label = "Canvas tools"
3206
+ }) {
3207
+ return /* @__PURE__ */ jsx8("span", { role: "group", "aria-label": label, className: ["mxc-kind-icons", className].filter(Boolean).join(" "), children: kinds.map((entry) => {
3208
+ const icon = typeof entry === "string" ? { kind: entry } : entry;
3209
+ return /* @__PURE__ */ jsx8(KindIconButton, { icon, variant }, `${icon.kind}::${icon.key ?? CANVAS_DEFAULT_KEY}`);
3210
+ }) });
3211
+ }
3212
+ function KindIconButton({ icon, variant }) {
3213
+ const kind = useCanvasKind(icon.kind);
3214
+ const toggle = useCanvasKindToggle({
3215
+ kind: icon.kind,
3216
+ key: icon.key,
3217
+ data: icon.data,
3218
+ title: icon.title,
3219
+ whenVisible: icon.whenVisible
3220
+ });
3221
+ if (!kind) return null;
3222
+ const Icon = kind.icon;
3223
+ const Button = variant === "glass" ? TapTargetButton : TapTargetButtonTransparent;
3224
+ const name = icon.label ?? kind.label;
3225
+ return /* @__PURE__ */ jsx8(
3226
+ Button,
3227
+ {
3228
+ ariaLabel: name,
3229
+ tooltip: name,
3230
+ icon: /* @__PURE__ */ jsx8(Icon, {}),
3231
+ pressed: toggle.isVisible,
3232
+ onClick: () => void toggle.toggle()
3233
+ }
3234
+ );
3235
+ }
2567
3236
  function CanvasFrame({ children, className }) {
2568
3237
  const { windowId } = useCanvasInternals();
2569
3238
  const isFullscreen = useCanvasState((s) => selectCanvasIsFullscreen(s, windowId));
2570
3239
  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, {})
3240
+ return /* @__PURE__ */ jsxs7("div", { className: ["mxc-frame", className].filter(Boolean).join(" "), "data-canvas-open": isOpen ? "" : void 0, children: [
3241
+ /* @__PURE__ */ jsx8("div", { className: "mxc-frame-main", hidden: isOpen && isFullscreen, children }),
3242
+ /* @__PURE__ */ jsx8(CanvasColumn, {})
2574
3243
  ] });
2575
3244
  }
2576
3245
  export {
@@ -2579,6 +3248,7 @@ export {
2579
3248
  CanvasColumn,
2580
3249
  CanvasFrame,
2581
3250
  CanvasHeaderButton,
3251
+ CanvasKindIcons,
2582
3252
  CanvasMenuTrigger,
2583
3253
  CanvasPaneView,
2584
3254
  CanvasPanel,
@@ -2601,7 +3271,10 @@ export {
2601
3271
  useCanvasKind,
2602
3272
  useCanvasKindToggle,
2603
3273
  useCanvasKinds,
3274
+ useCanvasLauncherEntries,
3275
+ useCanvasLauncherEntry,
2604
3276
  useCanvasNativeBodies,
3277
+ useCanvasPresentation,
2605
3278
  useCanvasState,
2606
3279
  useCanvasWindowId,
2607
3280
  useOptionalCanvas,