@ai-matrx/canvas 0.8.5 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0
4
+
5
+ Live walk of 0.8.5 (localhost, 2026-10-07): the scrolling strip showed the tabs beside the current one sliced at its edges ("uick Chat", "aved items", "atchpad"), and at 375px the second tab was just "("; Quick Notes / Notifications / Quick Chat stacked in an 800px window left Notifications its header and one row.
6
+
7
+ - **A tab is drawn whole or not at all** (JetBrains' single-row tabs; a scrolling strip — VS Code, Firefox — always leaves half-tabs at its edges). Tabs still shrink evenly from 12rem to the 120px floor; past it the strip draws the run of tabs that fits whole beside "All tabs" and holds the current tab, moving as little as possible when you switch (`fitCanvasTabWindow`). "All tabs (N)" shows how many tabs are out of the strip. The tab list no longer scrolls (no wheel-scroll); Left/Right/Home/End walk every tab, drawn or not. A strip narrower than one tab (a phone header) draws the current tab alone, its name ending in "…".
8
+ - **Panes keep a usable height.** New rule `paneMinHeight` (`CANVAS_PANE_MIN_HEIGHT` 200px, web and desktop). A split draws no child shorter than its minimum (`fitCanvasSplitSizes`; stored shares unchanged, divider drags stop at it). A new split — `open` with `split-down`/`split-right` (a kind's `preferredTarget` included) or `splitPane` — that cannot give both panes their minimum goes the other axis when that fits, else the new tab joins the focused pane (`fitCanvasSplit`, measured by the column through the new `controller.registerPaneMeasure`; `controller.fitSplit(paneId, orientation)` answers it). The pane menu offers only the splits that fit.
9
+ - **Removed:** `canvasTabsOverflow` (no consumer). **Added:** `fitCanvasTabWindow`, `CANVAS_TAB_LIST_ROOM`, `CanvasTabWindow`, `fitCanvasSplit`, `fitCanvasSplitSizes`, `CANVAS_PANE_MIN_HEIGHT`, `CanvasPaneMeasure`.
10
+
11
+ ### Consumer action
12
+
13
+ - None for hosts that render `<CanvasColumn/>`. CSS that targeted `.mxc-tabs` scrolling: gone. A host passing a full `CanvasLayoutRules` object may add `paneMinHeight` (missing → 200 via `resolveCanvasLayoutRules`; 0 turns it off).
14
+
3
15
  ## 0.8.5
4
16
 
5
17
  Automatic changed-only republish (docs/metadata drift since the last tag — see
package/README.md CHANGED
@@ -67,9 +67,9 @@ Navigating to something already open elsewhere focuses it there (a launcher tab
67
67
 
68
68
  **Forward.** After Back the header also shows Forward (`selectCanvasCanGoForward`); it returns the same live body Back left. A navigate somewhere new ends the forward trail. Shortcut data `canvas.back` / `canvas.forward` (⌘[ / ⌘]) is for a desktop host; browsers own those keys.
69
69
 
70
- **The strip** (Chrome's model). Tabs share the strip evenly — 12rem each with room — and shrink together to one readable floor, icon + ~10 letters + "…" (`CANVAS_TAB_MIN_WIDTH`, 120px); a cut name always ends in an ellipsis and every tab's tooltip is its full name. Past the floor the tab list scrolls sideways (wheel, trackpad; the current tab is kept in view) and "All tabs (N)" lists every tab (`canvasTabsOverflow` is the rule). A tab never shrinks to an icon sliver and nothing in the header overlaps another control.
70
+ **The strip** (Chrome's model). Tabs share the strip evenly — 12rem each with room — and shrink together to one readable floor, icon + ~10 letters + "…" (`CANVAS_TAB_MIN_WIDTH`, 120px); a cut name always ends in an ellipsis and every tab's tooltip is its full name. Past the floor the strip draws only the tabs that fit WHOLE, a run around the current tab that moves as little as possible (`fitCanvasTabWindow`), and "All tabs (N)" lists every tab and shows how many are out of the strip — a tab is never sliced at an edge (JetBrains' single-row tabs; a scrolling strip, as in VS Code or Firefox, leaves half-tabs at both edges). The arrow keys walk every tab. A tab never shrinks to an icon sliver and nothing in the header overlaps another control.
71
71
 
72
- **Splits.** A split canvas keeps ONE window-wide tab strip above its panes (`CanvasWindowTabs`), never one per pane: every pane's tabs are in it, the panes' visible tabs are drawn as ONE joined tab (the focused pane's half strongest; `planCanvasTabStrip`), and its "+" opens in the focused pane. Each pane header keeps its own Back/Forward, title, kind controls and menu. A side-by-side split whose panes would be narrower than `paneMinWidth` (web: `CANVAS_PANE_MIN_WIDTH`, 280px) renders stacked under the same one strip; the tree is unchanged.
72
+ **Splits.** A split canvas keeps ONE window-wide tab strip above its panes (`CanvasWindowTabs`), never one per pane: every pane's tabs are in it, the panes' visible tabs are drawn as ONE joined tab (the focused pane's half strongest; `planCanvasTabStrip`), and its "+" opens in the focused pane. Each pane header keeps its own Back/Forward, title, kind controls and menu. A side-by-side split whose panes would be narrower than `paneMinWidth` (web: `CANVAS_PANE_MIN_WIDTH`, 280px) renders stacked under the same one strip; the tree is unchanged. Every pane keeps `paneMinHeight` (`CANVAS_PANE_MIN_HEIGHT`, 200px): a split draws no child shorter (`fitCanvasSplitSizes`), and a new split that cannot give both panes their minimum goes the other axis, or the new tab joins the focused pane (`fitCanvasSplit`; the pane menu offers only splits that fit).
73
73
 
74
74
  **Phones** (`CANVAS_PHONE_MAX_WIDTH`, 767px): the column presents ONE pane — the focused one when it holds a tab, else the first that does (`selectCanvasPresentedPaneId`) — whose strip carries every pane's tabs; choosing a tab from another pane presents that pane. The pane menu offers no Split. The layout tree is untouched, so a wider screen gets its split back.
75
75
 
@@ -150,9 +150,10 @@ toggleKind(controller, { kind: "terminal" }); // absent → open · behind → f
150
150
  | `defaultWidth` | 640 | `{ fraction: 0.36 }` |
151
151
  | `minWidth` | 360 | 360 |
152
152
  | `centreMinWidth` (the canvas shrinks first) | 600 | 520 |
153
- | `stackSplit` (split down: first pane's share) | 0.5 | 0.7 |
153
+ | `stackSplit` (split down: first pane's share) | 0.7 | 0.7 |
154
154
  | `sideSplit` | 0.5 | 0.5 |
155
- | `paneMinWidth` (narrower side-by-side panes stack) | 0 (never) | 360 |
155
+ | `paneMinWidth` (narrower side-by-side panes stack) | 280 | 360 |
156
+ | `paneMinHeight` (no pane shorter; a split that cannot fit goes the other way or opens as a tab) | 200 | 200 |
156
157
 
157
158
  Expand is one flag per window; it fills whatever the host placed the column in (`useCanvasColumnWidth()` returns `null`).
158
159
 
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ __export(src_exports, {
30
30
  CANVAS_MIN_SPLIT_FRACTION: () => CANVAS_MIN_SPLIT_FRACTION,
31
31
  CANVAS_MIN_WIDTH: () => CANVAS_MIN_WIDTH,
32
32
  CANVAS_NARROW_WIDTH: () => CANVAS_NARROW_WIDTH,
33
+ CANVAS_PANE_MIN_HEIGHT: () => CANVAS_PANE_MIN_HEIGHT,
33
34
  CANVAS_PANE_MIN_WIDTH: () => CANVAS_PANE_MIN_WIDTH,
34
35
  CANVAS_PHONE_MAX_WIDTH: () => CANVAS_PHONE_MAX_WIDTH,
35
36
  CANVAS_SHORTCUTS: () => CANVAS_SHORTCUTS,
@@ -56,6 +57,8 @@ __export(src_exports, {
56
57
  createRemoteCanvasStore: () => createRemoteCanvasStore,
57
58
  defaultCanvasWidth: () => defaultCanvasWidth,
58
59
  findNonJson: () => findNonJson,
60
+ fitCanvasSplit: () => fitCanvasSplit,
61
+ fitCanvasSplitSizes: () => fitCanvasSplitSizes,
59
62
  fitCanvasWidth: () => fitCanvasWidth,
60
63
  foldCanvasWindows: () => foldCanvasWindows,
61
64
  isCanvasAction: () => isCanvasAction,
@@ -1116,6 +1119,7 @@ function toPersistableSnapshot(state, isRestorable) {
1116
1119
 
1117
1120
  // src/core/rules.ts
1118
1121
  var CANVAS_PANE_MIN_WIDTH = 280;
1122
+ var CANVAS_PANE_MIN_HEIGHT = 200;
1119
1123
  var CANVAS_WEB_LAYOUT_RULES = {
1120
1124
  defaultWidth: CANVAS_DEFAULT_WIDTH,
1121
1125
  minWidth: CANVAS_MIN_WIDTH,
@@ -1129,7 +1133,10 @@ var CANVAS_WEB_LAYOUT_RULES = {
1129
1133
  sideSplit: 0.5,
1130
1134
  // A pane narrower than this cannot show a header (Back, a readable tab, "+", the menu, close) and a
1131
1135
  // body side by side with another: a split right that would go under it stacks instead.
1132
- paneMinWidth: CANVAS_PANE_MIN_WIDTH
1136
+ paneMinWidth: CANVAS_PANE_MIN_WIDTH,
1137
+ // 2026-10-07 live: Quick Notes / Notifications / Quick Chat stacked in an 800px window left
1138
+ // Notifications its header and one row.
1139
+ paneMinHeight: CANVAS_PANE_MIN_HEIGHT
1133
1140
  };
1134
1141
  var CANVAS_DESKTOP_LAYOUT_RULES = {
1135
1142
  defaultWidth: { fraction: 0.36 },
@@ -1137,7 +1144,8 @@ var CANVAS_DESKTOP_LAYOUT_RULES = {
1137
1144
  centreMinWidth: 520,
1138
1145
  stackSplit: 0.7,
1139
1146
  sideSplit: 0.5,
1140
- paneMinWidth: 360
1147
+ paneMinWidth: 360,
1148
+ paneMinHeight: CANVAS_PANE_MIN_HEIGHT
1141
1149
  };
1142
1150
  var clampFraction = (value, fallback) => Number.isFinite(value) ? Math.min(0.88, Math.max(0.12, value)) : fallback;
1143
1151
  function resolveCanvasLayoutRules(rules) {
@@ -1149,7 +1157,8 @@ function resolveCanvasLayoutRules(rules) {
1149
1157
  centreMinWidth: positive(merged.centreMinWidth, CANVAS_WEB_LAYOUT_RULES.centreMinWidth),
1150
1158
  stackSplit: clampFraction(merged.stackSplit, 0.5),
1151
1159
  sideSplit: clampFraction(merged.sideSplit, 0.5),
1152
- paneMinWidth: positive(merged.paneMinWidth, 0)
1160
+ paneMinWidth: positive(merged.paneMinWidth, 0),
1161
+ paneMinHeight: positive(merged.paneMinHeight, 0)
1153
1162
  };
1154
1163
  }
1155
1164
  function defaultCanvasWidth(rules, region) {
@@ -1168,6 +1177,30 @@ function clampDraggedCanvasWidth(next, region, rules) {
1168
1177
  function shouldStackSplit(width, children, rules) {
1169
1178
  return rules.paneMinWidth > 0 && width > 0 && width < rules.paneMinWidth * children;
1170
1179
  }
1180
+ function fitCanvasSplit(size, orientation, rules) {
1181
+ if (!size || size.width <= 0 || size.height <= 0) return orientation;
1182
+ const sideFits = rules.paneMinWidth <= 0 || size.width >= 2 * rules.paneMinWidth;
1183
+ const stackFits = rules.paneMinHeight <= 0 || size.height >= 2 * rules.paneMinHeight;
1184
+ if (orientation === "horizontal") return sideFits || stackFits ? "horizontal" : null;
1185
+ return stackFits ? "vertical" : sideFits ? "horizontal" : null;
1186
+ }
1187
+ function fitCanvasSplitSizes(sizes, total, mins) {
1188
+ if (!(total > 0) || sizes.length === 0) return [...sizes];
1189
+ const sum = sizes.reduce((a, b) => a + b, 0) || 1;
1190
+ const px = sizes.map((s) => s / sum * total);
1191
+ const floor = mins.map((m) => Number.isFinite(m) && m > 0 ? m : 0);
1192
+ const needed = floor.reduce((a, b) => a + b, 0);
1193
+ if (needed >= total) return needed > 0 ? floor.map((m) => m / needed) : px.map((p) => p / total);
1194
+ const short = px.reduce((acc, p, i) => acc + Math.max(0, (floor[i] ?? 0) - p), 0);
1195
+ if (short <= 0) return px.map((p) => p / total);
1196
+ const spare = px.map((p, i) => Math.max(0, p - (floor[i] ?? 0)));
1197
+ const spareSum = spare.reduce((a, b) => a + b, 0);
1198
+ return px.map((p, i) => {
1199
+ const min = floor[i] ?? 0;
1200
+ if (p <= min) return min / total;
1201
+ return (p - short * (spare[i] ?? 0) / spareSum) / total;
1202
+ });
1203
+ }
1171
1204
 
1172
1205
  // src/core/commands.ts
1173
1206
  var CANVAS_SHORTCUTS = [
@@ -1265,6 +1298,7 @@ function createCanvasController(options) {
1265
1298
  options,
1266
1299
  onError: options.onError ?? consoleCanvasErrorSink,
1267
1300
  rules: resolveCanvasLayoutRules(options.layoutRules),
1301
+ measures: /* @__PURE__ */ new Set(),
1268
1302
  presentations: /* @__PURE__ */ new Map(),
1269
1303
  presentationListeners: /* @__PURE__ */ new Set(),
1270
1304
  views: /* @__PURE__ */ new Map(),
@@ -1285,6 +1319,14 @@ function view(shared, windowId) {
1285
1319
  for (const listener of [...shared.presentationListeners]) listener();
1286
1320
  };
1287
1321
  const shareFor = (orientation) => orientation === "vertical" ? rules.stackSplit : rules.sideSplit;
1322
+ const measurePane = (paneId) => {
1323
+ for (const measure of shared.measures) {
1324
+ const size = measure(paneId);
1325
+ if (size) return size;
1326
+ }
1327
+ return null;
1328
+ };
1329
+ const fitSplit = (paneId, orientation) => fitCanvasSplit(measurePane(paneId), orientation, rules);
1288
1330
  const acceptable = (input) => {
1289
1331
  if (input.kind !== CANVAS_LAUNCHER_KIND && options.isKnownKind && !options.isKnownKind(input.kind)) {
1290
1332
  onError({ code: "unknown-kind", message: `No canvas kind "${input.kind}" is registered.`, detail: input });
@@ -1315,6 +1357,13 @@ function view(shared, windowId) {
1315
1357
  if (holding) return { paneId: holding };
1316
1358
  return state.panes[frame2.focusedPaneId]?.activeItemId ? preferred : "focused";
1317
1359
  };
1360
+ const splitTargetFor = (asked, forWindow) => {
1361
+ const state = store.getState();
1362
+ const frame2 = canvasWindowFrame(state, forWindow ? canvasWindowId(forWindow) : windowId);
1363
+ if (!frame2) return asked;
1364
+ const fitted = fitSplit(frame2.focusedPaneId, asked === "split-down" ? "vertical" : "horizontal");
1365
+ return fitted === null ? "focused" : fitted === "vertical" ? "split-down" : "split-right";
1366
+ };
1318
1367
  const controller = {
1319
1368
  store,
1320
1369
  windowId,
@@ -1323,7 +1372,8 @@ function view(shared, windowId) {
1323
1372
  getState: () => store.getState(),
1324
1373
  open(input) {
1325
1374
  if (!acceptable(input)) return null;
1326
- const target = input.target ?? preferredTargetFor(input);
1375
+ const asked = input.target ?? preferredTargetFor(input);
1376
+ const target = asked === "split-down" || asked === "split-right" ? splitTargetFor(asked, input.windowId) : asked;
1327
1377
  const splitShare = target === "split-down" ? rules.stackSplit : target === "split-right" ? rules.sideSplit : void 0;
1328
1378
  const withTarget = input.target === target ? input : { ...input, target };
1329
1379
  const resolved = withTarget.windowId || windowId === CANVAS_MAIN_WINDOW ? withTarget : { ...withTarget, windowId };
@@ -1388,7 +1438,15 @@ function view(shared, windowId) {
1388
1438
  activate: (itemId) => dispatch(canvasActions.activate(itemId)),
1389
1439
  focusPane: (paneId) => dispatch(canvasActions.focusPane(paneId)),
1390
1440
  moveItem: (itemId, toPaneId, index) => dispatch(canvasActions.moveItem(itemId, toPaneId, index)),
1391
- splitPane: (paneId, orientation, moveItemId) => dispatch(canvasActions.splitPane(paneId, orientation, moveItemId, shareFor(orientation))),
1441
+ splitPane(paneId, orientation, moveItemId) {
1442
+ const fitted = fitSplit(paneId, orientation);
1443
+ if (fitted) dispatch(canvasActions.splitPane(paneId, fitted, moveItemId, shareFor(fitted)));
1444
+ },
1445
+ fitSplit,
1446
+ registerPaneMeasure(measure) {
1447
+ shared.measures.add(measure);
1448
+ return () => void shared.measures.delete(measure);
1449
+ },
1392
1450
  closePane: (paneId) => dispatch(canvasActions.closePane(paneId)),
1393
1451
  resizeSplit: (splitId, sizes) => dispatch(canvasActions.resizeSplit(splitId, sizes)),
1394
1452
  show: (w = windowId) => dispatch(canvasActions.setOpen(true, w)),