@ai-matrx/canvas 0.3.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,69 +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;
2735
+ const Toolbar = kind?.Toolbar;
2011
2736
  const single = tabStrip === "auto" && pane.itemIds.length === 1 && active;
2012
- const hideDrop = () => {
2013
- drop.current?.();
2014
- drop.current = null;
2015
- };
2016
- const onDragOver = (event) => {
2017
- if (!event.dataTransfer.types.includes(DRAG_MIME)) return;
2018
- event.preventDefault();
2019
- event.dataTransfer.dropEffect = "move";
2020
- if (!drop.current && sectionRef.current) {
2021
- drop.current = layer.show({ type: "drop-indicator", rect: toCanvasRect(sectionRef.current.getBoundingClientRect()), paneId });
2022
- }
2023
- };
2024
- const onDragLeave = (event) => {
2025
- const next = event.relatedTarget;
2026
- if (next && sectionRef.current?.contains(next)) return;
2027
- hideDrop();
2028
- };
2029
- const onDrop = (event) => {
2030
- hideDrop();
2031
- const itemId = event.dataTransfer.getData(DRAG_MIME);
2032
- if (!itemId) return;
2033
- event.preventDefault();
2034
- canvas.moveItem(itemId, paneId);
2035
- };
2036
2737
  const closeLabel = role === "pop-out" && paneCount === 1 ? "Close window" : paneCount > 1 ? "Close pane" : "Close canvas";
2037
- return /* @__PURE__ */ jsx5(
2738
+ return /* @__PURE__ */ jsxs6(
2038
2739
  CanvasPanel,
2039
2740
  {
2040
2741
  ref: sectionRef,
@@ -2043,10 +2744,14 @@ function CanvasPaneView({ paneId }) {
2043
2744
  title: active ? itemTitle(active, kind) : "Canvas",
2044
2745
  focused,
2045
2746
  "data-mxc-pane": paneId,
2046
- 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" }),
2047
- leading: Leading && kindProps ? /* @__PURE__ */ jsx5(Leading, { ...kindProps }) : null,
2048
- actions: Action && kindProps ? /* @__PURE__ */ jsx5(Action, { ...kindProps }) : null,
2049
- 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 }),
2050
2755
  expanded: isFullscreen,
2051
2756
  onToggleExpand: () => canvas.setFullscreen(!isFullscreen),
2052
2757
  expandShortcut: canvasShortcutHint("canvas.toggleFullscreen", isMac()),
@@ -2056,54 +2761,92 @@ function CanvasPaneView({ paneId }) {
2056
2761
  onPointerDownCapture: () => {
2057
2762
  if (!focused) canvas.focusPane(paneId);
2058
2763
  },
2059
- onDragOver,
2060
- onDragLeave,
2061
- onDrop,
2062
- children: pane.itemIds.length === 0 ? /* @__PURE__ */ jsx5(Launcher, { paneId }) : pane.itemIds.map((itemId) => {
2063
- const item = items[itemId];
2064
- if (!item) return null;
2065
- const isActive = itemId === pane.activeItemId;
2066
- return /* @__PURE__ */ jsx5(ItemBody, { item, paneId, windowId, isActive, isFocused: focused && isActive, hasNativePort: !!ports.onNativeBodyFrame }, itemId);
2067
- })
2764
+ ...drop,
2765
+ children: [
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 }) })
2768
+ ]
2068
2769
  }
2069
2770
  );
2070
2771
  }
