@ai-matrx/canvas 0.8.4 → 0.9.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.cjs CHANGED
@@ -34,6 +34,7 @@ var react_exports = {};
34
34
  __export(react_exports, {
35
35
  BrowserBar: () => BrowserBar,
36
36
  BrowserTabs: () => BrowserTabs,
37
+ CANVAS_TAB_LIST_ROOM: () => CANVAS_TAB_LIST_ROOM,
37
38
  CANVAS_TAB_MIN_WIDTH: () => CANVAS_TAB_MIN_WIDTH,
38
39
  CanvasColumn: () => CanvasColumn,
39
40
  CanvasFrame: () => CanvasFrame,
@@ -50,8 +51,8 @@ __export(react_exports, {
50
51
  CanvasWindowTabs: () => CanvasWindowTabs,
51
52
  beginCanvasResize: () => beginCanvasResize,
52
53
  canvasSessionKey: () => canvasSessionKey,
53
- canvasTabsOverflow: () => canvasTabsOverflow,
54
54
  defineCanvasKind: () => defineCanvasKind,
55
+ fitCanvasTabWindow: () => fitCanvasTabWindow,
55
56
  getCanvasKind: () => getCanvasKind,
56
57
  itemTitle: () => itemTitle,
57
58
  listCanvasKinds: () => listCanvasKinds,
@@ -1055,6 +1056,7 @@ function toPersistableSnapshot(state, isRestorable) {
1055
1056
 
1056
1057
  // src/core/rules.ts
1057
1058
  var CANVAS_PANE_MIN_WIDTH = 280;
1059
+ var CANVAS_PANE_MIN_HEIGHT = 200;
1058
1060
  var CANVAS_WEB_LAYOUT_RULES = {
1059
1061
  defaultWidth: CANVAS_DEFAULT_WIDTH,
1060
1062
  minWidth: CANVAS_MIN_WIDTH,
@@ -1068,7 +1070,10 @@ var CANVAS_WEB_LAYOUT_RULES = {
1068
1070
  sideSplit: 0.5,
1069
1071
  // A pane narrower than this cannot show a header (Back, a readable tab, "+", the menu, close) and a
1070
1072
  // body side by side with another: a split right that would go under it stacks instead.
1071
- paneMinWidth: CANVAS_PANE_MIN_WIDTH
1073
+ paneMinWidth: CANVAS_PANE_MIN_WIDTH,
1074
+ // 2026-10-07 live: Quick Notes / Notifications / Quick Chat stacked in an 800px window left
1075
+ // Notifications its header and one row.
1076
+ paneMinHeight: CANVAS_PANE_MIN_HEIGHT
1072
1077
  };
1073
1078
  var clampFraction = (value, fallback) => Number.isFinite(value) ? Math.min(0.88, Math.max(0.12, value)) : fallback;
1074
1079
  function resolveCanvasLayoutRules(rules) {
@@ -1080,7 +1085,8 @@ function resolveCanvasLayoutRules(rules) {
1080
1085
  centreMinWidth: positive(merged.centreMinWidth, CANVAS_WEB_LAYOUT_RULES.centreMinWidth),
1081
1086
  stackSplit: clampFraction(merged.stackSplit, 0.5),
1082
1087
  sideSplit: clampFraction(merged.sideSplit, 0.5),
1083
- paneMinWidth: positive(merged.paneMinWidth, 0)
1088
+ paneMinWidth: positive(merged.paneMinWidth, 0),
1089
+ paneMinHeight: positive(merged.paneMinHeight, 0)
1084
1090
  };
1085
1091
  }
1086
1092
  function defaultCanvasWidth(rules, region) {
@@ -1099,6 +1105,30 @@ function clampDraggedCanvasWidth(next, region, rules) {
1099
1105
  function shouldStackSplit(width, children, rules) {
1100
1106
  return rules.paneMinWidth > 0 && width > 0 && width < rules.paneMinWidth * children;
1101
1107
  }
1108
+ function fitCanvasSplit(size, orientation, rules) {
1109
+ if (!size || size.width <= 0 || size.height <= 0) return orientation;
1110
+ const sideFits = rules.paneMinWidth <= 0 || size.width >= 2 * rules.paneMinWidth;
1111
+ const stackFits = rules.paneMinHeight <= 0 || size.height >= 2 * rules.paneMinHeight;
1112
+ if (orientation === "horizontal") return sideFits || stackFits ? "horizontal" : null;
1113
+ return stackFits ? "vertical" : sideFits ? "horizontal" : null;
1114
+ }
1115
+ function fitCanvasSplitSizes(sizes, total, mins) {
1116
+ if (!(total > 0) || sizes.length === 0) return [...sizes];
1117
+ const sum = sizes.reduce((a, b) => a + b, 0) || 1;
1118
+ const px = sizes.map((s) => s / sum * total);
1119
+ const floor = mins.map((m) => Number.isFinite(m) && m > 0 ? m : 0);
1120
+ const needed = floor.reduce((a, b) => a + b, 0);
1121
+ if (needed >= total) return needed > 0 ? floor.map((m) => m / needed) : px.map((p) => p / total);
1122
+ const short = px.reduce((acc, p, i) => acc + Math.max(0, (floor[i] ?? 0) - p), 0);
1123
+ if (short <= 0) return px.map((p) => p / total);
1124
+ const spare = px.map((p, i) => Math.max(0, p - (floor[i] ?? 0)));
1125
+ const spareSum = spare.reduce((a, b) => a + b, 0);
1126
+ return px.map((p, i) => {
1127
+ const min = floor[i] ?? 0;
1128
+ if (p <= min) return min / total;
1129
+ return (p - short * (spare[i] ?? 0) / spareSum) / total;
1130
+ });
1131
+ }
1102
1132
 
1103
1133
  // src/core/commands.ts
1104
1134
  var CANVAS_SHORTCUTS = [
@@ -1196,6 +1226,7 @@ function createCanvasController(options) {
1196
1226
  options,
1197
1227
  onError: options.onError ?? consoleCanvasErrorSink,
1198
1228
  rules: resolveCanvasLayoutRules(options.layoutRules),
1229
+ measures: /* @__PURE__ */ new Set(),
1199
1230
  presentations: /* @__PURE__ */ new Map(),
1200
1231
  presentationListeners: /* @__PURE__ */ new Set(),
1201
1232
  views: /* @__PURE__ */ new Map(),
@@ -1216,6 +1247,14 @@ function view(shared, windowId) {
1216
1247
  for (const listener of [...shared.presentationListeners]) listener();
1217
1248
  };
1218
1249
  const shareFor = (orientation) => orientation === "vertical" ? rules.stackSplit : rules.sideSplit;
1250
+ const measurePane = (paneId) => {
1251
+ for (const measure of shared.measures) {
1252
+ const size = measure(paneId);
1253
+ if (size) return size;
1254
+ }
1255
+ return null;
1256
+ };
1257
+ const fitSplit = (paneId, orientation) => fitCanvasSplit(measurePane(paneId), orientation, rules);
1219
1258
  const acceptable = (input) => {
1220
1259
  if (input.kind !== CANVAS_LAUNCHER_KIND && options.isKnownKind && !options.isKnownKind(input.kind)) {
1221
1260
  onError({ code: "unknown-kind", message: `No canvas kind "${input.kind}" is registered.`, detail: input });
@@ -1246,6 +1285,13 @@ function view(shared, windowId) {
1246
1285
  if (holding) return { paneId: holding };
1247
1286
  return state.panes[frame2.focusedPaneId]?.activeItemId ? preferred : "focused";
1248
1287
  };
1288
+ const splitTargetFor = (asked, forWindow) => {
1289
+ const state = store.getState();
1290
+ const frame2 = canvasWindowFrame(state, forWindow ? canvasWindowId(forWindow) : windowId);
1291
+ if (!frame2) return asked;
1292
+ const fitted = fitSplit(frame2.focusedPaneId, asked === "split-down" ? "vertical" : "horizontal");
1293
+ return fitted === null ? "focused" : fitted === "vertical" ? "split-down" : "split-right";
1294
+ };
1249
1295
  const controller = {
1250
1296
  store,
1251
1297
  windowId,
@@ -1254,7 +1300,8 @@ function view(shared, windowId) {
1254
1300
  getState: () => store.getState(),
1255
1301
  open(input) {
1256
1302
  if (!acceptable(input)) return null;
1257
- const target = input.target ?? preferredTargetFor(input);
1303
+ const asked = input.target ?? preferredTargetFor(input);
1304
+ const target = asked === "split-down" || asked === "split-right" ? splitTargetFor(asked, input.windowId) : asked;
1258
1305
  const splitShare = target === "split-down" ? rules.stackSplit : target === "split-right" ? rules.sideSplit : void 0;
1259
1306
  const withTarget = input.target === target ? input : { ...input, target };
1260
1307
  const resolved = withTarget.windowId || windowId === CANVAS_MAIN_WINDOW ? withTarget : { ...withTarget, windowId };
@@ -1319,7 +1366,15 @@ function view(shared, windowId) {
1319
1366
  activate: (itemId) => dispatch(canvasActions.activate(itemId)),
1320
1367
  focusPane: (paneId) => dispatch(canvasActions.focusPane(paneId)),
1321
1368
  moveItem: (itemId, toPaneId, index) => dispatch(canvasActions.moveItem(itemId, toPaneId, index)),
1322
- splitPane: (paneId, orientation, moveItemId) => dispatch(canvasActions.splitPane(paneId, orientation, moveItemId, shareFor(orientation))),
1369
+ splitPane(paneId, orientation, moveItemId) {
1370
+ const fitted = fitSplit(paneId, orientation);
1371
+ if (fitted) dispatch(canvasActions.splitPane(paneId, fitted, moveItemId, shareFor(fitted)));
1372
+ },
1373
+ fitSplit,
1374
+ registerPaneMeasure(measure) {
1375
+ shared.measures.add(measure);
1376
+ return () => void shared.measures.delete(measure);
1377
+ },
1323
1378
  closePane: (paneId) => dispatch(canvasActions.closePane(paneId)),
1324
1379
  resizeSplit: (splitId, sizes) => dispatch(canvasActions.resizeSplit(splitId, sizes)),
1325
1380
  show: (w = windowId) => dispatch(canvasActions.setOpen(true, w)),
@@ -2378,7 +2433,7 @@ function BrowserBar({ url, canGoBack, canGoForward, onBack, onForward, onReload,
2378
2433
  "data-slot": "browser-address",
2379
2434
  className: "mxc-address",
2380
2435
  value: draft,
2381
- placeholder: "Search or enter an address",
2436
+ placeholder: "Search or enter website",
2382
2437
  spellCheck: false,
2383
2438
  onFocus: (event) => {
2384
2439
  setEditing(true);
@@ -3215,9 +3270,36 @@ function PaneTitle({ item, kind }) {
3215
3270
  }
3216
3271
  var CANVAS_TAB_MIN_WIDTH = 120;
3217
3272
  var TAB_GAP = 2;
3218
- function canvasTabsOverflow(count, width) {
3219
- if (count <= 1 || width <= 0) return false;
3220
- return count * CANVAS_TAB_MIN_WIDTH + (count - 1) * TAB_GAP > width;
3273
+ var CANVAS_TAB_LIST_ROOM = 44;
3274
+ function fitCanvasTabWindow(units, current, width, previousStart = 0) {
3275
+ const n = units.length;
3276
+ const need = (i) => Math.max(1, units[i] ?? 1) * CANVAS_TAB_MIN_WIDTH;
3277
+ const all = units.reduce((sum, _, i) => sum + need(i), 0) + Math.max(0, n - 1) * TAB_GAP;
3278
+ if (n <= 1 || width <= 0 || all <= width) return { start: 0, end: n, squeezed: n === 1 && width > 0 && all > width };
3279
+ const room = width - CANVAS_TAB_LIST_ROOM;
3280
+ const at = Math.min(n - 1, Math.max(0, current));
3281
+ let start = Math.min(Math.max(0, previousStart), at);
3282
+ let used = 0;
3283
+ let end = start;
3284
+ while (end < n && used + need(end) + (end > start ? TAB_GAP : 0) <= room) {
3285
+ used += need(end) + (end > start ? TAB_GAP : 0);
3286
+ end += 1;
3287
+ }
3288
+ if (end <= at) {
3289
+ start = at;
3290
+ end = at + 1;
3291
+ used = need(at);
3292
+ while (start > 0 && used + TAB_GAP + need(start - 1) <= room) {
3293
+ start -= 1;
3294
+ used += TAB_GAP + need(start);
3295
+ }
3296
+ } else {
3297
+ while (start > 0 && used + TAB_GAP + need(start - 1) <= room) {
3298
+ start -= 1;
3299
+ used += TAB_GAP + need(start);
3300
+ }
3301
+ }
3302
+ return { start, end, squeezed: need(at) > room && end - start === 1 };
3221
3303
  }
3222
3304
  function planCanvasTabStrip(tabs, actives) {
3223
3305
  const joined = actives ? tabs.filter((tab) => actives.get(tab.paneId) === tab.itemId) : [];
@@ -3252,9 +3334,12 @@ function TabStrip({
3252
3334
  activeItemId,
3253
3335
  actives
3254
3336
  }) {
3337
+ const canvas = useCanvas();
3255
3338
  const strip = (0, import_react9.useRef)(null);
3256
3339
  const list = (0, import_react9.useRef)(null);
3257
3340
  const [width, setWidth] = (0, import_react9.useState)(0);
3341
+ const windowStart = (0, import_react9.useRef)(0);
3342
+ const focusAfterMove = (0, import_react9.useRef)(false);
3258
3343
  const read = (0, import_react9.useRef)(() => {
3259
3344
  });
3260
3345
  useIsoLayoutEffect4(() => {
@@ -3279,31 +3364,31 @@ function TabStrip({
3279
3364
  useIsoLayoutEffect4(() => {
3280
3365
  read.current();
3281
3366
  }, [tabs.length]);
3282
- (0, import_react9.useEffect)(() => {
3283
- const el = list.current;
3284
- if (!el) return void 0;
3285
- const onWheel = (event) => {
3286
- if (Math.abs(event.deltaY) <= Math.abs(event.deltaX) || el.scrollWidth <= el.clientWidth) return;
3287
- event.preventDefault();
3288
- el.scrollLeft += event.deltaY;
3289
- };
3290
- el.addEventListener("wheel", onWheel, { passive: false });
3291
- return () => el.removeEventListener("wheel", onWheel);
3292
- }, []);
3293
3367
  useIsoLayoutEffect4(() => {
3294
- const el = list.current;
3295
- if (!el || !activeItemId) return;
3296
- const tab = [...el.querySelectorAll("[data-mxc-tab]")].find((node) => node.dataset.mxcTab === activeItemId);
3297
- if (!tab) return;
3298
- const target = tab.closest(".mxc-tab-group") ?? tab;
3299
- const box = el.getBoundingClientRect();
3300
- const rect = target.getBoundingClientRect();
3301
- if (rect.left < box.left) el.scrollLeft -= box.left - rect.left;
3302
- else if (rect.right > box.right) el.scrollLeft += rect.right - box.right;
3303
- }, [activeItemId, width, tabs.length]);
3368
+ if (!focusAfterMove.current || !activeItemId) return;
3369
+ focusAfterMove.current = false;
3370
+ tabElement(list.current, activeItemId)?.focus();
3371
+ }, [activeItemId]);
3304
3372
  const entries = planCanvasTabStrip(tabs, actives);
3305
- const overflow = canvasTabsOverflow(tabs.length, width);
3373
+ const entryTabs = (entry) => entry.type === "tab" ? [entry.tab] : entry.tabs;
3374
+ const current = Math.max(
3375
+ 0,
3376
+ entries.findIndex((entry) => entryTabs(entry).some((tab) => tab.itemId === activeItemId))
3377
+ );
3378
+ const fit = fitCanvasTabWindow(
3379
+ entries.map((entry) => entryTabs(entry).length),
3380
+ current,
3381
+ width,
3382
+ windowStart.current
3383
+ );
3384
+ useIsoLayoutEffect4(() => {
3385
+ windowStart.current = fit.start;
3386
+ });
3387
+ const shown = entries.slice(fit.start, fit.end);
3388
+ const hidden = tabs.length - shown.reduce((sum, entry) => sum + entryTabs(entry).length, 0);
3389
+ const overflow = hidden > 0;
3306
3390
  const joined = entries.some((entry) => entry.type === "split");
3391
+ const order = entries.flatMap(entryTabs).map((tab) => tab.itemId);
3307
3392
  const isActive = (tab) => actives ? actives.get(tab.paneId) === tab.itemId : tab.itemId === activeItemId;
3308
3393
  const tabView = (tab) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3309
3394
  ItemTab,
@@ -3316,6 +3401,22 @@ function TabStrip({
3316
3401
  },
3317
3402
  tab.itemId
3318
3403
  );
3404
+ const onKeyDown = (event) => {
3405
+ const keys = ["ArrowLeft", "ArrowRight", "Home", "End"];
3406
+ if (!keys.includes(event.key) || event.altKey || event.ctrlKey || event.metaKey || order.length < 2) return;
3407
+ const from = event.target.closest("[data-mxc-tab]")?.dataset.mxcTab;
3408
+ const at = from ? order.indexOf(from) : -1;
3409
+ if (at < 0) return;
3410
+ event.preventDefault();
3411
+ const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
3412
+ const step = event.key === "ArrowRight" !== rtl ? 1 : -1;
3413
+ const next = event.key === "Home" ? 0 : event.key === "End" ? order.length - 1 : (at + step + order.length) % order.length;
3414
+ const target = order[next];
3415
+ if (!target) return;
3416
+ focusAfterMove.current = true;
3417
+ canvas.activate(target);
3418
+ tabElement(list.current, target)?.focus();
3419
+ };
3319
3420
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3320
3421
  "div",
3321
3422
  {
@@ -3332,8 +3433,9 @@ function TabStrip({
3332
3433
  role: "tablist",
3333
3434
  "aria-label": "Canvas tabs",
3334
3435
  "aria-multiselectable": joined ? true : void 0,
3335
- onKeyDown: moveTabFocus,
3336
- children: entries.map(
3436
+ "data-squeezed": fit.squeezed ? "" : void 0,
3437
+ onKeyDown,
3438
+ children: shown.map(
3337
3439
  (entry) => entry.type === "tab" ? tabView(entry.tab) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3338
3440
  "div",
3339
3441
  {
@@ -3348,32 +3450,24 @@ function TabStrip({
3348
3450
  )
3349
3451
  }
3350
3452
  ),
3351
- overflow ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TabListMenu, { tabs, activeItemId }) : null
3453
+ overflow ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TabListMenu, { tabs, activeItemId, hidden }) : null
3352
3454
  ]
3353
3455
  }
3354
3456
  );
3355
3457
  }
3356
- function moveTabFocus(event) {
3357
- const keys = ["ArrowLeft", "ArrowRight", "Home", "End"];
3358
- if (!keys.includes(event.key) || event.altKey || event.ctrlKey || event.metaKey) return;
3359
- const tabs = [...event.currentTarget.querySelectorAll('[role="tab"]')];
3360
- const at = tabs.findIndex((tab) => tab === event.target || tab.contains(event.target));
3361
- if (at < 0 || tabs.length < 2) return;
3362
- event.preventDefault();
3363
- const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
3364
- const step = event.key === "ArrowRight" !== rtl ? 1 : -1;
3365
- const next = event.key === "Home" ? 0 : event.key === "End" ? tabs.length - 1 : (at + step + tabs.length) % tabs.length;
3366
- const target = tabs[next];
3367
- target?.focus();
3368
- target?.click();
3369
- }
3370
- function TabListMenu({ tabs, activeItemId }) {
3458
+ function tabElement(list, itemId) {
3459
+ return [...list?.querySelectorAll("[data-mxc-tab]") ?? []].find((node) => node.dataset.mxcTab === itemId);
3460
+ }
3461
+ function TabListMenu({ tabs, activeItemId, hidden }) {
3371
3462
  const canvas = useCanvas();
3372
3463
  const { ports, layer } = useCanvasInternals();
3373
3464
  const blocking = (0, import_react9.useRef)(null);
3374
3465
  (0, import_react9.useEffect)(() => () => blocking.current?.(), []);
3375
3466
  const label = `All tabs (${tabs.length})`;
3376
- const trigger = (props) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CanvasHeaderButton, { className: "mxc-tab-list", label, ...props, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChevronDownIcon, {}) });
3467
+ const trigger = (props) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(CanvasHeaderButton, { className: "mxc-tab-list", label, ...props, children: [
3468
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "mxc-tab-list-count", "aria-hidden": "true", children: hidden }),
3469
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChevronDownIcon, {})
3470
+ ] });
3377
3471
  const showMenu = ports.showMenu;
3378
3472
  if (showMenu) {
3379
3473
  return trigger({
@@ -3491,10 +3585,9 @@ function PaneMenu({
3491
3585
  return [
3492
3586
  [...kindItems, ...bodyItems, ...hostItems].map((entry) => ({ ...entry, id: `item:${entry.id}` })),
3493
3587
  [
3494
- ...canSplit ? [
3495
- { id: "split-right", label: "Split right", icon: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SplitRightIcon, {}), onSelect: () => canvas.splitPane(paneId, "horizontal", moveId) },
3496
- { id: "split-down", label: "Split down", icon: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SplitDownIcon, {}), onSelect: () => canvas.splitPane(paneId, "vertical", moveId) }
3497
- ] : [],
3588
+ // Only a split that leaves both panes usable (the layout rules' minimums) is offered.
3589
+ ...canSplit && canvas.fitSplit(paneId, "horizontal") === "horizontal" ? [{ id: "split-right", label: "Split right", icon: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SplitRightIcon, {}), onSelect: () => canvas.splitPane(paneId, "horizontal", moveId) }] : [],
3590
+ ...canSplit && canvas.fitSplit(paneId, "vertical") === "vertical" ? [{ id: "split-down", label: "Split down", icon: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SplitDownIcon, {}), onSelect: () => canvas.splitPane(paneId, "vertical", moveId) }] : [],
3498
3591
  ...activeItem && role === "pop-out" ? [{ id: "dock-back", label: "Dock back", icon: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(DockIcon, {}), onSelect: () => void canvas.dockBack(activeItem.id) }] : activeItem && ports.popOut ? [{ id: "pop-out", label: "Pop out", icon: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PopOutIcon, {}), onSelect: () => void canvas.popOut(activeItem.id) }] : []
3499
3592
  ],
3500
3593
  activeItem ? [
@@ -3603,6 +3696,15 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
3603
3696
  const [slots] = (0, import_react10.useState)(createCanvasSlots);
3604
3697
  const ref = (0, import_react10.useRef)(null);
3605
3698
  useRegisterCanvasPresence();
3699
+ (0, import_react10.useEffect)(
3700
+ () => controller.registerPaneMeasure((paneId) => {
3701
+ const section = slots.section(paneId);
3702
+ if (!section?.isConnected) return null;
3703
+ const rect = section.getBoundingClientRect();
3704
+ return rect.width > 0 && rect.height > 0 ? { width: rect.width, height: rect.height } : null;
3705
+ }),
3706
+ [controller, slots]
3707
+ );
3606
3708
  (0, import_react10.useEffect)(() => {
3607
3709
  const el = ref.current;
3608
3710
  if (!el) return void 0;
@@ -3724,28 +3826,42 @@ function LayoutNodeView({ node }) {
3724
3826
  if (node.type === "pane") return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CanvasPaneView, { paneId: node.paneId });
3725
3827
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SplitView, { node });
3726
3828
  }
3727
- function useMeasuredWidth(ref, enabled) {
3728
- const [width, setWidth] = (0, import_react10.useState)(0);
3829
+ function useMeasuredSize(ref, enabled) {
3830
+ const [size, setSize] = (0, import_react10.useState)({ width: 0, height: 0 });
3729
3831
  useIsoLayoutEffect5(() => {
3730
3832
  const el = ref.current;
3731
3833
  if (!enabled || !el) return;
3732
- setWidth(el.getBoundingClientRect().width);
3834
+ const read = () => {
3835
+ const rect = el.getBoundingClientRect();
3836
+ setSize((prev) => prev.width === rect.width && prev.height === rect.height ? prev : { width: rect.width, height: rect.height });
3837
+ };
3838
+ read();
3733
3839
  if (typeof ResizeObserver === "undefined") return;
3734
- const observer = new ResizeObserver(() => setWidth(el.getBoundingClientRect().width));
3840
+ const observer = new ResizeObserver(read);
3735
3841
  observer.observe(el);
3736
3842
  return () => observer.disconnect();
3737
3843
  }, [enabled]);
3738
- return width;
3844
+ return size;
3845
+ }
3846
+ function panesAlong(node, orientation) {
3847
+ if (node.type === "pane") return 1;
3848
+ const counts = node.children.map((child) => panesAlong(child, orientation));
3849
+ return node.orientation === orientation ? counts.reduce((a, b) => a + b, 0) : Math.max(...counts);
3850
+ }
3851
+ function childMinimums(node, orientation, rules) {
3852
+ const unit = orientation === "vertical" ? rules.paneMinHeight : rules.paneMinWidth;
3853
+ return node.children.map((child) => unit * panesAlong(child, orientation));
3739
3854
  }
3740
3855
  function SplitView({ node }) {
3741
3856
  const canvas = useCanvas();
3742
3857
  const ref = (0, import_react10.useRef)(null);
3743
3858
  const [liveSizes, setLiveSizes] = (0, import_react10.useState)(null);
3744
- const sizes = liveSizes ?? node.sizes;
3745
- const measuredWidth = useMeasuredWidth(ref, node.orientation === "horizontal" && canvas.rules.paneMinWidth > 0);
3746
- const stacked = node.orientation === "horizontal" && shouldStackSplit(measuredWidth, node.children.length, canvas.rules);
3859
+ const measured = useMeasuredSize(ref, canvas.rules.paneMinWidth > 0 || canvas.rules.paneMinHeight > 0);
3860
+ const stacked = node.orientation === "horizontal" && shouldStackSplit(measured.width, node.children.length, canvas.rules);
3747
3861
  const orientation = stacked ? "vertical" : node.orientation;
3748
3862
  const horizontal = orientation === "horizontal";
3863
+ const mins = childMinimums(node, orientation, canvas.rules);
3864
+ const sizes = fitCanvasSplitSizes(liveSizes ?? node.sizes, horizontal ? measured.width : measured.height, mins);
3749
3865
  const beginDrag = (index) => (event) => {
3750
3866
  const el = ref.current;
3751
3867
  if (!el) return;
@@ -3754,14 +3870,15 @@ function SplitView({ node }) {
3754
3870
  const rect = el.getBoundingClientRect();
3755
3871
  const total = horizontal ? rect.width : rect.height;
3756
3872
  const origin = horizontal ? event.clientX : event.clientY;
3757
- const base = [...node.sizes];
3873
+ const base = [...sizes];
3758
3874
  const pair = (base[index] ?? 0) + (base[index + 1] ?? 0);
3875
+ const minFirst = Math.max(CANVAS_MIN_SPLIT_FRACTION, total > 0 ? (mins[index] ?? 0) / total : 0);
3876
+ const minSecond = Math.max(CANVAS_MIN_SPLIT_FRACTION, total > 0 ? (mins[index + 1] ?? 0) / total : 0);
3759
3877
  const target = event.currentTarget;
3760
3878
  const end = beginCanvasResize(horizontal ? "col" : "row");
3761
3879
  const compute = (pos) => {
3762
3880
  const delta = (pos - origin) / total;
3763
- const min = CANVAS_MIN_SPLIT_FRACTION;
3764
- const first = Math.min(pair - min, Math.max(min, (base[index] ?? 0) + delta));
3881
+ const first = Math.max(Math.min(pair - minSecond, Math.max(minFirst, (base[index] ?? 0) + delta)), Math.min(minFirst, pair / 2));
3765
3882
  const next = [...base];
3766
3883
  next[index] = first;
3767
3884
  next[index + 1] = pair - first;