@ai-matrx/canvas 0.8.5 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ComponentType, ReactNode, CSSProperties, ButtonHTMLAttributes, HTMLAttributes, ReactElement } from 'react';
3
- import { b as CanvasJson, i as CanvasItem, d as CanvasPaneId, h as CanvasController, a as CanvasWindowId, O as CanvasPresentation, T as CanvasSessionEvent, R as CanvasSessionAttachReason, S as CanvasSessionDetachReason, N as CanvasPreferredTarget, C as CanvasItemId, Q as CanvasRect, e as CanvasStoreBinding, M as CanvasPersistencePort, E as CanvasErrorSink, H as CanvasLayoutRules, _ as ToggleKindInput, G as CanvasKindVisibility, f as CanvasState } from './toggleKind-BoFnqhzq.cjs';
3
+ import { b as CanvasJson, i as CanvasItem, d as CanvasPaneId, h as CanvasController, a as CanvasWindowId, Q as CanvasPresentation, V as CanvasSessionEvent, T as CanvasSessionAttachReason, U as CanvasSessionDetachReason, P as CanvasPreferredTarget, C as CanvasItemId, S as CanvasRect, e as CanvasStoreBinding, O as CanvasPersistencePort, F as CanvasErrorSink, I as CanvasLayoutRules, a0 as ToggleKindInput, H as CanvasKindVisibility, f as CanvasState } from './toggleKind-CI2nBjum.cjs';
4
4
 
5
5
  /**
6
6
  * THE canvas kind registry. A kind is one sort of thing the canvas can show —
@@ -392,13 +392,29 @@ declare function CanvasPaneView({ paneId }: {
392
392
  paneId: CanvasPaneId;
393
393
  }): react.JSX.Element | null;
394
394
  /**
395
- * Every tab's floor (px): icon + ~10 letters + "…" — still readable. Tabs shrink
396
- * evenly from 12rem down to this; past it the strip scrolls. Mirrored to CSS as
397
- * --mxc-tab-min.
395
+ * Every tab's floor (px): icon + ~10 letters + "…" — still readable. Tabs share
396
+ * the strip evenly from 12rem down to this; a tab that would go under it is not
397
+ * drawn (it waits in "All tabs"). Mirrored to CSS as --mxc-tab-min.
398
398
  */
399
399
  declare const CANVAS_TAB_MIN_WIDTH = 120;
400
- /** True when `count` tabs at their floor do not fit `width` px: the strip scrolls and offers "All tabs". */
401
- declare function canvasTabsOverflow(count: number, width: number): boolean;
400
+ /** Room kept for "All tabs" (its chevron and the count of tabs out of the strip) while it shows. */
401
+ declare const CANVAS_TAB_LIST_ROOM = 44;
402
+ /** The run of strip entries drawn whole: `start` (inclusive) to `end` (exclusive). */
403
+ interface CanvasTabWindow {
404
+ readonly start: number;
405
+ readonly end: number;
406
+ /** True when even the current entry alone is wider than the room: it is drawn under its floor (ellipsis). */
407
+ readonly squeezed: boolean;
408
+ }
409
+ /**
410
+ * Which strip entries are drawn: every one when they fit at their floor; else
411
+ * the run that fits WHOLE beside "All tabs" and holds the current entry, moved
412
+ * as little as possible from `previousStart` (so switching tabs does not
413
+ * reshuffle the strip). A tab is never drawn sliced at an edge — the rest wait
414
+ * in "All tabs". `units` is each entry's width in tabs (a split's joined tab
415
+ * counts its halves). An unmeasured strip (width 0) draws everything.
416
+ */
417
+ declare function fitCanvasTabWindow(units: readonly number[], current: number, width: number, previousStart?: number): CanvasTabWindow;
402
418
  /** One entry of a strip: a tab, or a split's visible tabs drawn as ONE joined tab (Chrome split view). */
