@ai-matrx/canvas 0.6.1 → 0.7.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
@@ -5,13 +5,52 @@
5
5
  import {
6
6
  createContext,
7
7
  useContext,
8
- useEffect as useEffect2,
8
+ useEffect as useEffect3,
9
9
  useMemo,
10
- useRef as useRef2,
10
+ useRef as useRef3,
11
11
  useState as useState2,
12
12
  useSyncExternalStore as useSyncExternalStore2
13
13
  } from "react";
14
14
 
15
+ // src/react/menuItems.ts
16
+ import { useEffect, useLayoutEffect, useRef } from "react";
17
+ var useIsoLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
18
+ function createCanvasItemMenus() {
19
+ const sources = /* @__PURE__ */ new Map();
20
+ return {
21
+ register(itemId, read) {
22
+ let set = sources.get(itemId);
23
+ if (!set) sources.set(itemId, set = /* @__PURE__ */ new Set());
24
+ set.add(read);
25
+ return () => {
26
+ const current = sources.get(itemId);
27
+ current?.delete(read);
28
+ if (current && current.size === 0) sources.delete(itemId);
29
+ };
30
+ },
31
+ read(itemId) {
32
+ const out = [];
33
+ for (const read of sources.get(itemId) ?? []) {
34
+ try {
35
+ out.push(...read());
36
+ } catch (error) {
37
+ console.error("[@ai-matrx/canvas] a body's menu entries threw", error);
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+ };
43
+ }
44
+ function useCanvasMenuItems(itemId, items) {
45
+ const { itemMenus } = useCanvasInternals();
46
+ const latest = useRef(items);
47
+ latest.current = items;
48
+ useIsoLayoutEffect(() => {
49
+ if (!itemId) return void 0;
50
+ return itemMenus.register(itemId, () => latest.current);
51
+ }, [itemMenus, itemId]);
52
+ }
53
+
15
54
  // src/core/types.ts
16
55
  var CANVAS_MAIN_WINDOW = "main";
17
56
  var CANVAS_LAUNCHER_KIND = "canvas.launcher";
@@ -179,6 +218,8 @@ var canvasActions = {
179
218
  navigate: (itemId, to) => ({ type: `${P}navigate`, payload: { itemId, to, now: Date.now() } }),
180
219
  /** Returns a tab to what it showed before its last navigate. */
181
220
  back: (itemId) => ({ type: `${P}back`, payload: { itemId, now: Date.now() } }),
221
+ /** Re-does the last Back (the step Back left), with that step's live body. */
222
+ forward: (itemId) => ({ type: `${P}forward`, payload: { itemId, now: Date.now() } }),
182
223
  closeItem: (itemId) => ({ type: `${P}closeItem`, payload: { itemId } }),
183
224
  closeOthers: (itemId) => ({ type: `${P}closeOthers`, payload: { itemId } }),
184
225
  activate: (itemId) => ({ type: `${P}activate`, payload: { itemId } }),
@@ -379,6 +420,7 @@ function newItem(kind, key, title, data, now, steps) {
379
420
  const id = canvasItemId(kind, key);
380
421
  const history = steps?.history ?? [];
381
422
  const kept = steps?.kept ?? [];
423
+ const forward = steps?.forward ?? [];
382
424
  return {
383
425
  id,
384
426
  kind,
@@ -390,7 +432,8 @@ function newItem(kind, key, title, data, now, steps) {
390
432
  // A reused instance is the SAME live body coming back (Back, or returning to a kept step).
391
433
  instance: steps?.instance ?? `${now.toString(36)}:${id}`,
392
434
  ...history.length > 0 ? { history: history.slice(-CANVAS_HISTORY_LIMIT) } : {},
393
- ...kept.length > 0 ? { kept: kept.slice(-CANVAS_KEEP_ALIVE_DEPTH) } : {}
435
+ ...kept.length > 0 ? { kept: kept.slice(-CANVAS_KEEP_ALIVE_DEPTH) } : {},
436
+ ...forward.length > 0 ? { forward: forward.slice(-CANVAS_HISTORY_LIMIT) } : {}
394
437
  };
395
438
  }
396
439
  function historyEntry(item) {
@@ -436,12 +479,14 @@ function sanitizeHistory(raw) {
436
479
  function sanitizeItem(item) {
437
480
  const history = sanitizeHistory(item.history);
438
481
  const kept = sanitizeHistory(item.kept).slice(-CANVAS_KEEP_ALIVE_DEPTH);
439
- const { history: _history, kept: _kept, instance, ...rest } = item;
482
+ const forward = sanitizeHistory(item.forward);
483
+ const { history: _history, kept: _kept, forward: _forward, instance, ...rest } = item;
440
484
  return {
441
485
  ...rest,
442
486
  ...typeof instance === "string" && instance ? { instance } : {},
443
487
  ...history.length > 0 ? { history } : {},
444
- ...kept.length > 0 ? { kept } : {}
488
+ ...kept.length > 0 ? { kept } : {},
489
+ ...forward.length > 0 ? { forward } : {}
445
490
  };
446
491
  }
447
492
  function clampWidth(width) {
@@ -632,6 +677,9 @@ function canvasReducer(state = createInitialCanvasState(), action) {
632
677
  const keptIndex = keptAll.findIndex((entry) => canvasItemId(entry.kind, entry.key) === targetId);
633
678
  const revived = keptIndex >= 0 ? keptAll[keptIndex] : void 0;
634
679
  const kept = keptAll.filter((_, i) => i !== keptIndex);
680
+ const forwardAll = current.forward ?? [];
681
+ const nextForward = forwardAll[forwardAll.length - 1];
682
+ const forward = nextForward && canvasItemId(nextForward.kind, nextForward.key) === targetId ? forwardAll.slice(0, -1) : [];
635
683
  return replaceInPlace(
636
684
  state,
637
685
  current,
@@ -641,7 +689,7 @@ function canvasReducer(state = createInitialCanvasState(), action) {
641
689
  to.title !== void 0 ? to.title : revived?.title ?? null,
642
690
  to.data !== void 0 ? to.data : revived?.data ?? null,
643
691
  now,
644
- { history, kept, instance: revived?.instance }
692
+ { history, kept, forward, instance: revived?.instance }
645
693
  )
646
694
  );
647
695
  }
@@ -657,10 +705,36 @@ function canvasReducer(state = createInitialCanvasState(), action) {
657
705
  return focusExisting(canvasReducer(state, canvasActions.closeItem(itemId)), targetId);
658
706
  }
659
707
  const kept = [...(current.kept ?? []).filter((k) => canvasItemId(k.kind, k.key) !== itemId), historyEntry(current)];
708
+ const forward = [...current.forward ?? [], historyEntry(current)];
660
709
  return replaceInPlace(
661
710
  state,
662
711
  current,
663
- newItem(entry.kind, entry.key, entry.title, entry.data, now, { history: history.slice(0, -1), kept, instance: entry.instance })
712
+ newItem(entry.kind, entry.key, entry.title, entry.data, now, { history: history.slice(0, -1), kept, forward, instance: entry.instance })
713
+ );
714
+ }
715
+ case `${P}forward`: {
716
+ const { itemId, now } = action.payload;
717
+ const current = state.items[itemId];
718
+ const forwardAll = current?.forward ?? [];
719
+ const entry = forwardAll[forwardAll.length - 1];
720
+ if (!current || !entry || !paneOf(state, itemId)) return state;
721
+ const targetId = canvasItemId(entry.kind, entry.key);
722
+ if (targetId === itemId) return state;
723
+ if (state.items[targetId]) {
724
+ return focusExisting(canvasReducer(state, canvasActions.closeItem(itemId)), targetId);
725
+ }
726
+ const keptAll = current.kept ?? [];
727
+ const revived = keptAll.find((k) => canvasItemId(k.kind, k.key) === targetId);
728
+ const kept = keptAll.filter((k) => canvasItemId(k.kind, k.key) !== targetId);
729
+ return replaceInPlace(
730
+ state,
731
+ current,
732
+ newItem(entry.kind, entry.key, entry.title, entry.data, now, {
733
+ history: [...current.history ?? [], historyEntry(current)],
734
+ kept,
735
+ forward: forwardAll.slice(0, -1),
736
+ instance: revived?.instance ?? entry.instance
737
+ })
664
738
  );
665
739
  }
666
740
  case `${P}closeItem`: {
@@ -912,6 +986,7 @@ function toPersistableSnapshot(state, isRestorable) {
912
986
  }
913
987
 
914
988
  // src/core/rules.ts
989
+ var CANVAS_PANE_MIN_WIDTH = 280;
915
990
  var CANVAS_WEB_LAYOUT_RULES = {
916
991
  defaultWidth: CANVAS_DEFAULT_WIDTH,
917
992
  minWidth: CANVAS_MIN_WIDTH,
@@ -919,7 +994,9 @@ var CANVAS_WEB_LAYOUT_RULES = {
919
994
  // Split down 70/30 like the desktop: what opens below (notifications, a feed) is the smaller pane.
920
995
  stackSplit: 0.7,
921
996
  sideSplit: 0.5,
922
- paneMinWidth: 0
997
+ // A pane narrower than this cannot show a header (Back, a readable tab, "+", the menu, close) and a
998
+ // body side by side with another: a split right that would go under it stacks instead.
999
+ paneMinWidth: CANVAS_PANE_MIN_WIDTH
923
1000
  };
924
1001
  var clampFraction = (value, fallback) => Number.isFinite(value) ? Math.min(0.88, Math.max(0.12, value)) : fallback;
925
1002
  function resolveCanvasLayoutRules(rules) {
@@ -958,6 +1035,9 @@ var CANVAS_SHORTCUTS = [
958
1035
  { id: "canvas.toggleFullscreen", accelerator: "CmdOrCtrl+Shift+Enter", when: "canvas-focus", label: "Expand canvas", web: true },
959
1036
  { id: "canvas.splitRight", accelerator: "CmdOrCtrl+Alt+\\", when: "canvas-focus", label: "Split right", web: true },
960
1037
  { id: "canvas.splitDown", accelerator: "CmdOrCtrl+Alt+Shift+\\", when: "canvas-focus", label: "Split down", web: true },
1038
+ // Browsers own ⌘[ / ⌘] for their own history: data for a desktop host (and the header's Back / Forward).
1039
+ { id: "canvas.back", accelerator: "CmdOrCtrl+[", when: "canvas-focus", label: "Back", web: false },
1040
+ { id: "canvas.forward", accelerator: "CmdOrCtrl+]", when: "canvas-focus", label: "Forward", web: false },
961
1041
  { id: "canvas.closeTab", accelerator: "CmdOrCtrl+W", when: "canvas-focus", label: "Close tab", web: false },
962
1042
  { id: "canvas.nextTab", accelerator: "Ctrl+Tab", when: "canvas-focus", label: "Next tab", web: false },
963
1043
  { id: "canvas.previousTab", accelerator: "Ctrl+Shift+Tab", when: "canvas-focus", label: "Previous tab", web: false }
@@ -1007,11 +1087,17 @@ function runCanvasCommand(controller, id, windowId = controller.windowId) {
1007
1087
  if (!frame2?.isOpen || !pane) return false;
1008
1088
  controller.splitPane(pane.id, id === "canvas.splitRight" ? "horizontal" : "vertical", pane.itemIds.length > 1 ? pane.activeItemId ?? void 0 : void 0);
1009
1089
  return true;
1090
+ case "canvas.back":
1091
+ if (!frame2?.isOpen || !pane?.activeItemId) return false;
1092
+ return controller.back(pane.activeItemId) !== null;
1093
+ case "canvas.forward":
1094
+ if (!frame2?.isOpen || !pane?.activeItemId) return false;
1095
+ return controller.forward(pane.activeItemId) !== null;
1010
1096
  default:
1011
1097
  return false;
1012
1098
  }
1013
1099
  }
1014
- var CODE_FOR = { "\\": "Backslash", Tab: "Tab", Escape: "Escape", Enter: "Enter" };
1100
+ var CODE_FOR = { "\\": "Backslash", "[": "BracketLeft", "]": "BracketRight", Tab: "Tab", Escape: "Escape", Enter: "Enter" };
1015
1101
  function matchesCanvasAccelerator(input, accelerator, isMac3) {
1016
1102
  const parts = accelerator.split("+");
1017
1103
  const key = parts.pop() ?? "";
@@ -1128,6 +1214,13 @@ function view(shared, windowId) {
1128
1214
  dispatch(canvasActions.back(itemId));
1129
1215
  return canvasItemId(entry.kind, entry.key);
1130
1216
  },
1217
+ forward(itemId) {
1218
+ const forward = store.getState().items[itemId]?.forward ?? [];
1219
+ const entry = forward[forward.length - 1];
1220
+ if (!entry) return null;
1221
+ dispatch(canvasActions.forward(itemId));
1222
+ return canvasItemId(entry.kind, entry.key);
1223
+ },
1131
1224
  newTab(paneId) {
1132
1225
  const state = store.getState();
1133
1226
  const frame2 = canvasWindowFrame(state, windowId);
@@ -1394,6 +1487,16 @@ function selectCanvasWindowItemCount(s, windowId) {
1394
1487
  var NO_WINDOWS = Object.freeze({});
1395
1488
  var NO_HISTORY = Object.freeze([]);
1396
1489
  var selectCanvasCanGoBack = (s, itemId) => (s.items[itemId]?.history?.length ?? 0) > 0;
1490
+ var selectCanvasCanGoForward = (s, itemId) => (s.items[itemId]?.forward?.length ?? 0) > 0;
1491
+ function selectCanvasPresentedPaneId(s, windowId) {
1492
+ const f = frame(s, windowId);
1493
+ if (!f) return null;
1494
+ const ids = listPaneIds(f.layout);
1495
+ const holds = (id) => (s.panes[id]?.itemIds.length ?? 0) > 0;
1496
+ const focused = ids.find((id) => id === f.focusedPaneId);
1497
+ if (focused && holds(focused)) return focused;
1498
+ return ids.find(holds) ?? focused ?? ids[0] ?? null;
1499
+ }
1397
1500
 
1398
1501
  // src/core/presentation.ts
1399
1502
  var CANVAS_PHONE_MAX_WIDTH = 767;
@@ -1437,7 +1540,9 @@ function toggleKind(controller, input) {
1437
1540
  const before = selectCanvasKindVisibility(controller.getState(), input.kind, key);
1438
1541
  if (before === "visible") {
1439
1542
  if ((input.whenVisible ?? "close") === "close") {
1543
+ const windowId = canvasWindowOfItem(controller.getState(), itemId);
1440
1544
  controller.close(itemId);
1545
+ if (windowId && selectCanvasWindowItemCount(controller.getState(), windowId) === 0) controller.hide(windowId);
1441
1546
  } else {
1442
1547
  controller.hide(canvasWindowOfItem(controller.getState(), itemId) ?? void 0);
1443
1548
  }
@@ -1459,7 +1564,7 @@ function toggleKind(controller, input) {
1459
1564
  }
1460
1565
 
1461
1566
  // src/react/layer.tsx
1462
- import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
1567
+ import { useEffect as useEffect2, useLayoutEffect as useLayoutEffect2, useRef as useRef2, useState, useSyncExternalStore } from "react";
1463
1568
  import { createPortal } from "react-dom";
1464
1569
  import { jsx } from "react/jsx-runtime";
1465
1570
  function createCanvasLayer(getPorts) {
@@ -1528,7 +1633,7 @@ function createCanvasLayer(getPorts) {
1528
1633
  function CanvasDomOverlay({ layer }) {
1529
1634
  const elements = useSyncExternalStore(layer.dom.subscribe, layer.dom.list, layer.dom.list);
1530
1635
  const [mounted, setMounted] = useState(false);
1531
- useEffect(() => setMounted(true), []);
1636
+ useEffect2(() => setMounted(true), []);
1532
1637
  if (!mounted || elements.length === 0 || typeof document === "undefined") return null;
1533
1638
  return createPortal(
1534
1639
  /* @__PURE__ */ jsx("div", { className: "mxc-overlay", "aria-hidden": "true", children: elements.map(([id, element]) => /* @__PURE__ */ jsx(FloatingView, { element }, id)) }),
@@ -1544,11 +1649,11 @@ function FloatingView({ element }) {
1544
1649
  return null;
1545
1650
  }
1546
1651
  function Tooltip({ element }) {
1547
- const ref = useRef(null);
1652
+ const ref = useRef2(null);
1548
1653
  const { anchor, side, text } = element;
1549
1654
  const top = side === "bottom" ? anchor.y + anchor.height + 6 : anchor.y - 6;
1550
1655
  const centre = anchor.x + anchor.width / 2;
1551
- useLayoutEffect(() => {
1656
+ useLayoutEffect2(() => {
1552
1657
  const el = ref.current;
1553
1658
  if (!el) return;
1554
1659
  const width = el.offsetWidth;
@@ -1683,9 +1788,9 @@ function CanvasProvider({
1683
1788
  () => ({ itemActions, popOut, dockBack, showMenu, overlay, onNativeBodyFrame }),
1684
1789
  [itemActions, popOut, dockBack, showMenu, overlay, onNativeBodyFrame]
1685
1790
  );
1686
- const portsRef = useRef2(hostPorts);
1791
+ const portsRef = useRef3(hostPorts);
1687
1792
  portsRef.current = hostPorts;
1688
- const [{ controller, layer, launcherEntries }] = useState2(() => ({
1793
+ const [{ controller, layer, launcherEntries, itemMenus }] = useState2(() => ({
1689
1794
  controller: createCanvasController({
1690
1795
  store: store ?? createCanvasStore(),
1691
1796
  persistence: persistence === void 0 ? createLocalStorageCanvasPersistence() : persistence,
@@ -1699,11 +1804,12 @@ function CanvasProvider({
1699
1804
  preferredTarget: (kind) => getCanvasKind(kind)?.preferredTarget
1700
1805
  }),
1701
1806
  layer: createCanvasLayer(() => portsRef.current),
1702
- launcherEntries: createLauncherEntryStore()
1807
+ launcherEntries: createLauncherEntryStore(),
1808
+ itemMenus: createCanvasItemMenus()
1703
1809
  }));
1704
1810
  const windowId = controller.windowId;
1705
- useEffect2(() => controller.start(), [controller]);
1706
- useEffect2(() => {
1811
+ useEffect3(() => controller.start(), [controller]);
1812
+ useEffect3(() => {
1707
1813
  if (!hotkeys) return;
1708
1814
  const isMac3 = isMacPlatform();
1709
1815
  const applies = (when, event) => {
@@ -1725,8 +1831,8 @@ function CanvasProvider({
1725
1831
  return () => window.removeEventListener("keydown", onKey);
1726
1832
  }, [controller, hotkeys, windowId]);
1727
1833
  const value = useMemo(
1728
- () => ({ controller, windowId, ports: hostPorts, layer, regionWidth, tabStrip, launcherEntries }),
1729
- [controller, windowId, hostPorts, layer, regionWidth, tabStrip, launcherEntries]
1834
+ () => ({ controller, windowId, ports: hostPorts, layer, regionWidth, tabStrip, launcherEntries, itemMenus }),
1835
+ [controller, windowId, hostPorts, layer, regionWidth, tabStrip, launcherEntries, itemMenus]
1730
1836
  );
1731
1837
  return /* @__PURE__ */ jsxs(CanvasContext.Provider, { value, children: [
1732
1838
  children,
@@ -1771,7 +1877,7 @@ function useRegisterCanvasPresence() {
1771
1877
  const context = useContext(CanvasContext);
1772
1878
  const controller = context?.controller;
1773
1879
  const windowId = context?.windowId;
1774
- useEffect2(() => controller?.registerPresentation(windowId), [controller, windowId]);
1880
+ useEffect3(() => controller?.registerPresentation(windowId), [controller, windowId]);
1775
1881
  }
1776
1882
  var noPresence = () => () => void 0;
1777
1883
  function useCanvasIsPresented() {
@@ -1822,7 +1928,7 @@ function useCanvasLauncherEntry(entry) {
1822
1928
  const store = useContext(CanvasContext)?.launcherEntries;
1823
1929
  const fingerprint = JSON.stringify([entry.kind, entry.key, entry.title ?? null, entry.data ?? null]);
1824
1930
  const icon = entry.icon;
1825
- useEffect2(() => {
1931
+ useEffect3(() => {
1826
1932
  if (!store) return void 0;
1827
1933
  const [kind, key, title, data] = JSON.parse(fingerprint);
1828
1934
  return store.add({ kind, key, title, data, ...icon ? { icon } : {} });
@@ -1844,9 +1950,9 @@ function useCanvasLauncherEntries() {
1844
1950
 
1845
1951
  // src/react/CanvasColumn.tsx
1846
1952
  import {
1847
- useEffect as useEffect7,
1848
- useLayoutEffect as useLayoutEffect5,
1849
- useRef as useRef7,
1953
+ useEffect as useEffect8,
1954
+ useLayoutEffect as useLayoutEffect6,
1955
+ useRef as useRef8,
1850
1956
  useState as useState6,
1851
1957
  useSyncExternalStore as useSyncExternalStore4
1852
1958
  } from "react";
@@ -1858,19 +1964,19 @@ import {
1858
1964
  Suspense,
1859
1965
  lazy,
1860
1966
  memo,
1861
- useEffect as useEffect5,
1862
- useLayoutEffect as useLayoutEffect3,
1967
+ useEffect as useEffect6,
1968
+ useLayoutEffect as useLayoutEffect4,
1863
1969
  useMemo as useMemo2,
1864
- useRef as useRef5,
1970
+ useRef as useRef6,
1865
1971
  useState as useState4
1866
1972
  } from "react";
1867
1973
 
1868
1974
  // src/react/chrome.tsx
1869
1975
  import {
1870
1976
  forwardRef,
1871
- useEffect as useEffect3,
1872
- useLayoutEffect as useLayoutEffect2,
1873
- useRef as useRef3,
1977
+ useEffect as useEffect4,
1978
+ useLayoutEffect as useLayoutEffect3,
1979
+ useRef as useRef4,
1874
1980
  useState as useState3
1875
1981
  } from "react";
1876
1982
 
@@ -1970,16 +2076,15 @@ var LayoutGridIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
1970
2076
  /* @__PURE__ */ jsx3("rect", { width: "7", height: "7", x: "14", y: "14", rx: "1" }),
1971
2077
  /* @__PURE__ */ jsx3("rect", { width: "7", height: "7", x: "3", y: "14", rx: "1" })
1972
2078
  ] });
1973
- var ChevronLeftIcon = (p) => /* @__PURE__ */ jsx3(Glyph, { ...p, children: /* @__PURE__ */ jsx3("path", { d: "m15 18-6-6 6-6" }) });
1974
- var ChevronRightIcon = (p) => /* @__PURE__ */ jsx3(Glyph, { ...p, children: /* @__PURE__ */ jsx3("path", { d: "m9 18 6-6-6-6" }) });
2079
+ var ChevronDownIcon = (p) => /* @__PURE__ */ jsx3(Glyph, { ...p, children: /* @__PURE__ */ jsx3("path", { d: "m6 9 6 6 6-6" }) });
1975
2080
 
1976
2081
  // src/react/chrome.tsx
1977
2082
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1978
- var useIsoLayoutEffect = typeof window === "undefined" ? useEffect3 : useLayoutEffect2;
2083
+ var useIsoLayoutEffect2 = typeof window === "undefined" ? useEffect4 : useLayoutEffect3;
1979
2084
  function useTruncated(title) {
1980
- const ref = useRef3(null);
2085
+ const ref = useRef4(null);
1981
2086
  const [truncated, setTruncated] = useState3(false);
1982
- useIsoLayoutEffect(() => {
2087
+ useIsoLayoutEffect2(() => {
1983
2088
  const el = ref.current;
1984
2089
  if (!el) return void 0;
1985
2090
  const check = () => setTruncated(el.scrollWidth - el.clientWidth > 1);
@@ -1993,15 +2098,15 @@ function useTruncated(title) {
1993
2098
  var cx = (...names) => names.filter(Boolean).join(" ");
1994
2099
  function useTooltip(text) {
1995
2100
  const internals = useOptionalCanvasInternals();
1996
- const timer = useRef3(null);
1997
- const hide = useRef3(null);
2101
+ const timer = useRef4(null);
2102
+ const hide = useRef4(null);
1998
2103
  const close = () => {
1999
2104
  if (timer.current) clearTimeout(timer.current);
2000
2105
  timer.current = null;
2001
2106
  hide.current?.();
2002
2107
  hide.current = null;
2003
2108
  };
2004
- useEffect3(() => close, []);
2109
+ useEffect4(() => close, []);
2005
2110
  if (!text) return { close, handlers: {} };
2006
2111
  if (!internals) return { close, handlers: { title: text } };
2007
2112
  const open = (element, delay) => {
@@ -2198,7 +2303,7 @@ function BrowserTabs({ tabs, activeId, onSelect, onClose, onNew, newLabel = "New
2198
2303
  function BrowserBar({ url, canGoBack, canGoForward, onBack, onForward, onReload, onNavigate, trailing, className }) {
2199
2304
  const [draft, setDraft] = useState3(url);
2200
2305
  const [editing, setEditing] = useState3(false);
2201
- useEffect3(() => {
2306
+ useEffect4(() => {
2202
2307
  if (!editing) setDraft(url);
2203
2308
  }, [url, editing]);
2204
2309
  return /* @__PURE__ */ jsxs3("div", { "data-slot": "browser-bar", className: cx("mxc-browser-bar", className), children: [
@@ -2240,15 +2345,15 @@ function BrowserBar({ url, canGoBack, canGoForward, onBack, onForward, onReload,
2240
2345
  }
2241
2346
 
2242
2347
  // src/react/dnd.ts
2243
- import { useEffect as useEffect4, useRef as useRef4 } from "react";
2348
+ import { useEffect as useEffect5, useRef as useRef5 } from "react";
2244
2349
  var CANVAS_DRAG_MIME = "application/x-matrx-canvas-item";
2245
2350
  function useTabDrag(itemId, title) {
2246
2351
  const { layer } = useCanvasInternals();
2247
- const active = useRef4([]);
2352
+ const active = useRef5([]);
2248
2353
  const end = () => {
2249
2354
  for (const dispose of active.current.splice(0)) dispose();
2250
2355
  };
2251
- useEffect4(() => end, []);
2356
+ useEffect5(() => end, []);
2252
2357
  return {
2253
2358
  draggable: true,
2254
2359
  onDragStart: (event) => {
@@ -2263,8 +2368,8 @@ function useTabDrag(itemId, title) {
2263
2368
  function usePaneDrop(paneId, getOutline) {
2264
2369
  const canvas = useCanvas();
2265
2370
  const { layer } = useCanvasInternals();
2266
- const drop = useRef4(null);
2267
- useEffect4(() => () => drop.current?.(), []);
2371
+ const drop = useRef5(null);
2372
+ useEffect5(() => () => drop.current?.(), []);
2268
2373
  const hide = () => {
2269
2374
  drop.current?.();
2270
2375
  drop.current = null;
@@ -2395,6 +2500,9 @@ function useItemKind(kindId) {
2395
2500
  const registered = useCanvasKind(kindId);
2396
2501
  return kindId === CANVAS_LAUNCHER_KIND ? CANVAS_LAUNCHER_KIND_DEFINITION : registered;
2397
2502
  }
2503
+ function resolveItemKind(kindId, lookup) {
2504
+ return kindId === CANVAS_LAUNCHER_KIND ? CANVAS_LAUNCHER_KIND_DEFINITION : lookup(kindId);
2505
+ }
2398
2506
 
2399
2507
  // src/react/presentation.tsx
2400
2508
  import { createContext as createContext2, useContext as useContext2 } from "react";
@@ -2489,7 +2597,7 @@ function useSlotSize(slots, paneId) {
2489
2597
 
2490
2598
  // src/react/bodies.tsx
2491
2599
  import { Fragment, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2492
- var useIsoLayoutEffect2 = typeof window === "undefined" ? useEffect5 : useLayoutEffect3;
2600
+ var useIsoLayoutEffect3 = typeof window === "undefined" ? useEffect6 : useLayoutEffect4;
2493
2601
  var LAZY = /* @__PURE__ */ Symbol.for("ai-matrx.canvas.lazy");
2494
2602
  function kindComponent(kind) {
2495
2603
  if (kind.component) return kind.component;
@@ -2540,17 +2648,24 @@ function layerSignature(state, windowId, onlyPane) {
2540
2648
  }
2541
2649
  }
2542
2650
  rows.sort((a, b) => a.born - b.born || a.hostId.localeCompare(b.hostId));
2543
- const head = [frame2.isOpen ? "1" : "0", frame2.isFullscreen ? "1" : "0", frame2.focusedPaneId, String(paneIds.length)].join(SEP2);
2651
+ const head = [
2652
+ frame2.isOpen ? "1" : "0",
2653
+ frame2.isFullscreen ? "1" : "0",
2654
+ frame2.focusedPaneId,
2655
+ String(paneIds.length),
2656
+ selectCanvasPresentedPaneId(state, windowId) ?? ""
2657
+ ].join(SEP2);
2544
2658
  return [head, ...rows.map((r) => r.row)].join(ROW);
2545
2659
  }
2546
2660
  function parseSignature(signature) {
2547
2661
  if (!signature) return null;
2548
2662
  const [head = "", ...rows] = signature.split(ROW);
2549
- const [open, full, focusedPaneId = "", count = "1"] = head.split(SEP2);
2663
+ const [open, full, focusedPaneId = "", count = "1", presentedPaneId = ""] = head.split(SEP2);
2550
2664
  return {
2551
2665
  isOpen: open === "1",
2552
2666
  isFullscreen: full === "1",
2553
2667
  focusedPaneId,
2668
+ presentedPaneId,
2554
2669
  paneCount: Number(count) || 1,
2555
2670
  placed: rows.map((row) => {
2556
2671
  const [hostId = "", itemId = "", paneId = "", active, kept] = row.split(SEP2);
@@ -2602,12 +2717,12 @@ function CanvasBodyLayer() {
2602
2717
  const slots = useCanvasSlots();
2603
2718
  const signature = useCanvasState((s) => layerSignature(s, windowId));
2604
2719
  const single = useCanvasSinglePane();
2605
- const root = useRef5(null);
2720
+ const root = useRef6(null);
2606
2721
  const [placement] = useState4(
2607
2722
  () => slots ? createPlacement(() => root.current, slots, () => layer.native.remeasure()) : null
2608
2723
  );
2609
- useIsoLayoutEffect2(() => placement?.place());
2610
- useIsoLayoutEffect2(() => slots && placement ? slots.subscribe(placement.place) : void 0, [slots, placement]);
2724
+ useIsoLayoutEffect3(() => placement?.place());
2725
+ useIsoLayoutEffect3(() => slots && placement ? slots.subscribe(placement.place) : void 0, [slots, placement]);
2611
2726
  const shape = parseSignature(signature);
2612
2727
  if (!slots || !placement || !shape) return null;
2613
2728
  return /* @__PURE__ */ jsx6("div", { ref: root, className: "mxc-bodies", "data-mxc-bodies": windowId, children: shape.placed.map(({ hostId, itemId, paneId, active, kept }) => /* @__PURE__ */ jsx6(
@@ -2617,7 +2732,7 @@ function CanvasBodyLayer() {
2617
2732
  itemId,
2618
2733
  kept,
2619
2734
  paneId,
2620
- visible: shape.isOpen && active && (!single || paneId === shape.focusedPaneId),
2735
+ visible: shape.isOpen && active && (!single || paneId === shape.presentedPaneId),
2621
2736
  focused: active && shape.focusedPaneId === paneId,
2622
2737
  isFullscreen: shape.isFullscreen,
2623
2738
  paneCount: shape.paneCount,
@@ -2683,13 +2798,13 @@ var BodyHost = memo(function BodyHost2({
2683
2798
  const keptItem = useMemo2(() => step ? stepAsItem(step, owner ?? 0) : void 0, [step, owner]);
2684
2799
  const item = kept ? keptItem : live;
2685
2800
  const kind = useItemKind(item?.kind ?? "");
2686
- const element = useRef5(null);
2801
+ const element = useRef6(null);
2687
2802
  const [everShown, setEverShown] = useState4(visible);
2688
2803
  if (visible && !everShown) setEverShown(true);
2689
2804
  const mounted = kind?.unmountWhenHidden ? visible : everShown;
2690
2805
  const presentation = usePanePresentation(slots, paneId, isFullscreen, paneCount);
2691
2806
  const drop = usePaneDrop(paneId, () => slots.section(paneId));
2692
- useIsoLayoutEffect2(() => {
2807
+ useIsoLayoutEffect3(() => {
2693
2808
  const el = element.current;
2694
2809
  if (!placement || !el) return void 0;
2695
2810
  return placement.register(hostId, { element: el, paneId });
@@ -2775,8 +2890,8 @@ function KindRender({ kind, props }) {
2775
2890
  }
2776
2891
  function NativeBody({ itemId, windowId, visible, children }) {
2777
2892
  const { layer } = useCanvasInternals();
2778
- const ref = useRef5(null);
2779
- const measure = useRef5(() => void 0);
2893
+ const ref = useRef6(null);
2894
+ const measure = useRef6(() => void 0);
2780
2895
  measure.current = () => {
2781
2896
  const el = ref.current;
2782
2897
  const box = visible && el?.isConnected ? el.getBoundingClientRect() : null;
@@ -2787,8 +2902,8 @@ function NativeBody({ itemId, windowId, visible, children }) {
2787
2902
  const rect = { ...toCanvasRect(box), devicePixelRatio: window.devicePixelRatio || 1 };
2788
2903
  layer.native.report(itemId, rect, { visible: !layer.isBlocked(), windowId });
2789
2904
  };
2790
- useIsoLayoutEffect2(() => measure.current());
2791
- useIsoLayoutEffect2(() => {
2905
+ useIsoLayoutEffect3(() => measure.current());
2906
+ useIsoLayoutEffect3(() => {
2792
2907
  const el = ref.current;
2793
2908
  const run = () => measure.current();
2794
2909
  const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(run);
@@ -2840,11 +2955,28 @@ var ItemBoundary = class extends Component {
2840
2955
  }
2841
2956
  };
2842
2957
 
2958
+ // src/react/resizeGesture.ts
2959
+ function beginCanvasResize(mode) {
2960
+ if (typeof document === "undefined") return () => void 0;
2961
+ const root = document.documentElement;
2962
+ root.dataset.mxcResizing = mode;
2963
+ document.getSelection?.()?.removeAllRanges();
2964
+ const refuse = (event) => event.preventDefault();
2965
+ document.addEventListener("selectstart", refuse, true);
2966
+ let ended = false;
2967
+ return () => {
2968
+ if (ended) return;
2969
+ ended = true;
2970
+ delete root.dataset.mxcResizing;
2971
+ document.removeEventListener("selectstart", refuse, true);
2972
+ };
2973
+ }
2974
+
2843
2975
  // src/react/CanvasPane.tsx
2844
- import { Component as Component2, useEffect as useEffect6, useLayoutEffect as useLayoutEffect4, useRef as useRef6, useState as useState5 } from "react";
2976
+ import { Component as Component2, useEffect as useEffect7, useLayoutEffect as useLayoutEffect5, useRef as useRef7, useState as useState5 } from "react";
2845
2977
  import * as Menu from "@radix-ui/react-dropdown-menu";
2846
2978
  import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2847
- var useIsoLayoutEffect3 = typeof window === "undefined" ? useEffect6 : useLayoutEffect4;
2979
+ var useIsoLayoutEffect4 = typeof window === "undefined" ? useEffect7 : useLayoutEffect5;
2848
2980
  function itemTitle(item, kind) {
2849
2981
  if (item.title) return item.title;
2850
2982
  if (item.kind === CANVAS_LAUNCHER_KIND) return "New tab";
@@ -2894,14 +3026,15 @@ function CanvasPaneView({ paneId }) {
2894
3026
  const paneCount = useCanvasState((s) => selectCanvasPaneCount(s, windowId));
2895
3027
  const role = useCanvasState((s) => windowId === CANVAS_MAIN_WINDOW ? "main" : s.windows[windowId]?.role ?? "window");
2896
3028
  const canGoBack = useCanvasState((s) => active ? selectCanvasCanGoBack(s, active.id) : false);
3029
+ const canGoForward = useCanvasState((s) => active ? selectCanvasCanGoForward(s, active.id) : false);
2897
3030
  const kind = useItemKind(active?.kind ?? "");
2898
3031
  const singlePane = useCanvasSinglePane();
2899
3032
  const windowTabs = useCanvasState((s) => singlePane ? windowTabsSignature(s, windowId) : "");
2900
- const sectionRef = useRef6(null);
2901
- const bodyRef = useRef6(null);
3033
+ const sectionRef = useRef7(null);
3034
+ const bodyRef = useRef7(null);
2902
3035
  const presentation = usePanePresentation(slots, paneId, isFullscreen, paneCount);
2903
3036
  const drop = usePaneDrop(paneId, () => sectionRef.current);
2904
- useIsoLayoutEffect3(() => {
3037
+ useIsoLayoutEffect4(() => {
2905
3038
  const body = bodyRef.current;
2906
3039
  const section = sectionRef.current;
2907
3040
  if (body) slots.set(paneId, body);
@@ -2929,7 +3062,17 @@ function CanvasPaneView({ paneId }) {
2929
3062
  focused,
2930
3063
  "data-mxc-pane": paneId,
2931
3064
  header: /* @__PURE__ */ jsxs6(Fragment2, { children: [
2932
- canGoBack && active ? /* @__PURE__ */ jsx7(CanvasHeaderButton, { className: "mxc-back", label: "Back", onClick: () => void canvas.back(active.id), children: /* @__PURE__ */ jsx7(ArrowLeftIcon, {}) }) : null,
3065
+ canGoBack && active ? /* @__PURE__ */ jsx7(CanvasHeaderButton, { className: "mxc-back", label: "Back", shortcut: canvasShortcutHint("canvas.back", isMac()), onClick: () => void canvas.back(active.id), children: /* @__PURE__ */ jsx7(ArrowLeftIcon, {}) }) : null,
3066
+ canGoForward && active ? /* @__PURE__ */ jsx7(
3067
+ CanvasHeaderButton,
3068
+ {
3069
+ className: "mxc-forward",
3070
+ label: "Forward",
3071
+ shortcut: canvasShortcutHint("canvas.forward", isMac()),
3072
+ onClick: () => void canvas.forward(active.id),
3073
+ children: /* @__PURE__ */ jsx7(ArrowRightIcon, {})
3074
+ }
3075
+ ) : null,
2933
3076
  single && active ? /* @__PURE__ */ jsx7(SingleTitle, { item: active, kind }) : tabs.length > 0 ? /* @__PURE__ */ jsx7(TabStrip, { tabs, activeItemId: pane.activeItemId }) : /* @__PURE__ */ jsx7("span", { className: "mxc-panel-title", children: "New tab" }),
2934
3077
  /* @__PURE__ */ jsx7(CanvasHeaderButton, { className: "mxc-new-tab", label: "New tab", onClick: () => void canvas.newTab(paneId), children: /* @__PURE__ */ jsx7(PlusIcon, {}) })
2935
3078
  ] }),
@@ -2938,7 +3081,19 @@ function CanvasPaneView({ paneId }) {
2938
3081
  // Its own gapless row: a kind's tap buttons space themselves (the tap-target placement rule).
2939
3082
  /* @__PURE__ */ jsx7("span", { className: "mxc-kind-actions", children: /* @__PURE__ */ jsx7(Action, { ...kindProps }) })
2940
3083
  ) : null,
2941
- renderMenu: () => /* @__PURE__ */ jsx7(PaneMenu, { paneId, windowId, activeItem: active, kind, kindProps, tabCount: pane.itemIds.length, role }),
3084
+ renderMenu: () => /* @__PURE__ */ jsx7(
3085
+ PaneMenu,
3086
+ {
3087
+ paneId,
3088
+ windowId,
3089
+ activeItem: active,
3090
+ kind,
3091
+ kindProps,
3092
+ tabCount: pane.itemIds.length,
3093
+ role,
3094
+ canSplit: !singlePane
3095
+ }
3096
+ ),
2942
3097
  expanded: isFullscreen,
2943
3098
  onToggleExpand: () => canvas.setFullscreen(!isFullscreen),
2944
3099
  expandShortcut: canvasShortcutHint("canvas.toggleFullscreen", isMac()),
@@ -2972,77 +3127,135 @@ function SingleTitle({ item, kind }) {
2972
3127
  const canvas = useCanvas();
2973
3128
  const title = itemTitle(item, kind);
2974
3129
  const drag = useTabDrag(item.id, title);
3130
+ const name = useTruncated(title);
2975
3131
  const Icon = kind?.icon ?? AlertIcon;
2976
- return /* @__PURE__ */ jsxs6("span", { className: "mxc-panel-title mxc-single-title", "data-draggable": "", title, ...drag, children: [
3132
+ return /* @__PURE__ */ jsxs6("span", { className: "mxc-panel-title mxc-single-title", "data-draggable": "", "data-truncated": name.truncated ? "" : void 0, title, ...drag, children: [
2977
3133
  /* @__PURE__ */ jsx7(Icon, { className: "mxc-title-icon" }),
2978
- /* @__PURE__ */ jsx7("span", { className: "mxc-tab-title", children: title }),
3134
+ /* @__PURE__ */ jsx7("span", { ref: name.ref, className: "mxc-tab-title", children: title }),
2979
3135
  /* @__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, {}) })
2980
3136
  ] });
2981
3137
  }
3138
+ var CANVAS_TAB_MIN_WIDTH = 78;
3139
+ var CANVAS_ACTIVE_TAB_MIN_WIDTH = 100;
3140
+ var TAB_LIST_BUTTON_ROOM = 30;
3141
+ var TAB_GAP = 2;
3142
+ function fitCanvasTabs(count, activeIndex, width) {
3143
+ if (count <= 0) return { start: 0, end: 0, overflow: false, compact: false };
3144
+ const need = (n) => CANVAS_ACTIVE_TAB_MIN_WIDTH + (n - 1) * (CANVAS_TAB_MIN_WIDTH + TAB_GAP);
3145
+ if (width <= 0 || need(count) <= width) return { start: 0, end: count, overflow: false, compact: false };
3146
+ if (count === 1) return { start: 0, end: 1, overflow: false, compact: true };
3147
+ let fit = 1;
3148
+ while (fit + 1 < count && need(fit + 1) + TAB_LIST_BUTTON_ROOM <= width) fit += 1;
3149
+ const active = Math.min(Math.max(activeIndex, 0), count - 1);
3150
+ const start = active < fit ? 0 : active - fit + 1;
3151
+ return { start, end: start + fit, overflow: true, compact: need(fit) + TAB_LIST_BUTTON_ROOM > width };
3152
+ }
2982
3153
  function TabStrip({ tabs, activeItemId }) {
2983
- const strip = useRef6(null);
2984
- const [overflow, setOverflow] = useState5({ start: false, end: false });
2985
- const measure = useRef6(() => void 0);
2986
- measure.current = () => {
2987
- const el = strip.current;
2988
- if (!el) return;
2989
- const start = el.scrollLeft > 1;
2990
- const end = el.scrollLeft + el.clientWidth < el.scrollWidth - 1;
2991
- setOverflow((prev) => prev.start === start && prev.end === end ? prev : { start, end });
2992
- };
2993
- const tabKey = tabs.map((tab) => tab.itemId).join("|");
2994
- useIsoLayoutEffect3(() => {
3154
+ const canvas = useCanvas();
3155
+ const strip = useRef7(null);
3156
+ const [width, setWidth] = useState5(0);
3157
+ useIsoLayoutEffect4(() => {
2995
3158
  const el = strip.current;
2996
3159
  if (!el) return void 0;
2997
- const reveal = () => {
2998
- const tab = el.querySelector('[role="tab"][aria-selected="true"]');
2999
- if (tab) {
3000
- const left = tab.offsetLeft;
3001
- const right = left + tab.offsetWidth;
3002
- if (left < el.scrollLeft) el.scrollLeft = left;
3003
- else if (right > el.scrollLeft + el.clientWidth) el.scrollLeft = right - el.clientWidth;
3004
- }
3005
- measure.current();
3006
- };
3007
- reveal();
3008
- const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(reveal);
3160
+ const read = () => setWidth(Math.floor(el.getBoundingClientRect().width));
3161
+ read();
3162
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(read);
3009
3163
  observer?.observe(el);
3010
- for (const tab of el.querySelectorAll('[role="tab"]')) observer?.observe(tab);
3011
3164
  return () => observer?.disconnect();
3012
- }, [activeItemId, tabKey]);
3013
- const page = (direction) => {
3014
- const el = strip.current;
3015
- if (!el) return;
3016
- const by = Math.max(48, Math.round(el.clientWidth * 0.8)) * direction;
3017
- if (typeof el.scrollBy === "function") el.scrollBy({ left: by, behavior: "smooth" });
3018
- else el.scrollLeft += by;
3019
- };
3020
- const overflowing = overflow.start || overflow.end;
3165
+ }, []);
3166
+ const activeIndex = Math.max(0, tabs.findIndex((tab) => tab.itemId === activeItemId));
3167
+ const fit = fitCanvasTabs(tabs.length, activeIndex, width);
3168
+ const shown = tabs.slice(fit.start, fit.end);
3169
+ const hidden = tabs.length - shown.length;
3021
3170
  return /* @__PURE__ */ jsxs6(
3022
3171
  "div",
3023
3172
  {
3173
+ ref: strip,
3024
3174
  className: "mxc-tabstrip",
3025
- "data-overflow": overflowing ? "" : void 0,
3026
- "data-overflow-start": overflow.start ? "" : void 0,
3027
- "data-overflow-end": overflow.end ? "" : void 0,
3175
+ "data-overflow": fit.overflow ? "" : void 0,
3176
+ "data-compact": fit.compact ? "" : void 0,
3177
+ style: { ["--mxc-tab-min"]: `${CANVAS_TAB_MIN_WIDTH}px`, ["--mxc-tab-active-min"]: `${CANVAS_ACTIVE_TAB_MIN_WIDTH}px` },
3028
3178
  children: [
3029
- overflowing ? /* @__PURE__ */ jsx7(CanvasHeaderButton, { className: "mxc-tabs-scroll", label: "Scroll tabs left", available: overflow.start, onClick: () => page(-1), children: /* @__PURE__ */ jsx7(ChevronLeftIcon, {}) }) : null,
3030
3179
  /* @__PURE__ */ jsx7(
3031
3180
  "div",
3032
3181
  {
3033
- ref: strip,
3034
3182
  className: "mxc-tabs",
3035
3183
  role: "tablist",
3036
3184
  "aria-label": "Canvas tabs",
3037
- onScroll: () => measure.current(),
3038
3185
  onWheel: (event) => {
3039
- const el = strip.current;
3040
- if (el && Math.abs(event.deltaY) > Math.abs(event.deltaX)) el.scrollLeft += event.deltaY;
3186
+ if (!fit.overflow || Math.abs(event.deltaY) < Math.abs(event.deltaX) || tabs.length < 2) return;
3187
+ const next = tabs[(activeIndex + (event.deltaY > 0 ? 1 : -1) + tabs.length) % tabs.length];
3188
+ if (next) canvas.activate(next.itemId);
3041
3189
  },
3042
- children: tabs.map((tab) => /* @__PURE__ */ jsx7(ItemTab, { itemId: tab.itemId, paneId: tab.paneId, index: tab.index, active: tab.itemId === activeItemId }, tab.itemId))
3190
+ children: shown.map((tab) => /* @__PURE__ */ jsx7(ItemTab, { itemId: tab.itemId, paneId: tab.paneId, index: tab.index, active: tab.itemId === activeItemId }, tab.itemId))
3043
3191
  }
3044
3192
  ),
3045
- overflowing ? /* @__PURE__ */ jsx7(CanvasHeaderButton, { className: "mxc-tabs-scroll", label: "Scroll tabs right", available: overflow.end, onClick: () => page(1), children: /* @__PURE__ */ jsx7(ChevronRightIcon, {}) }) : null
3193
+ fit.overflow ? /* @__PURE__ */ jsx7(TabListMenu, { tabs, activeItemId, hidden }) : null
3194
+ ]
3195
+ }
3196
+ );
3197
+ }
3198
+ function TabListMenu({ tabs, activeItemId, hidden }) {
3199
+ const canvas = useCanvas();
3200
+ const { ports, layer } = useCanvasInternals();
3201
+ const blocking = useRef7(null);
3202
+ useEffect7(() => () => blocking.current?.(), []);
3203
+ const label = `All tabs (${tabs.length})`;
3204
+ const trigger = (props) => /* @__PURE__ */ jsxs6(CanvasHeaderButton, { className: "mxc-tab-list", label, "data-hidden-count": hidden, ...props, children: [
3205
+ /* @__PURE__ */ jsx7(ChevronDownIcon, {}),
3206
+ /* @__PURE__ */ jsx7("span", { className: "mxc-tab-list-count", "aria-hidden": "true", children: hidden })
3207
+ ] });
3208
+ const showMenu = ports.showMenu;
3209
+ if (showMenu) {
3210
+ return trigger({
3211
+ onClick: (event) => {
3212
+ const state = canvas.getState();
3213
+ const entries = tabs.map((tab) => {
3214
+ const item = state.items[tab.itemId];
3215
+ const title = item ? itemTitle(item, resolveItemKind(item.kind, getCanvasKind)) : tab.itemId;
3216
+ return { type: "item", id: tab.itemId, label: tab.itemId === activeItemId ? `${title} (current)` : title };
3217
+ });
3218
+ void showMenu(entries, toCanvasRect(event.currentTarget.getBoundingClientRect())).then(
3219
+ (chosen) => {
3220
+ if (chosen) canvas.activate(chosen);
3221
+ },
3222
+ (error) => console.error("[@ai-matrx/canvas] the host menu failed", error)
3223
+ );
3224
+ }
3225
+ });
3226
+ }
3227
+ return /* @__PURE__ */ jsxs6(
3228
+ Menu.Root,
3229
+ {
3230
+ onOpenChange: (open) => {
3231
+ blocking.current?.();
3232
+ blocking.current = open ? layer.beginBlocking() : null;
3233
+ },
3234
+ children: [
3235
+ /* @__PURE__ */ jsx7(Menu.Trigger, { asChild: true, children: trigger({}) }),
3236
+ /* @__PURE__ */ jsx7(Menu.Portal, { children: /* @__PURE__ */ jsx7(Menu.Content, { align: "end", sideOffset: 4, className: "mxc-menu mxc-tab-list-menu", "aria-label": "All tabs", children: tabs.map((tab) => /* @__PURE__ */ jsx7(TabListEntry, { itemId: tab.itemId, active: tab.itemId === activeItemId }, tab.itemId)) }) })
3237
+ ]
3238
+ }
3239
+ );
3240
+ }
3241
+ function TabListEntry({ itemId, active }) {
3242
+ const canvas = useCanvas();
3243
+ const item = useCanvasState((s) => s.items[itemId]);
3244
+ const kind = useItemKind(item?.kind ?? "");
3245
+ if (!item) return null;
3246
+ const Icon = kind?.icon ?? AlertIcon;
3247
+ const title = itemTitle(item, kind);
3248
+ return /* @__PURE__ */ jsxs6(
3249
+ Menu.Item,
3250
+ {
3251
+ className: "mxc-menu-item mxc-tab-list-item",
3252
+ "data-active": active ? "" : void 0,
3253
+ "aria-current": active ? "true" : void 0,
3254
+ title,
3255
+ onSelect: () => canvas.activate(itemId),
3256
+ children: [
3257
+ /* @__PURE__ */ jsx7(Icon, {}),
3258
+ /* @__PURE__ */ jsx7("span", { className: "mxc-tab-list-title", children: title })
3046
3259
  ]
3047
3260
  }
3048
3261
  );
@@ -3086,42 +3299,48 @@ function PaneMenu({
3086
3299
  kind,
3087
3300
  kindProps,
3088
3301
  tabCount,
3089
- role
3302
+ role,
3303
+ canSplit
3090
3304
  }) {
3091
3305
  const canvas = useCanvas();
3092
- const { ports, layer } = useCanvasInternals();
3306
+ const { ports, layer, itemMenus } = useCanvasInternals();
3093
3307
  const moveId = tabCount > 1 ? activeItem?.id : void 0;
3094
- const blocking = useRef6(null);
3095
- useEffect6(() => () => blocking.current?.(), []);
3096
- const kindItems = kind?.menuItems && kindProps ? safeMenu(() => kind.menuItems?.(kindProps)) : [];
3097
- const hostItems = activeItem && ports.itemActions ? safeMenu(() => ports.itemActions?.(activeItem, kind)) : [];
3098
- const sections = [
3099
- [...kindItems, ...hostItems].map((entry) => ({ ...entry, id: `item:${entry.id}` })),
3100
- [
3101
- { id: "split-right", label: "Split right", icon: /* @__PURE__ */ jsx7(SplitRightIcon, {}), onSelect: () => canvas.splitPane(paneId, "horizontal", moveId) },
3102
- { id: "split-down", label: "Split down", icon: /* @__PURE__ */ jsx7(SplitDownIcon, {}), onSelect: () => canvas.splitPane(paneId, "vertical", moveId) },
3103
- ...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) }] : []
3104
- ],
3105
- activeItem ? [
3106
- { id: "close-tab", label: "Close tab", onSelect: () => canvas.close(activeItem.id) },
3107
- ...tabCount > 1 ? [{ id: "close-others", label: "Close other tabs", onSelect: () => canvas.closeOthers(activeItem.id) }] : []
3108
- ] : []
3109
- ].filter((section) => section.length > 0);
3308
+ const blocking = useRef7(null);
3309
+ useEffect7(() => () => blocking.current?.(), []);
3310
+ const buildSections = () => {
3311
+ const kindItems = kind?.menuItems && kindProps ? safeMenu(() => kind.menuItems?.(kindProps)) : [];
3312
+ const hostItems = activeItem && ports.itemActions ? safeMenu(() => ports.itemActions?.(activeItem, kind)) : [];
3313
+ const bodyItems = activeItem ? itemMenus.read(activeItem.id) : [];
3314
+ return [
3315
+ [...kindItems, ...bodyItems, ...hostItems].map((entry) => ({ ...entry, id: `item:${entry.id}` })),
3316
+ [
3317
+ ...canSplit ? [
3318
+ { id: "split-right", label: "Split right", icon: /* @__PURE__ */ jsx7(SplitRightIcon, {}), onSelect: () => canvas.splitPane(paneId, "horizontal", moveId) },
3319
+ { id: "split-down", label: "Split down", icon: /* @__PURE__ */ jsx7(SplitDownIcon, {}), onSelect: () => canvas.splitPane(paneId, "vertical", moveId) }
3320
+ ] : [],
3321
+ ...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) }] : []
3322
+ ],
3323
+ activeItem ? [
3324
+ { id: "close-tab", label: "Close tab", onSelect: () => canvas.close(activeItem.id) },
3325
+ ...tabCount > 1 ? [{ id: "close-others", label: "Close other tabs", onSelect: () => canvas.closeOthers(activeItem.id) }] : []
3326
+ ] : []
3327
+ ].filter((section) => section.length > 0);
3328
+ };
3110
3329
  const showMenu = ports.showMenu;
3111
3330
  if (showMenu) {
3112
- const entries = [];
3113
- const handlers = /* @__PURE__ */ new Map();
3114
- sections.forEach((section, i) => {
3115
- if (i > 0) entries.push({ type: "separator" });
3116
- for (const action of section) {
3117
- entries.push({ type: "item", id: action.id, label: action.label, ...action.destructive ? { destructive: true } : {} });
3118
- handlers.set(action.id, action.onSelect);
3119
- }
3120
- });
3121
3331
  return /* @__PURE__ */ jsx7(
3122
3332
  CanvasMenuTrigger,
3123
3333
  {
3124
3334
  onClick: (event) => {
3335
+ const entries = [];
3336
+ const handlers = /* @__PURE__ */ new Map();
3337
+ buildSections().forEach((section, i) => {
3338
+ if (i > 0) entries.push({ type: "separator" });
3339
+ for (const action of section) {
3340
+ entries.push({ type: "item", id: action.id, label: action.label, ...action.destructive ? { destructive: true } : {} });
3341
+ handlers.set(action.id, action.onSelect);
3342
+ }
3343
+ });
3125
3344
  const anchor = toCanvasRect(event.currentTarget.getBoundingClientRect());
3126
3345
  void showMenu(entries, anchor).then(
3127
3346
  (chosen) => {
@@ -3142,11 +3361,14 @@ function PaneMenu({
3142
3361
  },
3143
3362
  children: [
3144
3363
  /* @__PURE__ */ jsx7(Menu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx7(CanvasMenuTrigger, {}) }),
3145
- /* @__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)) }) })
3364
+ /* @__PURE__ */ jsx7(Menu.Portal, { children: /* @__PURE__ */ jsx7(Menu.Content, { align: "end", sideOffset: 4, className: "mxc-menu", children: /* @__PURE__ */ jsx7(MenuSections, { build: buildSections }) }) })
3146
3365
  ]
3147
3366
  }
3148
3367
  );
3149
3368
  }
3369
+ function MenuSections({ build }) {
3370
+ return /* @__PURE__ */ jsx7(Fragment2, { children: build().map((section, i) => /* @__PURE__ */ jsx7(MenuSection, { first: i === 0, actions: section }, i)) });
3371
+ }
3150
3372
  function MenuSection({ first, actions }) {
3151
3373
  return /* @__PURE__ */ jsxs6(Fragment2, { children: [
3152
3374
  first ? null : /* @__PURE__ */ jsx7(Menu.Separator, { className: "mxc-menu-separator" }),
@@ -3167,7 +3389,7 @@ function safeMenu(build) {
3167
3389
 
3168
3390
  // src/react/CanvasColumn.tsx
3169
3391
  import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
3170
- var useIsoLayoutEffect4 = typeof window === "undefined" ? useEffect7 : useLayoutEffect5;
3392
+ var useIsoLayoutEffect5 = typeof window === "undefined" ? useEffect8 : useLayoutEffect6;
3171
3393
  function subscribeViewport(listener) {
3172
3394
  window.addEventListener("resize", listener);
3173
3395
  return () => window.removeEventListener("resize", listener);
@@ -3196,15 +3418,15 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
3196
3418
  const isFullscreen = useCanvasState((s) => selectCanvasIsFullscreen(s, windowId));
3197
3419
  const width = useCanvasState((s) => selectCanvasWidth(s, windowId));
3198
3420
  const layout = useCanvasState((s) => selectCanvasLayout(s, windowId));
3199
- const focusedPaneId = useCanvasState((s) => selectCanvasFocusedPaneId(s, windowId));
3421
+ const presentedPaneId = useCanvasState((s) => selectCanvasPresentedPaneId(s, windowId));
3200
3422
  const region = useRegionWidth();
3201
3423
  const viewport = useViewportWidth();
3202
3424
  const singlePane = placement === "docked" && viewport > 0 && viewport <= CANVAS_PHONE_MAX_WIDTH;
3203
3425
  const [dragWidth, setDragWidth] = useState6(null);
3204
3426
  const [slots] = useState6(createCanvasSlots);
3205
- const ref = useRef7(null);
3427
+ const ref = useRef8(null);
3206
3428
  useRegisterCanvasPresence();
3207
- useEffect7(() => {
3429
+ useEffect8(() => {
3208
3430
  const el = ref.current;
3209
3431
  if (!el) return void 0;
3210
3432
  let frame2 = 0;
@@ -3265,7 +3487,7 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
3265
3487
  }
3266
3488
  ) : null,
3267
3489
  /* @__PURE__ */ jsx8(CanvasSlotsContext.Provider, { value: slots, children: /* @__PURE__ */ jsxs7(CanvasSinglePaneContext.Provider, { value: singlePane, children: [
3268
- /* @__PURE__ */ jsx8("div", { className: "mxc-layout", "data-single-pane": singlePane ? "" : void 0, children: singlePane ? /* @__PURE__ */ jsx8(CanvasPaneView, { paneId: presentedPane(layout, focusedPaneId) }) : /* @__PURE__ */ jsx8(LayoutNodeView, { node: layout }) }),
3490
+ /* @__PURE__ */ jsx8("div", { className: "mxc-layout", "data-single-pane": singlePane ? "" : void 0, children: singlePane ? /* @__PURE__ */ jsx8(CanvasPaneView, { paneId: presentedPaneId ?? listPaneIds(layout)[0] }) : /* @__PURE__ */ jsx8(LayoutNodeView, { node: layout }) }),
3269
3491
  /* @__PURE__ */ jsx8(CanvasBodyLayer, {})
3270
3492
  ] }) })
3271
3493
  ] })
@@ -3274,13 +3496,14 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
3274
3496
  }
3275
3497
  function WidthHandle({ width, region, onDrag }) {
3276
3498
  const canvas = useCanvas();
3277
- const start = useRef7(null);
3499
+ const start = useRef8(null);
3500
+ useEffect8(() => () => start.current?.end(), []);
3278
3501
  const clamp = (next) => clampDraggedCanvasWidth(next, region, canvas.rules);
3279
3502
  const onPointerDown = (event) => {
3280
3503
  event.preventDefault();
3281
3504
  event.currentTarget.setPointerCapture(event.pointerId);
3282
- start.current = { x: event.clientX, width };
3283
- document.documentElement.dataset.mxcResizing = "";
3505
+ start.current?.end();
3506
+ start.current = { x: event.clientX, width, end: beginCanvasResize("") };
3284
3507
  };
3285
3508
  const onPointerMove = (event) => {
3286
3509
  if (!start.current) return;
@@ -3289,8 +3512,8 @@ function WidthHandle({ width, region, onDrag }) {
3289
3512
  const finish = (event) => {
3290
3513
  if (!start.current) return;
3291
3514
  const next = clamp(start.current.width + (start.current.x - event.clientX));
3515
+ start.current.end();
3292
3516
  start.current = null;
3293
- delete document.documentElement.dataset.mxcResizing;
3294
3517
  onDrag(null);
3295
3518
  canvas.setWidth(next);
3296
3519
  };
@@ -3307,6 +3530,7 @@ function WidthHandle({ width, region, onDrag }) {
3307
3530
  onPointerMove,
3308
3531
  onPointerUp: finish,
3309
3532
  onPointerCancel: finish,
3533
+ onLostPointerCapture: finish,
3310
3534
  onDoubleClick: () => canvas.setWidth(clamp(Math.round(region / 2))),
3311
3535
  onKeyDown: (event) => {
3312
3536
  if (event.key === "ArrowLeft") canvas.setWidth(clamp(width + 32));
@@ -3315,17 +3539,13 @@ function WidthHandle({ width, region, onDrag }) {
3315
3539
  }
3316
3540
  );
3317
3541
  }
3318
- function presentedPane(layout, focused) {
3319
- const ids = listPaneIds(layout);
3320
- return ids.find((id) => id === focused) ?? ids[0];
3321
- }
3322
3542
  function LayoutNodeView({ node }) {
3323
3543
  if (node.type === "pane") return /* @__PURE__ */ jsx8(CanvasPaneView, { paneId: node.paneId });
3324
3544
  return /* @__PURE__ */ jsx8(SplitView, { node });
3325
3545
  }
3326
3546
  function useMeasuredWidth(ref, enabled) {
3327
3547
  const [width, setWidth] = useState6(0);
3328
- useIsoLayoutEffect4(() => {
3548
+ useIsoLayoutEffect5(() => {
3329
3549
  const el = ref.current;
3330
3550
  if (!enabled || !el) return;
3331
3551
  setWidth(el.getBoundingClientRect().width);
@@ -3338,7 +3558,7 @@ function useMeasuredWidth(ref, enabled) {
3338
3558
  }
3339
3559
  function SplitView({ node }) {
3340
3560
  const canvas = useCanvas();
3341
- const ref = useRef7(null);
3561
+ const ref = useRef8(null);
3342
3562
  const [liveSizes, setLiveSizes] = useState6(null);
3343
3563
  const sizes = liveSizes ?? node.sizes;
3344
3564
  const measuredWidth = useMeasuredWidth(ref, node.orientation === "horizontal" && canvas.rules.paneMinWidth > 0);
@@ -3356,7 +3576,7 @@ function SplitView({ node }) {
3356
3576
  const base = [...node.sizes];
3357
3577
  const pair = (base[index] ?? 0) + (base[index + 1] ?? 0);
3358
3578
  const target = event.currentTarget;
3359
- document.documentElement.dataset.mxcResizing = horizontal ? "col" : "row";
3579
+ const end = beginCanvasResize(horizontal ? "col" : "row");
3360
3580
  const compute = (pos) => {
3361
3581
  const delta = (pos - origin) / total;
3362
3582
  const min = CANVAS_MIN_SPLIT_FRACTION;
@@ -3371,7 +3591,7 @@ function SplitView({ node }) {
3371
3591
  target.removeEventListener("pointermove", move);
3372
3592
  target.removeEventListener("pointerup", up);
3373
3593
  target.removeEventListener("pointercancel", up);
3374
- delete document.documentElement.dataset.mxcResizing;
3594
+ end();
3375
3595
  setLiveSizes(null);
3376
3596
  canvas.resizeSplit(node.id, compute(horizontal ? e.clientX : e.clientY));
3377
3597
  };
@@ -3475,6 +3695,8 @@ function CanvasFrame({ children, className }) {
3475
3695
  export {
3476
3696
  BrowserBar,
3477
3697
  BrowserTabs,
3698
+ CANVAS_ACTIVE_TAB_MIN_WIDTH,
3699
+ CANVAS_TAB_MIN_WIDTH,
3478
3700
  CanvasColumn,
3479
3701
  CanvasFrame,
3480
3702
  CanvasHeaderButton,
@@ -3487,8 +3709,10 @@ export {
3487
3709
  CanvasTab,
3488
3710
  CanvasToggle,
3489
3711
  CanvasWindowScope,
3712
+ beginCanvasResize,
3490
3713
  canvasSessionKey,
3491
3714
  defineCanvasKind,
3715
+ fitCanvasTabs,
3492
3716
  getCanvasKind,
3493
3717
  itemTitle,
3494
3718
  listCanvasKinds,
@@ -3503,6 +3727,7 @@ export {
3503
3727
  useCanvasKinds,
3504
3728
  useCanvasLauncherEntries,
3505
3729
  useCanvasLauncherEntry,
3730
+ useCanvasMenuItems,
3506
3731
  useCanvasNativeBodies,
3507
3732
  useCanvasPresentation,
3508
3733
  useCanvasState,