2772
+ var ToolbarBoundary = class extends Component2 {
2773
+ state = { failed: false };
2774
+ static getDerivedStateFromError() {
2775
+ return { failed: true };
2776
+ }
2777
+ componentDidCatch(error) {
2778
+ console.error(`[@ai-matrx/canvas] "${this.props.kindId}" Toolbar threw`, error);
2779
+ }
2780
+ render() {
2781
+ return this.state.failed ? null : this.props.children;
2782
+ }
2783
+ };
2071
2784
  function SingleTitle({ item, kind }) {
2785
+ const canvas = useCanvas();
2072
2786
  const title = itemTitle(item, kind);
2073
2787
  const drag = useTabDrag(item.id, title);
2074
2788
  const Icon = kind?.icon ?? AlertIcon;
2075
- return /* @__PURE__ */ jsxs4("span", { className: "mxc-panel-title", "data-draggable": "", title, ...drag, children: [
2076
- /* @__PURE__ */ jsx5(Icon, { className: "mxc-title-icon" }),
2077
- /* @__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, {}) })
2078
2793
  ] });
2079
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
+ }
2080
2828
  function ItemTab({ itemId, paneId, index, active }) {
2081
2829
  const canvas = useCanvas();
2082
2830
  const item = useCanvasState((s) => s.items[itemId]);
2083
- const kind = useCanvasKind(item?.kind ?? "");
2084
- const ref = useRef4(null);
2831
+ const kind = useItemKind(item?.kind ?? "");
2085
2832
  const title = item ? itemTitle(item, kind) : "";
2086
2833
  const drag = useTabDrag(itemId, title);
2087
- useEffect4(() => {
2088
- if (active) ref.current?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
2089
- }, [active]);
2090
2834
  if (!item) return null;
2091
2835
  const Icon = kind?.icon ?? AlertIcon;
2092
- return /* @__PURE__ */ jsx5(
2836
+ return /* @__PURE__ */ jsx7(
2093
2837
  CanvasTab,
2094
2838
  {
2095
- ref,
2096
2839
  title,
2097
- icon: /* @__PURE__ */ jsx5(Icon, {}),
2840
+ icon: /* @__PURE__ */ jsx7(Icon, {}),
2098
2841
  active,
2099
2842
  onSelect: () => canvas.activate(itemId),
2100
2843
  onClose: () => canvas.close(itemId),
2101
2844
  ...drag,
2102
2845
  onDragOver: (event) => {
2103
- if (event.dataTransfer.types.includes(DRAG_MIME)) event.preventDefault();
2846
+ if (event.dataTransfer.types.includes(CANVAS_DRAG_MIME)) event.preventDefault();
2104
2847
  },
2105
2848
  onDrop: (event) => {
2106
- const dragged = event.dataTransfer.getData(DRAG_MIME);
2849
+ const dragged = event.dataTransfer.getData(CANVAS_DRAG_MIME);
2107
2850
  if (!dragged) return;
2108
2851
  event.preventDefault();
2109
2852
  if (dragged !== itemId) canvas.moveItem(dragged, paneId, index);
@@ -2126,16 +2869,16 @@ function PaneMenu({
2126
2869
  const canvas = useCanvas();
2127
2870
  const { ports, layer } = useCanvasInternals();
2128
2871
  const moveId = tabCount > 1 ? activeItem?.id : void 0;
2129
- const blocking = useRef4(null);
2130
- useEffect4(() => () => blocking.current?.(), []);
2872
+ const blocking = useRef6(null);
2873
+ useEffect6(() => () => blocking.current?.(), []);
2131
2874
  const kindItems = kind?.menuItems && kindProps ? safeMenu(() => kind.menuItems?.(kindProps)) : [];
2132
2875
  const hostItems = activeItem && ports.itemActions ? safeMenu(() => ports.itemActions?.(activeItem, kind)) : [];
2133
2876
  const sections = [
2134
2877
  [...kindItems, ...hostItems].map((entry) => ({ ...entry, id: `item:${entry.id}` })),
2135
2878
  [
2136
- { id: "split-right", label: "Split right", icon: /* @__PURE__ */ jsx5(SplitRightIcon, {}), onSelect: () => canvas.splitPane(paneId, "horizontal", moveId) },
2137
- { id: "split-down", label: "Split down", icon: /* @__PURE__ */ jsx5(SplitDownIcon, {}), onSelect: () => canvas.splitPane(paneId, "vertical", moveId) },
2138
- ...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) }] : []
2139
2882
  ],
2140
2883
  activeItem ? [
2141
2884
  { id: "close-tab", label: "Close tab", onSelect: () => canvas.close(activeItem.id) },
@@ -2153,7 +2896,7 @@ function PaneMenu({
2153
2896
  handlers.set(action.id, action.onSelect);
2154
2897
  }
2155
2898
  });
2156
- return /* @__PURE__ */ jsx5(
2899
+ return /* @__PURE__ */ jsx7(
2157
2900
  CanvasMenuTrigger,
2158
2901
  {
2159
2902
  onClick: (event) => {
@@ -2168,7 +2911,7 @@ function PaneMenu({
2168
2911
  }
2169
2912
  );
2170
2913
  }
2171
- return /* @__PURE__ */ jsxs4(
2914
+ return /* @__PURE__ */ jsxs6(
2172
2915
  Menu.Root,
2173
2916
  {
2174
2917
  onOpenChange: (open) => {
@@ -2176,16 +2919,16 @@ function PaneMenu({
2176
2919
  blocking.current = open ? layer.beginBlocking() : null;
2177
2920
  },
2178
2921
  children: [
2179
- /* @__PURE__ */ jsx5(Menu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx5(CanvasMenuTrigger, {}) }),
2180
- /* @__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)) }) })
2181
2924
  ]
2182
2925
  }
2183
2926
  );
2184
2927
  }
2185
2928
  function MenuSection({ first, actions }) {
2186
- return /* @__PURE__ */ jsxs4(Fragment, { children: [
2187
- first ? null : /* @__PURE__ */ jsx5(Menu.Separator, { className: "mxc-menu-separator" }),
2188
- 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: [
2189
2932
  action.icon,
2190
2933
  action.label
2191
2934
  ] }, action.id))
@@ -2199,150 +2942,16 @@ function safeMenu(build) {
2199
2942
  return [];
2200
2943
  }
2201
2944
  }
2202
- function ItemBody({
2203
- item,
2204
- paneId,
2205
- windowId,
2206
- isActive,
2207
- isFocused,
2208
- hasNativePort
2209
- }) {
2210
- const canvas = useCanvas();
2211
- const kind = useCanvasKind(item.kind);
2212
- if (!isActive && !kind?.keepAlive) return null;
2213
- const props = { item, data: item.data, paneId, isFocused, canvas, windowId };
2214
- let unavailable = null;
2215
- try {
2216
- unavailable = kind?.unavailable?.(item.data, item) ?? null;
2217
- } catch (error) {
2218
- console.error(`[@ai-matrx/canvas] "${item.kind}" unavailable() threw`, error);
2219
- }
2220
- let body;
2221
- if (!kind) {
2222
- body = /* @__PURE__ */ jsx5(ItemProblem, { message: "This item can no longer be shown.", onClose: () => canvas.close(item.id) });
2223
- } else if (unavailable) {
2224
- body = /* @__PURE__ */ jsx5(CanvasPanelEmpty, { children: unavailable });
2225
- } else if (kind.body === "native") {
2226
- 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" });
2227
- } else {
2228
- body = /* @__PURE__ */ jsx5(Rendered, { kind, item, props });
2229
- }
2230
- return /* @__PURE__ */ jsx5("div", { className: "mxc-item", hidden: !isActive, "data-kind": item.kind, children: body });
2231
- }
2232
- function Rendered({ kind, item, props }) {
2233
- const canvas = useCanvas();
2234
- 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 }) }) });
2235
- }
2236
- function KindRender({ kind, props }) {
2237
- const Render = kindComponent(kind);
2238
- if (!Render) throw new Error(`[@ai-matrx/canvas] kind "${kind.id}" has nothing to render.`);
2239
- return /* @__PURE__ */ jsx5(Render, { ...props });
2240
- }
2241
- function NativeBody({ itemId, windowId, children }) {
2242
- const { layer } = useCanvasInternals();
2243
- const ref = useRef4(null);
2244
- const measure = useRef4(() => void 0);
2245
- measure.current = () => {
2246
- const el = ref.current;
2247
- const box = el?.isConnected ? el.getBoundingClientRect() : null;
2248
- if (!box || box.width <= 0 || box.height <= 0) {
2249
- layer.native.report(itemId, null, { visible: false, windowId });
2250
- return;
2251
- }
2252
- const rect = { ...toCanvasRect(box), devicePixelRatio: window.devicePixelRatio || 1 };
2253
- layer.native.report(itemId, rect, { visible: !layer.isBlocked(), windowId });
2254
- };
2255
- useIsoLayoutEffect(() => measure.current());
2256
- useIsoLayoutEffect(() => {
2257
- const el = ref.current;
2258
- const run = () => measure.current();
2259
- const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(run);
2260
- if (el) observer?.observe(el);
2261
- const unregister = layer.native.register(run);
2262
- window.addEventListener("resize", run);
2263
- return () => {
2264
- observer?.disconnect();
2265
- unregister();
2266
- window.removeEventListener("resize", run);
2267
- layer.native.report(itemId, null, { visible: false, windowId });
2268
- };
2269
- }, [itemId, windowId, layer]);
2270
- return /* @__PURE__ */ jsx5("div", { ref, className: "mxc-native-body", "data-native-body": itemId, children });
2271
- }
2272
- function ItemProblem({ message, onClose, onRetry }) {
2273
- return /* @__PURE__ */ jsxs4("div", { className: "mxc-problem", role: "alert", children: [
2274
- /* @__PURE__ */ jsx5(AlertIcon, {}),
2275
- /* @__PURE__ */ jsx5("p", { children: message }),
2276
- /* @__PURE__ */ jsxs4("div", { className: "mxc-problem-actions", children: [
2277
- onRetry ? /* @__PURE__ */ jsx5("button", { type: "button", className: "mxc-text-button", onClick: onRetry, children: "Try again" }) : null,
2278
- /* @__PURE__ */ jsx5("button", { type: "button", className: "mxc-text-button", onClick: onClose, children: "Close tab" })
2279
- ] })
2280
- ] });
2281
- }
2282
- var ItemBoundary = class extends Component {
2283
- state = { error: null };
2284
- static getDerivedStateFromError(error) {
2285
- return { error };
2286
- }
2287
- componentDidCatch(error, info) {
2288
- console.error(`[@ai-matrx/canvas] "${this.props.item.kind}" failed to render`, error, info.componentStack);
2289
- }
2290
- render() {
2291
- if (this.state.error) {
2292
- return /* @__PURE__ */ jsx5(
2293
- ItemProblem,
2294
- {
2295
- message: "This item failed to load.",
2296
- onClose: this.props.onClose,
2297
- onRetry: () => {
2298
- this.props.onRetry();
2299
- this.setState({ error: null });
2300
- }
2301
- }
2302
- );
2303
- }
2304
- return this.props.children;
2305
- }
2306
- };
2307
- function Launcher({ paneId }) {
2308
- const canvas = useCanvas();
2309
- const kinds = useCanvasKinds().filter((kind) => kind.launcher);
2310
- if (kinds.length === 0) return /* @__PURE__ */ jsx5(CanvasPanelEmpty, { children: "Nothing open" });
2311
- return /* @__PURE__ */ jsx5("div", { className: "mxc-launcher", children: kinds.map((kind) => {
2312
- const Icon = kind.icon;
2313
- const launcher = kind.launcher;
2314
- if (!launcher) return null;
2315
- return /* @__PURE__ */ jsxs4(
2316
- "button",
2317
- {
2318
- type: "button",
2319
- className: "mxc-launcher-item",
2320
- onClick: () => canvas.open({
2321
- kind: kind.id,
2322
- key: launcher.key,
2323
- data: launcher.data,
2324
- title: launcher.title ?? null,
2325
- target: { paneId }
2326
- }),
2327
- children: [
2328
- /* @__PURE__ */ jsx5(Icon, { className: "mxc-launcher-icon" }),
2329
- /* @__PURE__ */ jsx5("span", { children: launcher.title ?? kind.label })
2330
- ]
2331
- },
2332
- kind.id
2333
- );
2334
- }) });
2335
- }
2336
2945
 
2337
2946
  // src/react/CanvasColumn.tsx
2338
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2339
- 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;
2340
2949
  function subscribeViewport(listener) {
2341
2950
  window.addEventListener("resize", listener);
2342
2951
  return () => window.removeEventListener("resize", listener);
2343
2952
  }
2344
2953
  function useViewportWidth() {
2345
- return useSyncExternalStore3(subscribeViewport, () => window.innerWidth, () => 0);
2954
+ return useSyncExternalStore4(subscribeViewport, () => window.innerWidth, () => 0);
2346
2955
  }
2347
2956
  function useRegionWidth() {
2348
2957
  const viewport = useViewportWidth();
@@ -2360,28 +2969,66 @@ function useCanvasColumnWidth() {
2360
2969
  return fitCanvasWidth(width, region, controller.rules);
2361
2970
  }
2362
2971
  function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
2363
- const { controller, windowId } = useCanvasInternals();
2972
+ const { controller, windowId, layer } = useCanvasInternals();
2364
2973
  const isOpen = useCanvasState((s) => selectCanvasIsOpen(s, windowId));
2365
2974
  const isFullscreen = useCanvasState((s) => selectCanvasIsFullscreen(s, windowId));
2366
2975
  const width = useCanvasState((s) => selectCanvasWidth(s, windowId));
2367
2976
  const layout = useCanvasState((s) => selectCanvasLayout(s, windowId));
2368
2977
  const region = useRegionWidth();
2369
- const [dragWidth, setDragWidth] = useState4(null);
2978
+ const [dragWidth, setDragWidth] = useState6(null);
2979
+ const [slots] = useState6(createCanvasSlots);
2980
+ const ref = useRef7(null);
2370
2981
  useRegisterCanvasPresence();
2371
- 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;
2372
3015
  const fitted = fitCanvasWidth(width, region, controller.rules);
2373
3016
  const shown = dragWidth ?? fitted;
2374
- return /* @__PURE__ */ jsxs5(
3017
+ return /* @__PURE__ */ jsx8(
2375
3018
  "aside",
2376
3019
  {
3020
+ ref,
2377
3021
  className: ["mxc-column", className].filter(Boolean).join(" "),
2378
- "data-fullscreen": isFullscreen ? "" : void 0,
3022
+ "data-open": isOpen ? "" : void 0,
3023
+ "data-fullscreen": isOpen && isFullscreen ? "" : void 0,
2379
3024
  "data-placement": placement,
2380
3025
  "data-mxc-window": windowId,
2381
3026
  "aria-label": "Canvas",
3027
+ "aria-hidden": isOpen ? void 0 : true,
3028
+ inert: !isOpen,
2382
3029
  style: { ...style, ["--mxc-width"]: `${shown}px` },
2383
- children: [
2384
- placement === "docked" && !isFullscreen ? /* @__PURE__ */ jsx6(
3030
+ children: /* @__PURE__ */ jsxs7("div", { className: "mxc-column-inner", children: [
3031
+ placement === "docked" && isOpen && !isFullscreen ? /* @__PURE__ */ jsx8(
2385
3032
  WidthHandle,
2386
3033
  {
2387
3034
  width: fitted,
@@ -2392,14 +3039,17 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
2392
3039
  }
2393
3040
  }
2394
3041
  ) : null,
2395
- /* @__PURE__ */ jsx6("div", { className: "mxc-layout", children: /* @__PURE__ */ jsx6(LayoutNodeView, { node: layout }) })
2396
- ]
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
+ ] })
2397
3047
  }
2398
3048
  );
2399
3049
  }
2400
3050
  function WidthHandle({ width, region, onDrag }) {
2401
3051
  const canvas = useCanvas();
2402
- const start = useRef5(null);
3052
+ const start = useRef7(null);
2403
3053
  const clamp = (next) => clampDraggedCanvasWidth(next, region, canvas.rules);
2404
3054
  const onPointerDown = (event) => {
2405
3055
  event.preventDefault();
@@ -2419,7 +3069,7 @@ function WidthHandle({ width, region, onDrag }) {
2419
3069
  onDrag(null);
2420
3070
  canvas.setWidth(next);
2421
3071
  };
2422
- return /* @__PURE__ */ jsx6(
3072
+ return /* @__PURE__ */ jsx8(
2423
3073
  "div",
2424
3074
  {
2425
3075
  className: "mxc-width-handle",
@@ -2441,12 +3091,12 @@ function WidthHandle({ width, region, onDrag }) {
2441
3091
  );
2442
3092
  }
2443
3093
  function LayoutNodeView({ node }) {
2444
- if (node.type === "pane") return /* @__PURE__ */ jsx6(CanvasPaneView, { paneId: node.paneId });
2445
- return /* @__PURE__ */ jsx6(SplitView, { node });
3094
+ if (node.type === "pane") return /* @__PURE__ */ jsx8(CanvasPaneView, { paneId: node.paneId });
3095
+ return /* @__PURE__ */ jsx8(SplitView, { node });
2446
3096
  }
2447
3097
  function useMeasuredWidth(ref, enabled) {
2448
- const [width, setWidth] = useState4(0);
2449
- useIsoLayoutEffect2(() => {
3098
+ const [width, setWidth] = useState6(0);
3099
+ useIsoLayoutEffect3(() => {
2450
3100
  const el = ref.current;
2451
3101
  if (!enabled || !el) return;
2452
3102
  setWidth(el.getBoundingClientRect().width);
@@ -2459,8 +3109,8 @@ function useMeasuredWidth(ref, enabled) {
2459
3109
  }
2460
3110
  function SplitView({ node }) {
2461
3111
  const canvas = useCanvas();
2462
- const ref = useRef5(null);
2463
- const [liveSizes, setLiveSizes] = useState4(null);
3112
+ const ref = useRef7(null);
3113
+ const [liveSizes, setLiveSizes] = useState6(null);
2464
3114
  const sizes = liveSizes ?? node.sizes;
2465
3115
  const measuredWidth = useMeasuredWidth(ref, node.orientation === "horizontal" && canvas.rules.paneMinWidth > 0);
2466
3116
  const stacked = node.orientation === "horizontal" && shouldStackSplit(measuredWidth, node.children.length, canvas.rules);
@@ -2500,11 +3150,11 @@ function SplitView({ node }) {
2500
3150
  target.addEventListener("pointerup", up);
2501
3151
  target.addEventListener("pointercancel", up);
2502
3152
  };
2503
- 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(
2504
3154
  SplitChild,
2505
3155
  {
2506
3156
  size: sizes[i] ?? 1 / node.children.length,
2507
- handle: i < node.children.length - 1 ? /* @__PURE__ */ jsx6(
3157
+ handle: i < node.children.length - 1 ? /* @__PURE__ */ jsx8(
2508
3158
  "div",
2509
3159
  {
2510
3160
  className: "mxc-split-handle",
@@ -2514,14 +3164,14 @@ function SplitView({ node }) {
2514
3164
  onDoubleClick: () => canvas.resizeSplit(node.id, node.children.map(() => 1 / node.children.length))
2515
3165
  }
2516
3166
  ) : null,
2517
- children: /* @__PURE__ */ jsx6(LayoutNodeView, { node: child })
3167
+ children: /* @__PURE__ */ jsx8(LayoutNodeView, { node: child })
2518
3168
  },
2519
3169
  child.type === "pane" ? child.paneId : child.id
2520
3170
  )) });
2521
3171
  }
2522
3172
  function SplitChild({ size, handle, children }) {
2523
- return /* @__PURE__ */ jsxs5(Fragment2, { children: [
2524
- /* @__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 }),
2525
3175
  handle
2526
3176
  ] });
2527
3177
  }
@@ -2535,26 +3185,61 @@ function CanvasToggle({
2535
3185
  const count = useCanvasState((s) => selectCanvasWindowItemCount(s, windowId));
2536
3186
  const hint = canvasShortcutHint("canvas.toggle", isMac2());
2537
3187
  const Button = variant === "glass" ? TapTargetButton : TapTargetButtonTransparent;
2538
- return /* @__PURE__ */ jsxs5("span", { className: ["mxc-toggle", className].filter(Boolean).join(" "), "data-open": isOpen ? "" : void 0, children: [
2539
- /* @__PURE__ */ jsx6(
3188
+ return /* @__PURE__ */ jsxs7("span", { className: ["mxc-toggle", className].filter(Boolean).join(" "), "data-open": isOpen ? "" : void 0, children: [
3189
+ /* @__PURE__ */ jsx8(
2540
3190
  Button,
2541
3191
  {
2542
3192
  ariaLabel: isOpen ? "Hide canvas" : "Show canvas",
2543
3193
  tooltip: `${isOpen ? "Hide canvas" : "Show canvas"}${hint ? ` (${hint})` : ""}`,
2544
- icon: /* @__PURE__ */ jsx6(PanelRightIcon, {}),
3194
+ icon: /* @__PURE__ */ jsx8(PanelRightIcon, {}),
2545
3195
  onClick: () => controller.toggle()
2546
3196
  }
2547
3197
  ),
2548
- 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
2549
3199
  ] });
2550
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
+ }
2551
3236
  function CanvasFrame({ children, className }) {
2552
3237
  const { windowId } = useCanvasInternals();
2553
3238
  const isFullscreen = useCanvasState((s) => selectCanvasIsFullscreen(s, windowId));
2554
3239
  const isOpen = useCanvasState((s) => selectCanvasIsOpen(s, windowId));
2555
- return /* @__PURE__ */ jsxs5("div", { className: ["mxc-frame", className].filter(Boolean).join(" "), "data-canvas-open": isOpen ? "" : void 0, children: [
2556
- /* @__PURE__ */ jsx6("div", { className: "mxc-frame-main", hidden: isOpen && isFullscreen, children }),
2557
- /* @__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, {})
2558
3243
  ] });
2559
3244
  }
2560
3245
  export {
@@ -2563,6 +3248,7 @@ export {
2563
3248
  CanvasColumn,
2564
3249
  CanvasFrame,
2565
3250
  CanvasHeaderButton,
3251
+ CanvasKindIcons,
2566
3252
  CanvasMenuTrigger,
2567
3253
  CanvasPaneView,
2568
3254
  CanvasPanel,
@@ -2585,7 +3271,10 @@ export {
2585
3271
  useCanvasKind,
2586
3272
  useCanvasKindToggle,
2587
3273
  useCanvasKinds,
3274
+ useCanvasLauncherEntries,
3275
+ useCanvasLauncherEntry,
2588
3276
  useCanvasNativeBodies,
3277
+ useCanvasPresentation,
2589
3278
  useCanvasState,
2590
3279
  useCanvasWindowId,
2591
3280
  useOptionalCanvas,