@ai-matrx/canvas 0.6.0 → 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 useLayoutEffect4,
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,18 +1964,19 @@ import {
1858
1964
  Suspense,
1859
1965
  lazy,
1860
1966
  memo,
1861
- useEffect as useEffect5,
1862
- useLayoutEffect as useLayoutEffect2,
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
- useRef as useRef3,
1977
+ useEffect as useEffect4,
1978
+ useLayoutEffect as useLayoutEffect3,
1979
+ useRef as useRef4,
1873
1980
  useState as useState3
1874
1981
  } from "react";
1875
1982
 
@@ -1969,23 +2076,37 @@ var LayoutGridIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
1969
2076
  /* @__PURE__ */ jsx3("rect", { width: "7", height: "7", x: "14", y: "14", rx: "1" }),
1970
2077
  /* @__PURE__ */ jsx3("rect", { width: "7", height: "7", x: "3", y: "14", rx: "1" })
1971
2078
  ] });
1972
- var ChevronLeftIcon = (p) => /* @__PURE__ */ jsx3(Glyph, { ...p, children: /* @__PURE__ */ jsx3("path", { d: "m15 18-6-6 6-6" }) });
1973
- 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" }) });
1974
2080
 
1975
2081
  // src/react/chrome.tsx
1976
2082
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2083
+ var useIsoLayoutEffect2 = typeof window === "undefined" ? useEffect4 : useLayoutEffect3;
2084
+ function useTruncated(title) {
2085
+ const ref = useRef4(null);
2086
+ const [truncated, setTruncated] = useState3(false);
2087
+ useIsoLayoutEffect2(() => {
2088
+ const el = ref.current;
2089
+ if (!el) return void 0;
2090
+ const check = () => setTruncated(el.scrollWidth - el.clientWidth > 1);
2091
+ check();
2092
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(check);
2093
+ observer?.observe(el);
2094
+ return () => observer?.disconnect();
2095
+ }, [title]);
2096
+ return { ref, truncated };
2097
+ }
1977
2098
  var cx = (...names) => names.filter(Boolean).join(" ");