403
419
  type CanvasStripEntry = {
404
420
  readonly type: "tab";
@@ -531,4 +547,4 @@ interface BrowserBarProps {
531
547
  /** A browser's back · forward · reload · address bar, for a kind's leading slot or its body. */
532
548
  declare function BrowserBar({ url, canGoBack, canGoForward, onBack, onForward, onReload, onNavigate, trailing, className }: BrowserBarProps): react.JSX.Element;
533
549
 
534
- export { type AnyCanvasKind, BrowserBar, type BrowserBarProps, type BrowserTab, BrowserTabs, type BrowserTabsProps, CANVAS_TAB_MIN_WIDTH, CanvasColumn, type CanvasColumnProps, type CanvasFloating, CanvasFrame, CanvasHeaderButton, type CanvasHeaderButtonProps, type CanvasHostPorts, type CanvasKind, type CanvasKindIcon, CanvasKindIcons, type CanvasKindProps, type CanvasLauncherEntry, type CanvasMenuEntry, type CanvasMenuItem, CanvasMenuTrigger, type CanvasNativeBodyFrame, type CanvasNativeBodyInfo, type CanvasNativeRect, type CanvasOverlayPort, CanvasPaneView, CanvasPanel, CanvasPanelEmpty, type CanvasPanelProps, CanvasProvider, type CanvasProviderProps, type CanvasShowMenu, type CanvasStripEntry, type CanvasStripTab, CanvasTab, type CanvasTabProps, type CanvasTabStripMode, CanvasToggle, CanvasWindowScope, CanvasWindowTabs, beginCanvasResize, canvasSessionKey, canvasTabsOverflow, defineCanvasKind, getCanvasKind, itemTitle, listCanvasKinds, planCanvasTabStrip, registerCanvasKind, registerCanvasKinds, useCanvas, useCanvasColumnWidth, useCanvasHostPorts, useCanvasIsPresented, useCanvasKind, useCanvasKindToggle, useCanvasKinds, useCanvasLauncherEntries, useCanvasLauncherEntry, useCanvasMenuItems, useCanvasNativeBodies, useCanvasPresentation, useCanvasState, useCanvasWindowId, useOptionalCanvas, useOptionalCanvasState };
550
+ export { type AnyCanvasKind, BrowserBar, type BrowserBarProps, type BrowserTab, BrowserTabs, type BrowserTabsProps, CANVAS_TAB_LIST_ROOM, CANVAS_TAB_MIN_WIDTH, CanvasColumn, type CanvasColumnProps, type CanvasFloating, CanvasFrame, CanvasHeaderButton, type CanvasHeaderButtonProps, type CanvasHostPorts, type CanvasKind, type CanvasKindIcon, CanvasKindIcons, type CanvasKindProps, type CanvasLauncherEntry, type CanvasMenuEntry, type CanvasMenuItem, CanvasMenuTrigger, type CanvasNativeBodyFrame, type CanvasNativeBodyInfo, type CanvasNativeRect, type CanvasOverlayPort, CanvasPaneView, CanvasPanel, CanvasPanelEmpty, type CanvasPanelProps, CanvasProvider, type CanvasProviderProps, type CanvasShowMenu, type CanvasStripEntry, type CanvasStripTab, CanvasTab, type CanvasTabProps, type CanvasTabStripMode, type CanvasTabWindow, CanvasToggle, CanvasWindowScope, CanvasWindowTabs, beginCanvasResize, canvasSessionKey, defineCanvasKind, fitCanvasTabWindow, getCanvasKind, itemTitle, listCanvasKinds, planCanvasTabStrip, registerCanvasKind, registerCanvasKinds, useCanvas, useCanvasColumnWidth, useCanvasHostPorts, useCanvasIsPresented, useCanvasKind, useCanvasKindToggle, useCanvasKinds, useCanvasLauncherEntries, useCanvasLauncherEntry, useCanvasMenuItems, useCanvasNativeBodies, useCanvasPresentation, useCanvasState, useCanvasWindowId, useOptionalCanvas, useOptionalCanvasState };
package/dist/react.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ComponentType, ReactNode, CSSProperties, ButtonHTMLAttributes, HTMLAttributes, ReactElement } from 'react';
3
- import { b as CanvasJson, i as CanvasItem, d as CanvasPaneId, h as CanvasController, a as CanvasWindowId, O as CanvasPresentation, T as CanvasSessionEvent, R as CanvasSessionAttachReason, S as CanvasSessionDetachReason, N as CanvasPreferredTarget, C as CanvasItemId, Q as CanvasRect, e as CanvasStoreBinding, M as CanvasPersistencePort, E as CanvasErrorSink, H as CanvasLayoutRules, _ as ToggleKindInput, G as CanvasKindVisibility, f as CanvasState } from './toggleKind-BoFnqhzq.js';
3
+ import { b as CanvasJson, i as CanvasItem, d as CanvasPaneId, h as CanvasController, a as CanvasWindowId, Q as CanvasPresentation, V as CanvasSessionEvent, T as CanvasSessionAttachReason, U as CanvasSessionDetachReason, P as CanvasPreferredTarget, C as CanvasItemId, S as CanvasRect, e as CanvasStoreBinding, O as CanvasPersistencePort, F as CanvasErrorSink, I as CanvasLayoutRules, a0 as ToggleKindInput, H as CanvasKindVisibility, f as CanvasState } from './toggleKind-CI2nBjum.js';
4
4
 
5
5
  /**
6
6
  * THE canvas kind registry. A kind is one sort of thing the canvas can show —
@@ -392,13 +392,29 @@ declare function CanvasPaneView({ paneId }: {
392
392
  paneId: CanvasPaneId;
393
393
  }): react.JSX.Element | null;
394
394
  /**
395
- * Every tab's floor (px): icon + ~10 letters + "…" — still readable. Tabs shrink
396
- * evenly from 12rem down to this; past it the strip scrolls. Mirrored to CSS as
397
- * --mxc-tab-min.
395
+ * Every tab's floor (px): icon + ~10 letters + "…" — still readable. Tabs share
396
+ * the strip evenly from 12rem down to this; a tab that would go under it is not
397
+ * drawn (it waits in "All tabs"). Mirrored to CSS as --mxc-tab-min.
398
398
  */
399
399
  declare const CANVAS_TAB_MIN_WIDTH = 120;
400
- /** True when `count` tabs at their floor do not fit `width` px: the strip scrolls and offers "All tabs". */
401
- declare function canvasTabsOverflow(count: number, width: number): boolean;
400
+ /** Room kept for "All tabs" (its chevron and the count of tabs out of the strip) while it shows. */
401
+ declare const CANVAS_TAB_LIST_ROOM = 44;
402
+ /** The run of strip entries drawn whole: `start` (inclusive) to `end` (exclusive). */
403
+ interface CanvasTabWindow {
404
+ readonly start: number;
405
+ readonly end: number;
406
+ /** True when even the current entry alone is wider than the room: it is drawn under its floor (ellipsis). */
407
+ readonly squeezed: boolean;
408
+ }
409
+ /**
410
+ * Which strip entries are drawn: every one when they fit at their floor; else
411
+ * the run that fits WHOLE beside "All tabs" and holds the current entry, moved
412
+ * as little as possible from `previousStart` (so switching tabs does not
413
+ * reshuffle the strip). A tab is never drawn sliced at an edge — the rest wait
414
+ * in "All tabs". `units` is each entry's width in tabs (a split's joined tab
415
+ * counts its halves). An unmeasured strip (width 0) draws everything.
416
+ */
417
+ declare function fitCanvasTabWindow(units: readonly number[], current: number, width: number, previousStart?: number): CanvasTabWindow;
402
418
  /** One entry of a strip: a tab, or a split's visible tabs drawn as ONE joined tab (Chrome split view). */
403
419
  type CanvasStripEntry = {
404
420
  readonly type: "tab";
@@ -531,4 +547,4 @@ interface BrowserBarProps {
531
547
  /** A browser's back · forward · reload · address bar, for a kind's leading slot or its body. */
532
548
  declare function BrowserBar({ url, canGoBack, canGoForward, onBack, onForward, onReload, onNavigate, trailing, className }: BrowserBarProps): react.JSX.Element;
533
549
 
534
- export { type AnyCanvasKind, BrowserBar, type BrowserBarProps, type BrowserTab, BrowserTabs, type BrowserTabsProps, CANVAS_TAB_MIN_WIDTH, CanvasColumn, type CanvasColumnProps, type CanvasFloating, CanvasFrame, CanvasHeaderButton, type CanvasHeaderButtonProps, type CanvasHostPorts, type CanvasKind, type CanvasKindIcon, CanvasKindIcons, type CanvasKindProps, type CanvasLauncherEntry, type CanvasMenuEntry, type CanvasMenuItem, CanvasMenuTrigger, type CanvasNativeBodyFrame, type CanvasNativeBodyInfo, type CanvasNativeRect, type CanvasOverlayPort, CanvasPaneView, CanvasPanel, CanvasPanelEmpty, type CanvasPanelProps, CanvasProvider, type CanvasProviderProps, type CanvasShowMenu, type CanvasStripEntry, type CanvasStripTab, CanvasTab, type CanvasTabProps, type CanvasTabStripMode, CanvasToggle, CanvasWindowScope, CanvasWindowTabs, beginCanvasResize, canvasSessionKey, canvasTabsOverflow, defineCanvasKind, getCanvasKind, itemTitle, listCanvasKinds, planCanvasTabStrip, registerCanvasKind, registerCanvasKinds, useCanvas, useCanvasColumnWidth, useCanvasHostPorts, useCanvasIsPresented, useCanvasKind, useCanvasKindToggle, useCanvasKinds, useCanvasLauncherEntries, useCanvasLauncherEntry, useCanvasMenuItems, useCanvasNativeBodies, useCanvasPresentation, useCanvasState, useCanvasWindowId, useOptionalCanvas, useOptionalCanvasState };
550
+ export { type AnyCanvasKind, BrowserBar, type BrowserBarProps, type BrowserTab, BrowserTabs, type BrowserTabsProps, CANVAS_TAB_LIST_ROOM, CANVAS_TAB_MIN_WIDTH, CanvasColumn, type CanvasColumnProps, type CanvasFloating, CanvasFrame, CanvasHeaderButton, type CanvasHeaderButtonProps, type CanvasHostPorts, type CanvasKind, type CanvasKindIcon, CanvasKindIcons, type CanvasKindProps, type CanvasLauncherEntry, type CanvasMenuEntry, type CanvasMenuItem, CanvasMenuTrigger, type CanvasNativeBodyFrame, type CanvasNativeBodyInfo, type CanvasNativeRect, type CanvasOverlayPort, CanvasPaneView, CanvasPanel, CanvasPanelEmpty, type CanvasPanelProps, CanvasProvider, type CanvasProviderProps, type CanvasShowMenu, type CanvasStripEntry, type CanvasStripTab, CanvasTab, type CanvasTabProps, type CanvasTabStripMode, type CanvasTabWindow, CanvasToggle, CanvasWindowScope, CanvasWindowTabs, beginCanvasResize, canvasSessionKey, defineCanvasKind, fitCanvasTabWindow, getCanvasKind, itemTitle, listCanvasKinds, planCanvasTabStrip, registerCanvasKind, registerCanvasKinds, useCanvas, useCanvasColumnWidth, useCanvasHostPorts, useCanvasIsPresented, useCanvasKind, useCanvasKindToggle, useCanvasKinds, useCanvasLauncherEntries, useCanvasLauncherEntry, useCanvasMenuItems, useCanvasNativeBodies, useCanvasPresentation, useCanvasState, useCanvasWindowId, useOptionalCanvas, useOptionalCanvasState };
package/dist/react.js CHANGED
@@ -987,6 +987,7 @@ function toPersistableSnapshot(state, isRestorable) {
987
987
 
988
988
  // src/core/rules.ts
989
989
  var CANVAS_PANE_MIN_WIDTH = 280;
990
+ var CANVAS_PANE_MIN_HEIGHT = 200;
990
991
  var CANVAS_WEB_LAYOUT_RULES = {
991
992
  defaultWidth: CANVAS_DEFAULT_WIDTH,
992
993
  minWidth: CANVAS_MIN_WIDTH,
@@ -1000,7 +1001,10 @@ var CANVAS_WEB_LAYOUT_RULES = {
1000
1001
  sideSplit: 0.5,
1001
1002
  // A pane narrower than this cannot show a header (Back, a readable tab, "+", the menu, close) and a
1002
1003
  // body side by side with another: a split right that would go under it stacks instead.
1003
- paneMinWidth: CANVAS_PANE_MIN_WIDTH
1004
+ paneMinWidth: CANVAS_PANE_MIN_WIDTH,
1005
+ // 2026-10-07 live: Quick Notes / Notifications / Quick Chat stacked in an 800px window left
1006
+ // Notifications its header and one row.
1007
+ paneMinHeight: CANVAS_PANE_MIN_HEIGHT
1004
1008
  };
1005
1009
  var clampFraction = (value, fallback) => Number.isFinite(value) ? Math.min(0.88, Math.max(0.12, value)) : fallback;
1006
1010
  function resolveCanvasLayoutRules(rules) {
@@ -1012,7 +1016,8 @@ function resolveCanvasLayoutRules(rules) {
1012
1016
  centreMinWidth: positive(merged.centreMinWidth, CANVAS_WEB_LAYOUT_RULES.centreMinWidth),
1013
1017
  stackSplit: clampFraction(merged.stackSplit, 0.5),
1014
1018
  sideSplit: clampFraction(merged.sideSplit, 0.5),
1015
- paneMinWidth: positive(merged.paneMinWidth, 0)
1019
+ paneMinWidth: positive(merged.paneMinWidth, 0),
1020
+ paneMinHeight: positive(merged.paneMinHeight, 0)
1016
1021
  };
1017
1022
  }
1018
1023
  function defaultCanvasWidth(rules, region) {
@@ -1031,6 +1036,30 @@ function clampDraggedCanvasWidth(next, region, rules) {
1031
1036
  function shouldStackSplit(width, children, rules) {
1032
1037
  return rules.paneMinWidth > 0 && width > 0 && width < rules.paneMinWidth * children;
1033
1038
  }
1039
+ function fitCanvasSplit(size, orientation, rules) {
1040
+ if (!size || size.width <= 0 || size.height <= 0) return orientation;
1041
+ const sideFits = rules.paneMinWidth <= 0 || size.width >= 2 * rules.paneMinWidth;
1042
+ const stackFits = rules.paneMinHeight <= 0 || size.height >= 2 * rules.paneMinHeight;
1043
+ if (orientation === "horizontal") return sideFits || stackFits ? "horizontal" : null;
1044
+ return stackFits ? "vertical" : sideFits ? "horizontal" : null;
1045
+ }
1046
+ function fitCanvasSplitSizes(sizes, total, mins) {
1047
+ if (!(total > 0) || sizes.length === 0) return [...sizes];
1048
+ const sum = sizes.reduce((a, b) => a + b, 0) || 1;
1049
+ const px = sizes.map((s) => s / sum * total);
1050
+ const floor = mins.map((m) => Number.isFinite(m) && m > 0 ? m : 0);
1051
+ const needed = floor.reduce((a, b) => a + b, 0);
1052
+ if (needed >= total) return needed > 0 ? floor.map((m) => m / needed) : px.map((p) => p / total);
1053
+ const short = px.reduce((acc, p, i) => acc + Math.max(0, (floor[i] ?? 0) - p), 0);
1054
+ if (short <= 0) return px.map((p) => p / total);
1055
+ const spare = px.map((p, i) => Math.max(0, p - (floor[i] ?? 0)));
1056
+ const spareSum = spare.reduce((a, b) => a + b, 0);
1057
+ return px.map((p, i) => {
1058
+ const min = floor[i] ?? 0;
1059
+ if (p <= min) return min / total;
1060
+ return (p - short * (spare[i] ?? 0) / spareSum) / total;
1061
+ });
1062
+ }
1034
1063
 
1035
1064
  // src/core/commands.ts
1036
1065
  var CANVAS_SHORTCUTS = [
@@ -1128,6 +1157,7 @@ function createCanvasController(options) {
1128
1157
  options,
1129
1158
  onError: options.onError ?? consoleCanvasErrorSink,
1130
1159
  rules: resolveCanvasLayoutRules(options.layoutRules),
1160
+ measures: /* @__PURE__ */ new Set(),
1131
1161
  presentations: /* @__PURE__ */ new Map(),
1132
1162
  presentationListeners: /* @__PURE__ */ new Set(),
1133
1163
  views: /* @__PURE__ */ new Map(),
@@ -1148,6 +1178,14 @@ function view(shared, windowId) {
1148
1178
  for (const listener of [...shared.presentationListeners]) listener();
1149
1179
  };
1150
1180
  const shareFor = (orientation) => orientation === "vertical" ? rules.stackSplit : rules.sideSplit;
1181
+ const measurePane = (paneId) => {
1182
+ for (const measure of shared.measures) {
1183
+ const size = measure(paneId);
1184
+ if (size) return size;
1185
+ }
1186
+ return null;
1187
+ };
1188
+ const fitSplit = (paneId, orientation) => fitCanvasSplit(measurePane(paneId), orientation, rules);
1151
1189
  const acceptable = (input) => {
1152
1190
  if (input.kind !== CANVAS_LAUNCHER_KIND && options.isKnownKind && !options.isKnownKind(input.kind)) {
1153
1191
  onError({ code: "unknown-kind", message: `No canvas kind "${input.kind}" is registered.`, detail: input });
@@ -1178,6 +1216,13 @@ function view(shared, windowId) {
1178
1216
  if (holding) return { paneId: holding };
1179
1217
  return state.panes[frame2.focusedPaneId]?.activeItemId ? preferred : "focused";
1180
1218
  };
1219
+ const splitTargetFor = (asked, forWindow) => {
1220
+ const state = store.getState();
1221
+ const frame2 = canvasWindowFrame(state, forWindow ? canvasWindowId(forWindow) : windowId);
1222
+ if (!frame2) return asked;
1223
+ const fitted = fitSplit(frame2.focusedPaneId, asked === "split-down" ? "vertical" : "horizontal");
1224
+ return fitted === null ? "focused" : fitted === "vertical" ? "split-down" : "split-right";
1225
+ };
1181
1226
  const controller = {
1182
1227
  store,
1183
1228
  windowId,
@@ -1186,7 +1231,8 @@ function view(shared, windowId) {
1186
1231
  getState: () => store.getState(),
1187
1232
  open(input) {
1188
1233
  if (!acceptable(input)) return null;
1189
- const target = input.target ?? preferredTargetFor(input);
1234
+ const asked = input.target ?? preferredTargetFor(input);
1235
+ const target = asked === "split-down" || asked === "split-right" ? splitTargetFor(asked, input.windowId) : asked;
1190
1236
  const splitShare = target === "split-down" ? rules.stackSplit : target === "split-right" ? rules.sideSplit : void 0;
1191
1237
  const withTarget = input.target === target ? input : { ...input, target };
1192
1238
  const resolved = withTarget.windowId || windowId === CANVAS_MAIN_WINDOW ? withTarget : { ...withTarget, windowId };
@@ -1251,7 +1297,15 @@ function view(shared, windowId) {
1251
1297
  activate: (itemId) => dispatch(canvasActions.activate(itemId)),
1252
1298
  focusPane: (paneId) => dispatch(canvasActions.focusPane(paneId)),
1253
1299
  moveItem: (itemId, toPaneId, index) => dispatch(canvasActions.moveItem(itemId, toPaneId, index)),
1254
- splitPane: (paneId, orientation, moveItemId) => dispatch(canvasActions.splitPane(paneId, orientation, moveItemId, shareFor(orientation))),
1300
+ splitPane(paneId, orientation, moveItemId) {
1301
+ const fitted = fitSplit(paneId, orientation);
1302
+ if (fitted) dispatch(canvasActions.splitPane(paneId, fitted, moveItemId, shareFor(fitted)));
1303
+ },
1304
+ fitSplit,
1305
+ registerPaneMeasure(measure) {
1306
+ shared.measures.add(measure);
1307
+ return () => void shared.measures.delete(measure);
1308
+ },
1255
1309
  closePane: (paneId) => dispatch(canvasActions.closePane(paneId)),
1256
1310
  resizeSplit: (splitId, sizes) => dispatch(canvasActions.resizeSplit(splitId, sizes)),
1257
1311
  show: (w = windowId) => dispatch(canvasActions.setOpen(true, w)),
@@ -2889,8 +2943,23 @@ var Rendered = memo(function Rendered2({
2889
2943
  presentation
2890
2944
  }) {
2891
2945
  const props = { item, data: item.data, paneId, isFocused, isVisible, canvas, windowId, presentation };
2892
- return /* @__PURE__ */ jsx6(ItemBoundary, { item, onClose: () => canvas.close(item.id), onRetry: () => resetKindComponent(kind), children: /* @__PURE__ */ jsx6(Suspense, { fallback: /* @__PURE__ */ jsx6("div", { className: "mxc-loading", "aria-busy": "true" }), children: /* @__PURE__ */ jsx6(KindRender, { kind, props }) }) });
2946
+ const [attempt, setAttempt] = useState4(0);
2947
+ const retry = () => {
2948
+ resetKindComponent(kind);
2949
+ setAttempt((n) => n + 1);
2950
+ };
2951
+ return /* @__PURE__ */ jsx6(ItemBoundary, { item, onClose: () => canvas.close(item.id), onRetry: retry, children: /* @__PURE__ */ jsx6(Suspense, { fallback: /* @__PURE__ */ jsx6(LoadingBody, { onRetry: retry, onClose: () => canvas.close(item.id) }), children: /* @__PURE__ */ jsx6(KindRender, { kind, props }) }, attempt) });
2893
2952
  });
2953
+ var CANVAS_BODY_STALL_MS = 2e4;
2954
+ function LoadingBody({ onRetry, onClose }) {
2955
+ const [stalled, setStalled] = useState4(false);
2956
+ useEffect6(() => {
2957
+ const timer = setTimeout(() => setStalled(true), CANVAS_BODY_STALL_MS);
2958
+ return () => clearTimeout(timer);
2959
+ }, []);
2960
+ if (!stalled) return /* @__PURE__ */ jsx6("div", { className: "mxc-loading", "aria-busy": "true" });
2961
+ return /* @__PURE__ */ jsx6(ItemProblem, { message: "This item is taking a long time to load.", onRetry, onClose });
2962
+ }
2894
2963
  function KindRender({ kind, props }) {
2895
2964
  const Render = kindComponent(kind);
2896
2965
  if (!Render) throw new Error(`[@ai-matrx/canvas] kind "${kind.id}" has nothing to render.`);
@@ -3169,9 +3238,36 @@ function PaneTitle({ item, kind }) {
3169
3238
  }
3170
3239
  var CANVAS_TAB_MIN_WIDTH = 120;
3171
3240
  var TAB_GAP = 2;
3172
- function canvasTabsOverflow(count, width) {
3173
- if (count <= 1 || width <= 0) return false;
3174
- return count * CANVAS_TAB_MIN_WIDTH + (count - 1) * TAB_GAP > width;
3241
+ var CANVAS_TAB_LIST_ROOM = 44;
3242
+ function fitCanvasTabWindow(units, current, width, previousStart = 0) {
3243
+ const n = units.length;
3244
+ const need = (i) => Math.max(1, units[i] ?? 1) * CANVAS_TAB_MIN_WIDTH;
3245
+ const all = units.reduce((sum, _, i) => sum + need(i), 0) + Math.max(0, n - 1) * TAB_GAP;
3246
+ if (n <= 1 || width <= 0 || all <= width) return { start: 0, end: n, squeezed: n === 1 && width > 0 && all > width };
3247
+ const room = width - CANVAS_TAB_LIST_ROOM;
3248
+ const at = Math.min(n - 1, Math.max(0, current));
3249
+ let start = Math.min(Math.max(0, previousStart), at);
3250
+ let used = 0;
3251
+ let end = start;
3252
+ while (end < n && used + need(end) + (end > start ? TAB_GAP : 0) <= room) {
3253
+ used += need(end) + (end > start ? TAB_GAP : 0);
3254
+ end += 1;
3255
+ }
3256
+ if (end <= at) {
3257
+ start = at;
3258
+ end = at + 1;
3259
+ used = need(at);
3260
+ while (start > 0 && used + TAB_GAP + need(start - 1) <= room) {
3261
+ start -= 1;
3262
+ used += TAB_GAP + need(start);
3263
+ }
3264
+ } else {
3265
+ while (start > 0 && used + TAB_GAP + need(start - 1) <= room) {
3266
+ start -= 1;
3267
+ used += TAB_GAP + need(start);
3268
+ }
3269
+ }
3270
+ return { start, end, squeezed: need(at) > room && end - start === 1 };
3175
3271
  }
3176
3272
  function planCanvasTabStrip(tabs, actives) {
3177
3273
  const joined = actives ? tabs.filter((tab) => actives.get(tab.paneId) === tab.itemId) : [];
@@ -3206,9 +3302,12 @@ function TabStrip({
3206
3302
  activeItemId,
3207
3303
  actives
3208
3304
  }) {
3305
+ const canvas = useCanvas();
3209
3306
  const strip = useRef7(null);
3210
3307
  const list = useRef7(null);
3211
3308
  const [width, setWidth] = useState5(0);
3309
+ const windowStart = useRef7(0);
3310
+ const focusAfterMove = useRef7(false);
3212
3311
  const read = useRef7(() => {
3213
3312
  });
3214
3313
  useIsoLayoutEffect4(() => {
@@ -3233,31 +3332,31 @@ function TabStrip({
3233
3332
  useIsoLayoutEffect4(() => {
3234
3333
  read.current();
3235
3334
  }, [tabs.length]);
3236
- useEffect7(() => {
3237
- const el = list.current;
3238
- if (!el) return void 0;
3239
- const onWheel = (event) => {
3240
- if (Math.abs(event.deltaY) <= Math.abs(event.deltaX) || el.scrollWidth <= el.clientWidth) return;
3241
- event.preventDefault();
3242
- el.scrollLeft += event.deltaY;
3243
- };
3244
- el.addEventListener("wheel", onWheel, { passive: false });
3245
- return () => el.removeEventListener("wheel", onWheel);
3246
- }, []);
3247
3335
  useIsoLayoutEffect4(() => {
3248
- const el = list.current;
3249
- if (!el || !activeItemId) return;
3250
- const tab = [...el.querySelectorAll("[data-mxc-tab]")].find((node) => node.dataset.mxcTab === activeItemId);
3251
- if (!tab) return;
3252
- const target = tab.closest(".mxc-tab-group") ?? tab;
3253
- const box = el.getBoundingClientRect();
3254
- const rect = target.getBoundingClientRect();
3255
- if (rect.left < box.left) el.scrollLeft -= box.left - rect.left;
3256
- else if (rect.right > box.right) el.scrollLeft += rect.right - box.right;
3257
- }, [activeItemId, width, tabs.length]);
3336
+ if (!focusAfterMove.current || !activeItemId) return;
3337
+ focusAfterMove.current = false;
3338
+ tabElement(list.current, activeItemId)?.focus();
3339
+ }, [activeItemId]);
3258
3340
  const entries = planCanvasTabStrip(tabs, actives);
3259
- const overflow = canvasTabsOverflow(tabs.length, width);
3341
+ const entryTabs = (entry) => entry.type === "tab" ? [entry.tab] : entry.tabs;
3342
+ const current = Math.max(
3343
+ 0,
3344
+ entries.findIndex((entry) => entryTabs(entry).some((tab) => tab.itemId === activeItemId))
3345
+ );
3346
+ const fit = fitCanvasTabWindow(
3347
+ entries.map((entry) => entryTabs(entry).length),
3348
+ current,
3349
+ width,
3350
+ windowStart.current
3351
+ );
3352
+ useIsoLayoutEffect4(() => {
3353
+ windowStart.current = fit.start;
3354
+ });
3355
+ const shown = entries.slice(fit.start, fit.end);
3356
+ const hidden = tabs.length - shown.reduce((sum, entry) => sum + entryTabs(entry).length, 0);
3357
+ const overflow = hidden > 0;
3260
3358
  const joined = entries.some((entry) => entry.type === "split");
3359
+ const order = entries.flatMap(entryTabs).map((tab) => tab.itemId);
3261
3360
  const isActive = (tab) => actives ? actives.get(tab.paneId) === tab.itemId : tab.itemId === activeItemId;
3262
3361
  const tabView = (tab) => /* @__PURE__ */ jsx7(
3263
3362
  ItemTab,
@@ -3270,6 +3369,22 @@ function TabStrip({
3270
3369
  },
3271
3370
  tab.itemId
3272
3371
  );
3372
+ const onKeyDown = (event) => {
3373
+ const keys = ["ArrowLeft", "ArrowRight", "Home", "End"];
3374
+ if (!keys.includes(event.key) || event.altKey || event.ctrlKey || event.metaKey || order.length < 2) return;
3375
+ const from = event.target.closest("[data-mxc-tab]")?.dataset.mxcTab;
3376
+ const at = from ? order.indexOf(from) : -1;
3377
+ if (at < 0) return;
3378
+ event.preventDefault();
3379
+ const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
3380
+ const step = event.key === "ArrowRight" !== rtl ? 1 : -1;
3381
+ const next = event.key === "Home" ? 0 : event.key === "End" ? order.length - 1 : (at + step + order.length) % order.length;
3382
+ const target = order[next];
3383
+ if (!target) return;
3384
+ focusAfterMove.current = true;
3385
+ canvas.activate(target);
3386
+ tabElement(list.current, target)?.focus();
3387
+ };
3273
3388
  return /* @__PURE__ */ jsxs6(
3274
3389
  "div",
3275
3390
  {
@@ -3286,8 +3401,9 @@ function TabStrip({
3286
3401
  role: "tablist",
3287
3402
  "aria-label": "Canvas tabs",
3288
3403
  "aria-multiselectable": joined ? true : void 0,
3289
- onKeyDown: moveTabFocus,
3290
- children: entries.map(
3404
+ "data-squeezed": fit.squeezed ? "" : void 0,
3405
+ onKeyDown,
3406
+ children: shown.map(
3291
3407
  (entry) => entry.type === "tab" ? tabView(entry.tab) : /* @__PURE__ */ jsx7(
3292
3408
  "div",
3293
3409
  {
@@ -3302,32 +3418,24 @@ function TabStrip({
3302
3418
  )
3303
3419
  }
3304
3420
  ),
3305
- overflow ? /* @__PURE__ */ jsx7(TabListMenu, { tabs, activeItemId }) : null
3421
+ overflow ? /* @__PURE__ */ jsx7(TabListMenu, { tabs, activeItemId, hidden }) : null
3306
3422
  ]
3307
3423
  }
3308
3424
  );
3309
3425
  }
3310
- function moveTabFocus(event) {
3311
- const keys = ["ArrowLeft", "ArrowRight", "Home", "End"];
3312
- if (!keys.includes(event.key) || event.altKey || event.ctrlKey || event.metaKey) return;
3313
- const tabs = [...event.currentTarget.querySelectorAll('[role="tab"]')];
3314
- const at = tabs.findIndex((tab) => tab === event.target || tab.contains(event.target));
3315
- if (at < 0 || tabs.length < 2) return;
3316
- event.preventDefault();
3317
- const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
3318
- const step = event.key === "ArrowRight" !== rtl ? 1 : -1;
3319
- const next = event.key === "Home" ? 0 : event.key === "End" ? tabs.length - 1 : (at + step + tabs.length) % tabs.length;
3320
- const target = tabs[next];
3321
- target?.focus();
3322
- target?.click();
3323
- }
3324
- function TabListMenu({ tabs, activeItemId }) {
3426
+ function tabElement(list, itemId) {
3427
+ return [...list?.querySelectorAll("[data-mxc-tab]") ?? []].find((node) => node.dataset.mxcTab === itemId);
3428
+ }
3429
+ function TabListMenu({ tabs, activeItemId, hidden }) {
3325
3430
  const canvas = useCanvas();
3326
3431
  const { ports, layer } = useCanvasInternals();
3327
3432
  const blocking = useRef7(null);
3328
3433
  useEffect7(() => () => blocking.current?.(), []);
3329
3434
  const label = `All tabs (${tabs.length})`;
3330
- const trigger = (props) => /* @__PURE__ */ jsx7(CanvasHeaderButton, { className: "mxc-tab-list", label, ...props, children: /* @__PURE__ */ jsx7(ChevronDownIcon, {}) });
3435
+ const trigger = (props) => /* @__PURE__ */ jsxs6(CanvasHeaderButton, { className: "mxc-tab-list", label, ...props, children: [
3436
+ /* @__PURE__ */ jsx7("span", { className: "mxc-tab-list-count", "aria-hidden": "true", children: hidden }),
3437
+ /* @__PURE__ */ jsx7(ChevronDownIcon, {})
3438
+ ] });
3331
3439
  const showMenu = ports.showMenu;
3332
3440
  if (showMenu) {
3333
3441
  return trigger({
@@ -3445,10 +3553,9 @@ function PaneMenu({
3445
3553
  return [
3446
3554
  [...kindItems, ...bodyItems, ...hostItems].map((entry) => ({ ...entry, id: `item:${entry.id}` })),
3447
3555
  [
3448
- ...canSplit ? [
3449
- { id: "split-right", label: "Split right", icon: /* @__PURE__ */ jsx7(SplitRightIcon, {}), onSelect: () => canvas.splitPane(paneId, "horizontal", moveId) },
3450
- { id: "split-down", label: "Split down", icon: /* @__PURE__ */ jsx7(SplitDownIcon, {}), onSelect: () => canvas.splitPane(paneId, "vertical", moveId) }
3451
- ] : [],
3556
+ // Only a split that leaves both panes usable (the layout rules' minimums) is offered.
3557
+ ...canSplit && canvas.fitSplit(paneId, "horizontal") === "horizontal" ? [{ id: "split-right", label: "Split right", icon: /* @__PURE__ */ jsx7(SplitRightIcon, {}), onSelect: () => canvas.splitPane(paneId, "horizontal", moveId) }] : [],
3558
+ ...canSplit && canvas.fitSplit(paneId, "vertical") === "vertical" ? [{ id: "split-down", label: "Split down", icon: /* @__PURE__ */ jsx7(SplitDownIcon, {}), onSelect: () => canvas.splitPane(paneId, "vertical", moveId) }] : [],
3452
3559
  ...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) }] : []
3453
3560
  ],
3454
3561
  activeItem ? [
@@ -3557,6 +3664,15 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
3557
3664
  const [slots] = useState6(createCanvasSlots);
3558
3665
  const ref = useRef8(null);
3559
3666
  useRegisterCanvasPresence();
3667
+ useEffect8(
3668
+ () => controller.registerPaneMeasure((paneId) => {
3669
+ const section = slots.section(paneId);
3670
+ if (!section?.isConnected) return null;
3671
+ const rect = section.getBoundingClientRect();
3672
+ return rect.width > 0 && rect.height > 0 ? { width: rect.width, height: rect.height } : null;
3673
+ }),
3674
+ [controller, slots]
3675
+ );
3560
3676
  useEffect8(() => {
3561
3677
  const el = ref.current;
3562
3678
  if (!el) return void 0;
@@ -3678,28 +3794,42 @@ function LayoutNodeView({ node }) {
3678
3794
  if (node.type === "pane") return /* @__PURE__ */ jsx8(CanvasPaneView, { paneId: node.paneId });
3679
3795
  return /* @__PURE__ */ jsx8(SplitView, { node });
3680
3796
  }
3681
- function useMeasuredWidth(ref, enabled) {
3682
- const [width, setWidth] = useState6(0);
3797
+ function useMeasuredSize(ref, enabled) {
3798
+ const [size, setSize] = useState6({ width: 0, height: 0 });
3683
3799
  useIsoLayoutEffect5(() => {
3684
3800
  const el = ref.current;
3685
3801
  if (!enabled || !el) return;
3686
- setWidth(el.getBoundingClientRect().width);
3802
+ const read = () => {
3803
+ const rect = el.getBoundingClientRect();
3804
+ setSize((prev) => prev.width === rect.width && prev.height === rect.height ? prev : { width: rect.width, height: rect.height });
3805
+ };
3806
+ read();
3687
3807
  if (typeof ResizeObserver === "undefined") return;
3688
- const observer = new ResizeObserver(() => setWidth(el.getBoundingClientRect().width));
3808
+ const observer = new ResizeObserver(read);
3689
3809
  observer.observe(el);
3690
3810
  return () => observer.disconnect();
3691
3811
  }, [enabled]);
3692
- return width;
3812
+ return size;
3813
+ }
3814
+ function panesAlong(node, orientation) {
3815
+ if (node.type === "pane") return 1;
3816
+ const counts = node.children.map((child) => panesAlong(child, orientation));
3817
+ return node.orientation === orientation ? counts.reduce((a, b) => a + b, 0) : Math.max(...counts);
3818
+ }
3819
+ function childMinimums(node, orientation, rules) {
3820
+ const unit = orientation === "vertical" ? rules.paneMinHeight : rules.paneMinWidth;
3821
+ return node.children.map((child) => unit * panesAlong(child, orientation));
3693
3822
  }
3694
3823
  function SplitView({ node }) {
3695
3824
  const canvas = useCanvas();
3696
3825
  const ref = useRef8(null);
3697
3826
  const [liveSizes, setLiveSizes] = useState6(null);
3698
- const sizes = liveSizes ?? node.sizes;
3699
- const measuredWidth = useMeasuredWidth(ref, node.orientation === "horizontal" && canvas.rules.paneMinWidth > 0);
3700
- const stacked = node.orientation === "horizontal" && shouldStackSplit(measuredWidth, node.children.length, canvas.rules);
3827
+ const measured = useMeasuredSize(ref, canvas.rules.paneMinWidth > 0 || canvas.rules.paneMinHeight > 0);
3828
+ const stacked = node.orientation === "horizontal" && shouldStackSplit(measured.width, node.children.length, canvas.rules);
3701
3829
  const orientation = stacked ? "vertical" : node.orientation;
3702
3830
  const horizontal = orientation === "horizontal";
3831
+ const mins = childMinimums(node, orientation, canvas.rules);
3832
+ const sizes = fitCanvasSplitSizes(liveSizes ?? node.sizes, horizontal ? measured.width : measured.height, mins);
3703
3833
  const beginDrag = (index) => (event) => {
3704
3834
  const el = ref.current;
3705
3835
  if (!el) return;
@@ -3708,14 +3838,15 @@ function SplitView({ node }) {
3708
3838
  const rect = el.getBoundingClientRect();
3709
3839
  const total = horizontal ? rect.width : rect.height;
3710
3840
  const origin = horizontal ? event.clientX : event.clientY;
3711
- const base = [...node.sizes];
3841
+ const base = [...sizes];
3712
3842
  const pair = (base[index] ?? 0) + (base[index + 1] ?? 0);
3843
+ const minFirst = Math.max(CANVAS_MIN_SPLIT_FRACTION, total > 0 ? (mins[index] ?? 0) / total : 0);
3844
+ const minSecond = Math.max(CANVAS_MIN_SPLIT_FRACTION, total > 0 ? (mins[index + 1] ?? 0) / total : 0);
3713
3845
  const target = event.currentTarget;
3714
3846
  const end = beginCanvasResize(horizontal ? "col" : "row");
3715
3847
  const compute = (pos) => {
3716
3848
  const delta = (pos - origin) / total;
3717
- const min = CANVAS_MIN_SPLIT_FRACTION;
3718
- const first = Math.min(pair - min, Math.max(min, (base[index] ?? 0) + delta));
3849
+ const first = Math.max(Math.min(pair - minSecond, Math.max(minFirst, (base[index] ?? 0) + delta)), Math.min(minFirst, pair / 2));
3719
3850
  const next = [...base];
3720
3851
  next[index] = first;
3721
3852
  next[index + 1] = pair - first;
@@ -3830,6 +3961,7 @@ function CanvasFrame({ children, className }) {
3830
3961
  export {
3831
3962
  BrowserBar,
3832
3963
  BrowserTabs,
3964
+ CANVAS_TAB_LIST_ROOM,
3833
3965
  CANVAS_TAB_MIN_WIDTH,
3834
3966
  CanvasColumn,
3835
3967
  CanvasFrame,
@@ -3846,8 +3978,8 @@ export {
3846
3978
  CanvasWindowTabs,
3847
3979
  beginCanvasResize,
3848
3980
  canvasSessionKey,
3849
- canvasTabsOverflow,
3850
3981
  defineCanvasKind,
3982
+ fitCanvasTabWindow,
3851
3983
  getCanvasKind,
3852
3984
  itemTitle,
3853
3985
  listCanvasKinds,