@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/CHANGELOG.md +22 -0
- package/README.md +5 -4
- package/dist/index.cjs +63 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +63 -5
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +197 -65
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +23 -7
- package/dist/react.d.ts +23 -7
- package/dist/react.js +197 -65
- package/dist/react.js.map +1 -1
- package/dist/styles.css +15 -10
- package/dist/{toggleKind-BoFnqhzq.d.cts → toggleKind-CI2nBjum.d.cts} +47 -2
- package/dist/{toggleKind-BoFnqhzq.d.ts → toggleKind-CI2nBjum.d.ts} +47 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.1
|
|
4
|
+
|
|
5
|
+
Live walk of 0.9.0 (localhost, 2026-10-07): a tab spun forever when its code never arrived (the dev server was paused mid-load), and a failed one showed "This item failed to load".
|
|
6
|
+
|
|
7
|
+
- **A body whose code does not arrive is never a silent spinner.** A failed load already ended in "This item failed to load" with Try again; a load that never answers now ends the same way after `CANVAS_BODY_STALL_MS` (20s): "This item is taking a long time to load." with Try again and Close tab. Try again (both paths) asks for the code from a fresh lazy component. The tab's data (a conversation, a draft's alias) lives outside the body and is untouched.
|
|
8
|
+
|
|
9
|
+
### Consumer action
|
|
10
|
+
|
|
11
|
+
- None.
|
|
12
|
+
|
|
13
|
+
## 0.9.0
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
|
|
17
|
+
- **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 "…".
|
|
18
|
+
- **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.
|
|
19
|
+
- **Removed:** `canvasTabsOverflow` (no consumer). **Added:** `fitCanvasTabWindow`, `CANVAS_TAB_LIST_ROOM`, `CanvasTabWindow`, `fitCanvasSplit`, `fitCanvasSplitSizes`, `CANVAS_PANE_MIN_HEIGHT`, `CanvasPaneMeasure`.
|
|
20
|
+
|
|
21
|
+
### Consumer action
|
|
22
|
+
|
|
23
|
+
- 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).
|
|
24
|
+
|
|
3
25
|
## 0.8.5
|
|
4
26
|
|
|
5
27
|
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
|
|
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.
|
|
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) |
|
|
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
|
|
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
|
|
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)),
|