1978
2099
  function useTooltip(text) {
1979
2100
  const internals = useOptionalCanvasInternals();
1980
- const timer = useRef3(null);
1981
- const hide = useRef3(null);
2101
+ const timer = useRef4(null);
2102
+ const hide = useRef4(null);
1982
2103
  const close = () => {
1983
2104
  if (timer.current) clearTimeout(timer.current);
1984
2105
  timer.current = null;
1985
2106
  hide.current?.();
1986
2107
  hide.current = null;
1987
2108
  };
1988
- useEffect3(() => close, []);
2109
+ useEffect4(() => close, []);
1989
2110
  if (!text) return { close, handlers: {} };
1990
2111
  if (!internals) return { close, handlers: { title: text } };
1991
2112
  const open = (element, delay) => {
@@ -2113,6 +2234,7 @@ function CanvasPanelEmpty({ children, className }) {
2113
2234
  }
2114
2235
  var CanvasTab = forwardRef(function CanvasTab2({ title, icon, note, active, onSelect, onClose, className, onKeyDown, onClick, ...rest }, ref) {
2115
2236
  const shown = title || "New tab";
2237
+ const name = useTruncated(shown);
2116
2238
  return /* @__PURE__ */ jsxs3(
2117
2239
  "div",
2118
2240
  {
@@ -2122,6 +2244,7 @@ var CanvasTab = forwardRef(function CanvasTab2({ title, icon, note, active, onSe
2122
2244
  tabIndex: active ? 0 : -1,
2123
2245
  "data-slot": "browser-tab",
2124
2246
  "data-active": active ? "" : void 0,
2247
+ "data-truncated": name.truncated ? "" : void 0,
2125
2248
  title: shown,
2126
2249
  className: cx("mxc-tab", className),
2127
2250
  ...rest,
@@ -2139,7 +2262,7 @@ var CanvasTab = forwardRef(function CanvasTab2({ title, icon, note, active, onSe
2139
2262
  },
2140
2263
  children: [
2141
2264
  /* @__PURE__ */ jsx4("span", { className: "mxc-tab-icon", children: icon }),
2142
- /* @__PURE__ */ jsx4("span", { className: "mxc-tab-title", children: shown }),
2265
+ /* @__PURE__ */ jsx4("span", { ref: name.ref, className: "mxc-tab-title", children: shown }),
2143
2266
  note ? /* @__PURE__ */ jsx4("span", { className: "mxc-tab-note", "data-slot": "browser-tab-note", children: note }) : null,
2144
2267
  onClose ? /* @__PURE__ */ jsx4(
2145
2268
  "button",
@@ -2180,7 +2303,7 @@ function BrowserTabs({ tabs, activeId, onSelect, onClose, onNew, newLabel = "New
2180
2303
  function BrowserBar({ url, canGoBack, canGoForward, onBack, onForward, onReload, onNavigate, trailing, className }) {
2181
2304
  const [draft, setDraft] = useState3(url);
2182
2305
  const [editing, setEditing] = useState3(false);
2183
- useEffect3(() => {
2306
+ useEffect4(() => {
2184
2307
  if (!editing) setDraft(url);
2185
2308
  }, [url, editing]);
2186
2309
  return /* @__PURE__ */ jsxs3("div", { "data-slot": "browser-bar", className: cx("mxc-browser-bar", className), children: [
@@ -2222,15 +2345,15 @@ function BrowserBar({ url, canGoBack, canGoForward, onBack, onForward, onReload,
2222
2345
  }
2223
2346
 
2224
2347
  // src/react/dnd.ts
2225
- import { useEffect as useEffect4, useRef as useRef4 } from "react";
2348
+ import { useEffect as useEffect5, useRef as useRef5 } from "react";
2226
2349
  var CANVAS_DRAG_MIME = "application/x-matrx-canvas-item";
2227
2350
  function useTabDrag(itemId, title) {
2228
2351
  const { layer } = useCanvasInternals();
2229
- const active = useRef4([]);
2352
+ const active = useRef5([]);
2230
2353
  const end = () => {
2231
2354
  for (const dispose of active.current.splice(0)) dispose();
2232
2355
  };
2233
- useEffect4(() => end, []);
2356
+ useEffect5(() => end, []);
2234
2357
  return {
2235
2358
  draggable: true,
2236
2359
  onDragStart: (event) => {
@@ -2245,8 +2368,8 @@ function useTabDrag(itemId, title) {
2245
2368
  function usePaneDrop(paneId, getOutline) {
2246
2369
  const canvas = useCanvas();
2247
2370
  const { layer } = useCanvasInternals();
2248
- const drop = useRef4(null);
2249
- useEffect4(() => () => drop.current?.(), []);
2371
+ const drop = useRef5(null);
2372
+ useEffect5(() => () => drop.current?.(), []);
2250
2373
  const hide = () => {
2251
2374
  drop.current?.();
2252
2375
  drop.current = null;
@@ -2377,6 +2500,9 @@ function useItemKind(kindId) {
2377
2500
  const registered = useCanvasKind(kindId);
2378
2501
  return kindId === CANVAS_LAUNCHER_KIND ? CANVAS_LAUNCHER_KIND_DEFINITION : registered;
2379
2502
  }
2503
+ function resolveItemKind(kindId, lookup) {
2504
+ return kindId === CANVAS_LAUNCHER_KIND ? CANVAS_LAUNCHER_KIND_DEFINITION : lookup(kindId);
2505
+ }
2380
2506
 
2381
2507
  // src/react/presentation.tsx
2382
2508
  import { createContext as createContext2, useContext as useContext2 } from "react";
@@ -2471,7 +2597,7 @@ function useSlotSize(slots, paneId) {
2471
2597
 
2472
2598
  // src/react/bodies.tsx
2473
2599
  import { Fragment, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2474
- var useIsoLayoutEffect = typeof window === "undefined" ? useEffect5 : useLayoutEffect2;
2600
+ var useIsoLayoutEffect3 = typeof window === "undefined" ? useEffect6 : useLayoutEffect4;
2475
2601
  var LAZY = /* @__PURE__ */ Symbol.for("ai-matrx.canvas.lazy");
2476
2602
  function kindComponent(kind) {
2477
2603
  if (kind.component) return kind.component;
@@ -2522,17 +2648,24 @@ function layerSignature(state, windowId, onlyPane) {
2522
2648
  }
2523
2649
  }
2524
2650
  rows.sort((a, b) => a.born - b.born || a.hostId.localeCompare(b.hostId));
2525
- 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);
2526
2658
  return [head, ...rows.map((r) => r.row)].join(ROW);
2527
2659
  }
2528
2660
  function parseSignature(signature) {
2529
2661
  if (!signature) return null;
2530
2662
  const [head = "", ...rows] = signature.split(ROW);
2531
- const [open, full, focusedPaneId = "", count = "1"] = head.split(SEP2);
2663
+ const [open, full, focusedPaneId = "", count = "1", presentedPaneId = ""] = head.split(SEP2);
2532
2664
  return {
2533
2665
  isOpen: open === "1",
2534
2666
  isFullscreen: full === "1",
2535
2667
  focusedPaneId,
2668
+ presentedPaneId,
2536
2669
  paneCount: Number(count) || 1,
2537
2670
  placed: rows.map((row) => {
2538
2671
  const [hostId = "", itemId = "", paneId = "", active, kept] = row.split(SEP2);
@@ -2584,12 +2717,12 @@ function CanvasBodyLayer() {
2584
2717
  const slots = useCanvasSlots();
2585
2718
  const signature = useCanvasState((s) => layerSignature(s, windowId));
2586
2719
  const single = useCanvasSinglePane();
2587
- const root = useRef5(null);
2720
+ const root = useRef6(null);
2588
2721
  const [placement] = useState4(
2589
2722
  () => slots ? createPlacement(() => root.current, slots, () => layer.native.remeasure()) : null
2590
2723
  );
2591
- useIsoLayoutEffect(() => placement?.place());
2592
- useIsoLayoutEffect(() => 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]);
2593
2726
  const shape = parseSignature(signature);
2594
2727
  if (!slots || !placement || !shape) return null;
2595
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(
@@ -2599,7 +2732,7 @@ function CanvasBodyLayer() {
2599
2732
  itemId,
2600
2733
  kept,
2601
2734
  paneId,
2602
- visible: shape.isOpen && active && (!single || paneId === shape.focusedPaneId),
2735
+ visible: shape.isOpen && active && (!single || paneId === shape.presentedPaneId),
2603
2736
  focused: active && shape.focusedPaneId === paneId,
2604
2737
  isFullscreen: shape.isFullscreen,
2605
2738
  paneCount: shape.paneCount,
@@ -2665,13 +2798,13 @@ var BodyHost = memo(function BodyHost2({
2665
2798
  const keptItem = useMemo2(() => step ? stepAsItem(step, owner ?? 0) : void 0, [step, owner]);
2666
2799
  const item = kept ? keptItem : live;
2667
2800
  const kind = useItemKind(item?.kind ?? "");
2668
- const element = useRef5(null);
2801
+ const element = useRef6(null);
2669
2802
  const [everShown, setEverShown] = useState4(visible);
2670
2803
  if (visible && !everShown) setEverShown(true);
2671
2804
  const mounted = kind?.unmountWhenHidden ? visible : everShown;
2672
2805
  const presentation = usePanePresentation(slots, paneId, isFullscreen, paneCount);
2673
2806
  const drop = usePaneDrop(paneId, () => slots.section(paneId));
2674
- useIsoLayoutEffect(() => {
2807
+ useIsoLayoutEffect3(() => {
2675
2808
  const el = element.current;
2676
2809
  if (!placement || !el) return void 0;
2677
2810
  return placement.register(hostId, { element: el, paneId });
@@ -2757,8 +2890,8 @@ function KindRender({ kind, props }) {
2757
2890
  }
2758
2891
  function NativeBody({ itemId, windowId, visible, children }) {
2759
2892
  const { layer } = useCanvasInternals();
2760
- const ref = useRef5(null);
2761
- const measure = useRef5(() => void 0);
2893
+ const ref = useRef6(null);
2894
+ const measure = useRef6(() => void 0);
2762
2895
  measure.current = () => {
2763
2896
  const el = ref.current;
2764
2897
  const box = visible && el?.isConnected ? el.getBoundingClientRect() : null;
@@ -2769,8 +2902,8 @@ function NativeBody({ itemId, windowId, visible, children }) {
2769
2902
  const rect = { ...toCanvasRect(box), devicePixelRatio: window.devicePixelRatio || 1 };
2770
2903
  layer.native.report(itemId, rect, { visible: !layer.isBlocked(), windowId });
2771
2904
  };
2772
- useIsoLayoutEffect(() => measure.current());
2773
- useIsoLayoutEffect(() => {
2905
+ useIsoLayoutEffect3(() => measure.current());
2906
+ useIsoLayoutEffect3(() => {
2774
2907
  const el = ref.current;
2775
2908
  const run = () => measure.current();
2776
2909
  const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(run);
@@ -2822,11 +2955,28 @@ var ItemBoundary = class extends Component {
2822
2955
  }
2823
2956
  };
2824
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
+
2825
2975
  // src/react/CanvasPane.tsx
2826
- import { Component as Component2, useEffect as useEffect6, useLayoutEffect as useLayoutEffect3, 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";
2827
2977
  import * as Menu from "@radix-ui/react-dropdown-menu";
2828
2978
  import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2829
- var useIsoLayoutEffect2 = typeof window === "undefined" ? useEffect6 : useLayoutEffect3;
2979
+ var useIsoLayoutEffect4 = typeof window === "undefined" ? useEffect7 : useLayoutEffect5;
2830
2980
  function itemTitle(item, kind) {
2831
2981
  if (item.title) return item.title;
2832
2982
  if (item.kind === CANVAS_LAUNCHER_KIND) return "New tab";
@@ -2876,14 +3026,15 @@ function CanvasPaneView({ paneId }) {
2876
3026
  const paneCount = useCanvasState((s) => selectCanvasPaneCount(s, windowId));
2877
3027
  const role = useCanvasState((s) => windowId === CANVAS_MAIN_WINDOW ? "main" : s.windows[windowId]?.role ?? "window");
2878
3028
  const canGoBack = useCanvasState((s) => active ? selectCanvasCanGoBack(s, active.id) : false);
3029
+ const canGoForward = useCanvasState((s) => active ? selectCanvasCanGoForward(s, active.id) : false);
2879
3030
  const kind = useItemKind(active?.kind ?? "");
2880
3031
  const singlePane = useCanvasSinglePane();
2881
3032
  const windowTabs = useCanvasState((s) => singlePane ? windowTabsSignature(s, windowId) : "");
2882
- const sectionRef = useRef6(null);
2883
- const bodyRef = useRef6(null);
3033
+ const sectionRef = useRef7(null);
3034
+ const bodyRef = useRef7(null);
2884
3035
  const presentation = usePanePresentation(slots, paneId, isFullscreen, paneCount);
2885
3036
  const drop = usePaneDrop(paneId, () => sectionRef.current);
2886
- useIsoLayoutEffect2(() => {
3037
+ useIsoLayoutEffect4(() => {
2887
3038
  const body = bodyRef.current;
2888
3039
  const section = sectionRef.current;
2889
3040
  if (body) slots.set(paneId, body);
@@ -2911,7 +3062,17 @@ function CanvasPaneView({ paneId }) {
2911
3062
  focused,
2912
3063
  "data-mxc-pane": paneId,
2913
3064
  header: /* @__PURE__ */ jsxs6(Fragment2, { children: [
2914
- 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,
2915
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" }),
2916
3077
  /* @__PURE__ */ jsx7(CanvasHeaderButton, { className: "mxc-new-tab", label: "New tab", onClick: () => void canvas.newTab(paneId), children: /* @__PURE__ */ jsx7(PlusIcon, {}) })
2917
3078
  ] }),
@@ -2920,7 +3081,19 @@ function CanvasPaneView({ paneId }) {
2920
3081
  // Its own gapless row: a kind's tap buttons space themselves (the tap-target placement rule).
2921
3082
  /* @__PURE__ */ jsx7("span", { className: "mxc-kind-actions", children: /* @__PURE__ */ jsx7(Action, { ...kindProps }) })
2922
3083
  ) : null,
2923
- 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
+ ),
2924
3097
  expanded: isFullscreen,
2925
3098
  onToggleExpand: () => canvas.setFullscreen(!isFullscreen),
2926
3099
  expandShortcut: canvasShortcutHint("canvas.toggleFullscreen", isMac()),
@@ -2954,77 +3127,135 @@ function SingleTitle({ item, kind }) {
2954
3127
  const canvas = useCanvas();
2955
3128
  const title = itemTitle(item, kind);
2956
3129
  const drag = useTabDrag(item.id, title);
3130
+ const name = useTruncated(title);
2957
3131
  const Icon = kind?.icon ?? AlertIcon;
2958
- 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: [
2959
3133
  /* @__PURE__ */ jsx7(Icon, { className: "mxc-title-icon" }),
2960
- /* @__PURE__ */ jsx7("span", { className: "mxc-tab-title", children: title }),
3134
+ /* @__PURE__ */ jsx7("span", { ref: name.ref, className: "mxc-tab-title", children: title }),
2961
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, {}) })
2962
3136
  ] });
2963
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
+ }
2964
3153
  function TabStrip({ tabs, activeItemId }) {
2965
- const strip = useRef6(null);
2966
- const [overflow, setOverflow] = useState5({ start: false, end: false });
2967
- const measure = useRef6(() => void 0);
2968
- measure.current = () => {
2969
- const el = strip.current;
2970
- if (!el) return;
2971
- const start = el.scrollLeft > 1;
2972
- const end = el.scrollLeft + el.clientWidth < el.scrollWidth - 1;
2973
- setOverflow((prev) => prev.start === start && prev.end === end ? prev : { start, end });
2974
- };
2975
- const tabKey = tabs.map((tab) => tab.itemId).join("|");
2976
- useIsoLayoutEffect2(() => {
3154
+ const canvas = useCanvas();
3155
+ const strip = useRef7(null);
3156
+ const [width, setWidth] = useState5(0);
3157
+ useIsoLayoutEffect4(() => {
2977
3158
  const el = strip.current;
2978
3159
  if (!el) return void 0;
2979
- const reveal = () => {
2980
- const tab = el.querySelector('[role="tab"][aria-selected="true"]');
2981
- if (tab) {
2982
- const left = tab.offsetLeft;
2983
- const right = left + tab.offsetWidth;
2984
- if (left < el.scrollLeft) el.scrollLeft = left;
2985
- else if (right > el.scrollLeft + el.clientWidth) el.scrollLeft = right - el.clientWidth;
2986
- }
2987
- measure.current();
2988
- };
2989
- reveal();
2990
- 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);
2991
3163
  observer?.observe(el);
2992
- for (const tab of el.querySelectorAll('[role="tab"]')) observer?.observe(tab);
2993
3164
  return () => observer?.disconnect();
2994
- }, [activeItemId, tabKey]);
2995
- const page = (direction) => {
2996
- const el = strip.current;
2997
- if (!el) return;
2998
- const by = Math.max(48, Math.round(el.clientWidth * 0.8)) * direction;
2999
- if (typeof el.scrollBy === "function") el.scrollBy({ left: by, behavior: "smooth" });
3000
- else el.scrollLeft += by;
3001
- };
3002
- 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;
3003
3170
  return /* @__PURE__ */ jsxs6(
3004
3171
  "div",
3005
3172
  {
3173
+ ref: strip,
3006
3174
  className: "mxc-tabstrip",
3007
- "data-overflow": overflowing ? "" : void 0,
3008
- "data-overflow-start": overflow.start ? "" : void 0,
3009
- "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` },
3010
3178
  children: [
3011
- overflowing ? /* @__PURE__ */ jsx7(CanvasHeaderButton, { className: "mxc-tabs-scroll", label: "Scroll tabs left", available: overflow.start, onClick: () => page(-1), children: /* @__PURE__ */ jsx7(ChevronLeftIcon, {}) }) : null,
3012
3179
  /* @__PURE__ */ jsx7(
3013
3180
  "div",
3014
3181
  {
3015
- ref: strip,
3016
3182
  className: "mxc-tabs",
3017
3183
  role: "tablist",
3018
3184
  "aria-label": "Canvas tabs",
3019
- onScroll: () => measure.current(),
3020
3185
  onWheel: (event) => {
3021
- const el = strip.current;
3022
- 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);
3023
3189
  },
3024
- 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))
3025
3191
  }
3026
3192
  ),
3027
- 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 })
3028
3259
  ]
3029
3260
  }
3030
3261
  );
@@ -3068,42 +3299,48 @@ function PaneMenu({
3068
3299
  kind,
3069
3300
  kindProps,
3070
3301
  tabCount,
3071
- role
3302
+ role,
3303
+ canSplit
3072
3304
  }) {
3073
3305
  const canvas = useCanvas();
3074
- const { ports, layer } = useCanvasInternals();
3306
+ const { ports, layer, itemMenus } = useCanvasInternals();
3075
3307
  const moveId = tabCount > 1 ? activeItem?.id : void 0;
3076
- const blocking = useRef6(null);
3077
- useEffect6(() => () => blocking.current?.(), []);
3078
- const kindItems = kind?.menuItems && kindProps ? safeMenu(() => kind.menuItems?.(kindProps)) : [];
3079
- const hostItems = activeItem && ports.itemActions ? safeMenu(() => ports.itemActions?.(activeItem, kind)) : [];
3080
- const sections = [
3081
- [...kindItems, ...hostItems].map((entry) => ({ ...entry, id: `item:${entry.id}` })),
3082
- [
3083
- { id: "split-right", label: "Split right", icon: /* @__PURE__ */ jsx7(SplitRightIcon, {}), onSelect: () => canvas.splitPane(paneId, "horizontal", moveId) },
3084
- { id: "split-down", label: "Split down", icon: /* @__PURE__ */ jsx7(SplitDownIcon, {}), onSelect: () => canvas.splitPane(paneId, "vertical", moveId) },
3085
- ...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) }] : []
3086
- ],
3087
- activeItem ? [
3088
- { id: "close-tab", label: "Close tab", onSelect: () => canvas.close(activeItem.id) },
3089
- ...tabCount > 1 ? [{ id: "close-others", label: "Close other tabs", onSelect: () => canvas.closeOthers(activeItem.id) }] : []
3090
- ] : []
3091
- ].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
+ };
3092
3329
  const showMenu = ports.showMenu;
3093
3330
  if (showMenu) {
3094
- const entries = [];
3095
- const handlers = /* @__PURE__ */ new Map();
3096
- sections.forEach((section, i) => {
3097
- if (i > 0) entries.push({ type: "separator" });
3098
- for (const action of section) {
3099
- entries.push({ type: "item", id: action.id, label: action.label, ...action.destructive ? { destructive: true } : {} });
3100
- handlers.set(action.id, action.onSelect);
3101
- }
3102
- });
3103
3331
  return /* @__PURE__ */ jsx7(
3104
3332
  CanvasMenuTrigger,
3105
3333
  {
3106
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
+ });
3107
3344
  const anchor = toCanvasRect(event.currentTarget.getBoundingClientRect());
3108
3345
  void showMenu(entries, anchor).then(
3109
3346
  (chosen) => {
@@ -3124,11 +3361,14 @@ function PaneMenu({
3124
3361
  },
3125
3362
  children: [
3126
3363
  /* @__PURE__ */ jsx7(Menu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx7(CanvasMenuTrigger, {}) }),
3127
- /* @__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 }) }) })
3128
3365
  ]
3129
3366
  }
3130
3367
  );
3131
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
+ }
3132
3372
  function MenuSection({ first, actions }) {
3133
3373
  return /* @__PURE__ */ jsxs6(Fragment2, { children: [
3134
3374
  first ? null : /* @__PURE__ */ jsx7(Menu.Separator, { className: "mxc-menu-separator" }),
@@ -3149,7 +3389,7 @@ function safeMenu(build) {
3149
3389
 
3150
3390
  // src/react/CanvasColumn.tsx
3151
3391
  import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
3152
- var useIsoLayoutEffect3 = typeof window === "undefined" ? useEffect7 : useLayoutEffect4;
3392
+ var useIsoLayoutEffect5 = typeof window === "undefined" ? useEffect8 : useLayoutEffect6;
3153
3393
  function subscribeViewport(listener) {
3154
3394
  window.addEventListener("resize", listener);
3155
3395
  return () => window.removeEventListener("resize", listener);
@@ -3178,15 +3418,15 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
3178
3418
  const isFullscreen = useCanvasState((s) => selectCanvasIsFullscreen(s, windowId));
3179
3419
  const width = useCanvasState((s) => selectCanvasWidth(s, windowId));
3180
3420
  const layout = useCanvasState((s) => selectCanvasLayout(s, windowId));
3181
- const focusedPaneId = useCanvasState((s) => selectCanvasFocusedPaneId(s, windowId));
3421
+ const presentedPaneId = useCanvasState((s) => selectCanvasPresentedPaneId(s, windowId));
3182
3422
  const region = useRegionWidth();
3183
3423
  const viewport = useViewportWidth();
3184
3424
  const singlePane = placement === "docked" && viewport > 0 && viewport <= CANVAS_PHONE_MAX_WIDTH;
3185
3425
  const [dragWidth, setDragWidth] = useState6(null);
3186
3426
  const [slots] = useState6(createCanvasSlots);
3187
- const ref = useRef7(null);
3427
+ const ref = useRef8(null);
3188
3428
  useRegisterCanvasPresence();
3189
- useEffect7(() => {
3429
+ useEffect8(() => {
3190
3430
  const el = ref.current;
3191
3431
  if (!el) return void 0;
3192
3432
  let frame2 = 0;
@@ -3247,7 +3487,7 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
3247
3487
  }
3248
3488
  ) : null,
3249
3489
  /* @__PURE__ */ jsx8(CanvasSlotsContext.Provider, { value: slots, children: /* @__PURE__ */ jsxs7(CanvasSinglePaneContext.Provider, { value: singlePane, children: [
3250
- /* @__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 }) }),
3251
3491
  /* @__PURE__ */ jsx8(CanvasBodyLayer, {})
3252
3492
  ] }) })
3253
3493
  ] })
@@ -3256,13 +3496,14 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
3256
3496
  }
3257
3497
  function WidthHandle({ width, region, onDrag }) {
3258
3498
  const canvas = useCanvas();
3259
- const start = useRef7(null);
3499
+ const start = useRef8(null);
3500
+ useEffect8(() => () => start.current?.end(), []);
3260
3501
  const clamp = (next) => clampDraggedCanvasWidth(next, region, canvas.rules);
3261
3502
  const onPointerDown = (event) => {
3262
3503
  event.preventDefault();
3263
3504
  event.currentTarget.setPointerCapture(event.pointerId);
3264
- start.current = { x: event.clientX, width };
3265
- document.documentElement.dataset.mxcResizing = "";
3505
+ start.current?.end();
3506
+ start.current = { x: event.clientX, width, end: beginCanvasResize("") };
3266
3507
  };
3267
3508
  const onPointerMove = (event) => {
3268
3509
  if (!start.current) return;
@@ -3271,8 +3512,8 @@ function WidthHandle({ width, region, onDrag }) {
3271
3512
  const finish = (event) => {
3272
3513
  if (!start.current) return;
3273
3514
  const next = clamp(start.current.width + (start.current.x - event.clientX));
3515
+ start.current.end();
3274
3516
  start.current = null;
3275
- delete document.documentElement.dataset.mxcResizing;
3276
3517
  onDrag(null);
3277
3518
  canvas.setWidth(next);
3278
3519
  };
@@ -3289,6 +3530,7 @@ function WidthHandle({ width, region, onDrag }) {
3289
3530
  onPointerMove,
3290
3531
  onPointerUp: finish,
3291
3532
  onPointerCancel: finish,
3533
+ onLostPointerCapture: finish,
3292
3534
  onDoubleClick: () => canvas.setWidth(clamp(Math.round(region / 2))),
3293
3535
  onKeyDown: (event) => {
3294
3536
  if (event.key === "ArrowLeft") canvas.setWidth(clamp(width + 32));
@@ -3297,17 +3539,13 @@ function WidthHandle({ width, region, onDrag }) {
3297
3539
  }
3298
3540
  );
3299
3541
  }
3300
- function presentedPane(layout, focused) {
3301
- const ids = listPaneIds(layout);
3302
- return ids.find((id) => id === focused) ?? ids[0];
3303
- }
3304
3542
  function LayoutNodeView({ node }) {
3305
3543
  if (node.type === "pane") return /* @__PURE__ */ jsx8(CanvasPaneView, { paneId: node.paneId });
3306
3544
  return /* @__PURE__ */ jsx8(SplitView, { node });
3307
3545
  }
3308
3546
  function useMeasuredWidth(ref, enabled) {
3309
3547
  const [width, setWidth] = useState6(0);
3310
- useIsoLayoutEffect3(() => {
3548
+ useIsoLayoutEffect5(() => {
3311
3549
  const el = ref.current;
3312
3550
  if (!enabled || !el) return;
3313
3551
  setWidth(el.getBoundingClientRect().width);
@@ -3320,7 +3558,7 @@ function useMeasuredWidth(ref, enabled) {
3320
3558
  }
3321
3559
  function SplitView({ node }) {
3322
3560
  const canvas = useCanvas();
3323
- const ref = useRef7(null);
3561
+ const ref = useRef8(null);
3324
3562
  const [liveSizes, setLiveSizes] = useState6(null);
3325
3563
  const sizes = liveSizes ?? node.sizes;
3326
3564
  const measuredWidth = useMeasuredWidth(ref, node.orientation === "horizontal" && canvas.rules.paneMinWidth > 0);
@@ -3338,7 +3576,7 @@ function SplitView({ node }) {
3338
3576
  const base = [...node.sizes];
3339
3577
  const pair = (base[index] ?? 0) + (base[index + 1] ?? 0);
3340
3578
  const target = event.currentTarget;
3341
- document.documentElement.dataset.mxcResizing = horizontal ? "col" : "row";
3579
+ const end = beginCanvasResize(horizontal ? "col" : "row");
3342
3580
  const compute = (pos) => {
3343
3581
  const delta = (pos - origin) / total;
3344
3582
  const min = CANVAS_MIN_SPLIT_FRACTION;
@@ -3353,7 +3591,7 @@ function SplitView({ node }) {
3353
3591
  target.removeEventListener("pointermove", move);
3354
3592
  target.removeEventListener("pointerup", up);
3355
3593
  target.removeEventListener("pointercancel", up);
3356
- delete document.documentElement.dataset.mxcResizing;
3594
+ end();
3357
3595
  setLiveSizes(null);
3358
3596
  canvas.resizeSplit(node.id, compute(horizontal ? e.clientX : e.clientY));
3359
3597
  };
@@ -3457,6 +3695,8 @@ function CanvasFrame({ children, className }) {
3457
3695
  export {
3458
3696
  BrowserBar,
3459
3697
  BrowserTabs,
3698
+ CANVAS_ACTIVE_TAB_MIN_WIDTH,
3699
+ CANVAS_TAB_MIN_WIDTH,
3460
3700
  CanvasColumn,
3461
3701
  CanvasFrame,
3462
3702
  CanvasHeaderButton,
@@ -3469,8 +3709,10 @@ export {
3469
3709
  CanvasTab,
3470
3710
  CanvasToggle,
3471
3711
  CanvasWindowScope,
3712
+ beginCanvasResize,
3472
3713
  canvasSessionKey,
3473
3714
  defineCanvasKind,
3715
+ fitCanvasTabs,
3474
3716
  getCanvasKind,
3475
3717
  itemTitle,
3476
3718
  listCanvasKinds,
@@ -3485,6 +3727,7 @@ export {
3485
3727
  useCanvasKinds,
3486
3728
  useCanvasLauncherEntries,
3487
3729
  useCanvasLauncherEntry,
3730
+ useCanvasMenuItems,
3488
3731
  useCanvasNativeBodies,
3489
3732
  useCanvasPresentation,
3490
3733
  useCanvasState,