@ai-matrx/canvas 0.7.2 → 0.8.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,7 +34,6 @@ var react_exports = {};
34
34
  __export(react_exports, {
35
35
  BrowserBar: () => BrowserBar,
36
36
  BrowserTabs: () => BrowserTabs,
37
- CANVAS_ACTIVE_TAB_MIN_WIDTH: () => CANVAS_ACTIVE_TAB_MIN_WIDTH,
38
37
  CANVAS_TAB_MIN_WIDTH: () => CANVAS_TAB_MIN_WIDTH,
39
38
  CanvasColumn: () => CanvasColumn,
40
39
  CanvasFrame: () => CanvasFrame,
@@ -48,13 +47,15 @@ __export(react_exports, {
48
47
  CanvasTab: () => CanvasTab,
49
48
  CanvasToggle: () => CanvasToggle,
50
49
  CanvasWindowScope: () => CanvasWindowScope,
50
+ CanvasWindowTabs: () => CanvasWindowTabs,
51
51
  beginCanvasResize: () => beginCanvasResize,
52
52
  canvasSessionKey: () => canvasSessionKey,
53
+ canvasTabsOverflow: () => canvasTabsOverflow,
53
54
  defineCanvasKind: () => defineCanvasKind,
54
- fitCanvasTabs: () => fitCanvasTabs,
55
55
  getCanvasKind: () => getCanvasKind,
56
56
  itemTitle: () => itemTitle,
57
57
  listCanvasKinds: () => listCanvasKinds,
58
+ planCanvasTabStrip: () => planCanvasTabStrip,
58
59
  registerCanvasKind: () => registerCanvasKind,
59
60
  registerCanvasKinds: () => registerCanvasKinds,
60
61
  useCanvas: () => useCanvas,
@@ -1057,7 +1058,11 @@ var CANVAS_PANE_MIN_WIDTH = 280;
1057
1058
  var CANVAS_WEB_LAYOUT_RULES = {
1058
1059
  defaultWidth: CANVAS_DEFAULT_WIDTH,
1059
1060
  minWidth: CANVAS_MIN_WIDTH,
1060
- centreMinWidth: 420,
1061
+ // The page beside the canvas keeps room for its route header (title, mode switch,
1062
+ // the shell's own controls): a 2026-10-03 census of 40 routes found headers 29-67px
1063
+ // wide at 420 with a 900px canvas. Hosts pass `regionWidth` so this is the PAGE's
1064
+ // width, not the window's.
1065
+ centreMinWidth: 600,
1061
1066
  // Split down 70/30 like the desktop: what opens below (notifications, a feed) is the smaller pane.
1062
1067
  stackSplit: 0.7,
1063
1068
  sideSplit: 0.5,
@@ -2632,6 +2637,10 @@ var CanvasSinglePaneContext = (0, import_react7.createContext)(false);
2632
2637
  function useCanvasSinglePane() {
2633
2638
  return (0, import_react7.useContext)(CanvasSinglePaneContext);
2634
2639
  }
2640
+ var CanvasWindowTabsContext = (0, import_react7.createContext)(false);
2641
+ function useCanvasWindowTabs() {
2642
+ return (0, import_react7.useContext)(CanvasWindowTabsContext);
2643
+ }
2635
2644
  function useSlotSize(slots, paneId) {
2636
2645
  return (0, import_react7.useSyncExternalStore)(
2637
2646
  slots.subscribe,
@@ -3044,6 +3053,20 @@ function windowTabsSignature(state, windowId) {
3044
3053
  }
3045
3054
  return rows.join("");
3046
3055
  }
3056
+ function paneActivesSignature(state, windowId) {
3057
+ const frame2 = canvasWindowFrame(state, windowId);
3058
+ if (!frame2) return "";
3059
+ return listPaneIds(frame2.layout).map((paneId) => `${paneId}${state.panes[paneId]?.activeItemId ?? ""}`).join("");
3060
+ }
3061
+ function parsePaneActives(signature) {
3062
+ const actives = /* @__PURE__ */ new Map();
3063
+ if (!signature) return actives;
3064
+ for (const row of signature.split("")) {
3065
+ const [paneId = "", itemId = ""] = row.split("");
3066
+ if (itemId) actives.set(paneId, itemId);
3067
+ }
3068
+ return actives;
3069
+ }
3047
3070
  function parseWindowTabs(signature) {
3048
3071
  if (!signature) return [];
3049
3072
  const perPane = /* @__PURE__ */ new Map();
@@ -3074,6 +3097,7 @@ function CanvasPaneView({ paneId }) {
3074
3097
  const canGoForward = useCanvasState((s) => active ? selectCanvasCanGoForward(s, active.id) : false);
3075
3098
  const kind = useItemKind(active?.kind ?? "");
3076
3099
  const singlePane = useCanvasSinglePane();
3100
+ const inWindowStrip = useCanvasWindowTabs() && !singlePane;
3077
3101
  const windowTabs = useCanvasState((s) => singlePane ? windowTabsSignature(s, windowId) : "");
3078
3102
  const sectionRef = (0, import_react9.useRef)(null);
3079
3103
  const bodyRef = (0, import_react9.useRef)(null);
@@ -3118,8 +3142,8 @@ function CanvasPaneView({ paneId }) {
3118
3142
  children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ArrowRightIcon, {})
3119
3143
  }
3120
3144
  ) : null,
3121
- single && active ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SingleTitle, { item: active, kind }) : tabs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TabStrip, { tabs, activeItemId: pane.activeItemId }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "mxc-panel-title", children: "New tab" }),
3122
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CanvasHeaderButton, { className: "mxc-new-tab", label: "New tab", onClick: () => void canvas.newTab(paneId), children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PlusIcon, {}) })
3145
+ inWindowStrip ? active ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PaneTitle, { item: active, kind }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "mxc-panel-title", children: "New tab" }) : single && active ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SingleTitle, { item: active, kind }) : tabs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TabStrip, { tabs, activeItemId: pane.activeItemId }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "mxc-panel-title", children: "New tab" }),
3146
+ inWindowStrip ? null : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CanvasHeaderButton, { className: "mxc-new-tab", label: "New tab", onClick: () => void canvas.newTab(paneId), children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PlusIcon, {}) })
3123
3147
  ] }),
3124
3148
  leading: Leading && kindProps ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Leading, { ...kindProps }) : null,
3125
3149
  actions: Action && kindProps ? (
@@ -3180,24 +3204,56 @@ function SingleTitle({ item, kind }) {
3180
3204
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "mxc-tab-close", "data-slot": "browser-tab-close", "aria-label": `Close ${title}`, onClick: () => canvas.close(item.id), children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CloseIcon, {}) })
3181
3205
  ] });
3182
3206
  }
3183
- var CANVAS_TAB_MIN_WIDTH = 88;
3184
- var CANVAS_ACTIVE_TAB_MIN_WIDTH = 112;
3185
- var TAB_LIST_BUTTON_ROOM = 36;
3207
+ function PaneTitle({ item, kind }) {
3208
+ const title = itemTitle(item, kind);
3209
+ const drag = useTabDrag(item.id, title);
3210
+ const Icon = kind?.icon ?? AlertIcon;
3211
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "mxc-panel-title mxc-pane-title", "data-draggable": "", title, ...drag, children: [
3212
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Icon, { className: "mxc-title-icon" }),
3213
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "mxc-tab-title", children: title })
3214
+ ] });
3215
+ }
3216
+ var CANVAS_TAB_MIN_WIDTH = 120;
3186
3217
  var TAB_GAP = 2;
3187
- function fitCanvasTabs(count, activeIndex, width) {
3188
- if (count <= 0) return { start: 0, end: 0, overflow: false, compact: false };
3189
- const need = (n) => CANVAS_ACTIVE_TAB_MIN_WIDTH + (n - 1) * (CANVAS_TAB_MIN_WIDTH + TAB_GAP);
3190
- if (width <= 0 || need(count) <= width) return { start: 0, end: count, overflow: false, compact: false };
3191
- if (count === 1) return { start: 0, end: 1, overflow: false, compact: true };
3192
- let fit = 1;
3193
- while (fit + 1 < count && need(fit + 1) + TAB_LIST_BUTTON_ROOM <= width) fit += 1;
3194
- const active = Math.min(Math.max(activeIndex, 0), count - 1);
3195
- const start = active < fit ? 0 : active - fit + 1;
3196
- return { start, end: start + fit, overflow: true, compact: need(fit) + TAB_LIST_BUTTON_ROOM > width };
3197
- }
3198
- function TabStrip({ tabs, activeItemId }) {
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;
3221
+ }
3222
+ function planCanvasTabStrip(tabs, actives) {
3223
+ const joined = actives ? tabs.filter((tab) => actives.get(tab.paneId) === tab.itemId) : [];
3224
+ if (joined.length < 2) return tabs.map((tab) => ({ type: "tab", tab }));
3225
+ const entries = [];
3226
+ let placed = false;
3227
+ for (const tab of tabs) {
3228
+ if (!joined.includes(tab)) entries.push({ type: "tab", tab });
3229
+ else if (!placed) {
3230
+ entries.push({ type: "split", tabs: joined });
3231
+ placed = true;
3232
+ }
3233
+ }
3234
+ return entries;
3235
+ }
3236
+ function CanvasWindowTabs() {
3199
3237
  const canvas = useCanvas();
3238
+ const { windowId } = useCanvasInternals();
3239
+ const signature = useCanvasState((s) => windowTabsSignature(s, windowId));
3240
+ const activesSignature = useCanvasState((s) => paneActivesSignature(s, windowId));
3241
+ const focusedPaneId = useCanvasState((s) => selectCanvasFocusedPaneId(s, windowId));
3242
+ const focusedActive = useCanvasState((s) => focusedPaneId ? s.panes[focusedPaneId]?.activeItemId ?? null : null);
3243
+ const tabs = parseWindowTabs(signature);
3244
+ const actives = parsePaneActives(activesSignature);
3245
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "mxc-window-tabs", "data-slot": "canvas-window-tabs", children: [
3246
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TabStrip, { tabs, activeItemId: focusedActive, actives }),
3247
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CanvasHeaderButton, { className: "mxc-new-tab", label: "New tab", onClick: () => void canvas.newTab(focusedPaneId ?? void 0), children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PlusIcon, {}) })
3248
+ ] });
3249
+ }
3250
+ function TabStrip({
3251
+ tabs,
3252
+ activeItemId,
3253
+ actives
3254
+ }) {
3200
3255
  const strip = (0, import_react9.useRef)(null);
3256
+ const list = (0, import_react9.useRef)(null);
3201
3257
  const [width, setWidth] = (0, import_react9.useState)(0);
3202
3258
  const read = (0, import_react9.useRef)(() => {
3203
3259
  });
@@ -3223,48 +3279,75 @@ function TabStrip({ tabs, activeItemId }) {
3223
3279
  useIsoLayoutEffect4(() => {
3224
3280
  read.current();
3225
3281
  }, [tabs.length]);
3226
- const activeIndex = Math.max(0, tabs.findIndex((tab) => tab.itemId === activeItemId));
3227
- const fit = fitCanvasTabs(tabs.length, activeIndex, width);
3228
- const shown = tabs.slice(fit.start, fit.end);
3229
- const hidden = tabs.length - shown.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
+ 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 box = el.getBoundingClientRect();
3299
+ const rect = tab.getBoundingClientRect();
3300
+ if (rect.left < box.left) el.scrollLeft -= box.left - rect.left;
3301
+ else if (rect.right > box.right) el.scrollLeft += rect.right - box.right;
3302
+ }, [activeItemId, width, tabs.length]);
3303
+ const entries = planCanvasTabStrip(tabs, actives);
3304
+ const overflow = canvasTabsOverflow(tabs.length, width);
3305
+ const joined = entries.some((entry) => entry.type === "split");
3306
+ const isActive = (tab) => actives ? actives.get(tab.paneId) === tab.itemId : tab.itemId === activeItemId;
3307
+ const tabView = (tab) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3308
+ ItemTab,
3309
+ {
3310
+ itemId: tab.itemId,
3311
+ paneId: tab.paneId,
3312
+ index: tab.index,
3313
+ active: isActive(tab),
3314
+ current: joined && tab.itemId === activeItemId
3315
+ },
3316
+ tab.itemId
3317
+ );
3230
3318
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3231
3319
  "div",
3232
3320
  {
3233
3321
  ref: strip,
3234
3322
  className: "mxc-tabstrip",
3235
- "data-overflow": fit.overflow ? "" : void 0,
3236
- "data-compact": fit.compact ? "" : void 0,
3237
- style: { ["--mxc-tab-min"]: `${CANVAS_TAB_MIN_WIDTH}px`, ["--mxc-tab-active-min"]: `${CANVAS_ACTIVE_TAB_MIN_WIDTH}px` },
3323
+ "data-overflow": overflow ? "" : void 0,
3324
+ style: { ["--mxc-tab-min"]: `${CANVAS_TAB_MIN_WIDTH}px` },
3238
3325
  children: [
3239
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3240
- "div",
3241
- {
3242
- className: "mxc-tabs",
3243
- role: "tablist",
3244
- "aria-label": "Canvas tabs",
3245
- onWheel: (event) => {
3246
- if (!fit.overflow || Math.abs(event.deltaY) < Math.abs(event.deltaX) || tabs.length < 2) return;
3247
- const next = tabs[(activeIndex + (event.deltaY > 0 ? 1 : -1) + tabs.length) % tabs.length];
3248
- if (next) canvas.activate(next.itemId);
3326
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { ref: list, className: "mxc-tabs", role: "tablist", "aria-label": "Canvas tabs", "aria-multiselectable": joined ? true : void 0, children: entries.map(
3327
+ (entry) => entry.type === "tab" ? tabView(entry.tab) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3328
+ "div",
3329
+ {
3330
+ className: "mxc-tab-group",
3331
+ "data-split": "",
3332
+ role: "presentation",
3333
+ style: { ["--mxc-group-n"]: entry.tabs.length },
3334
+ children: entry.tabs.map(tabView)
3249
3335
  },
3250
- children: shown.map((tab) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ItemTab, { itemId: tab.itemId, paneId: tab.paneId, index: tab.index, active: tab.itemId === activeItemId }, tab.itemId))
3251
- }
3252
- ),
3253
- fit.overflow ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TabListMenu, { tabs, activeItemId, hidden }) : null
3336
+ `split:${entry.tabs.map((tab) => tab.itemId).join("|")}`
3337
+ )
3338
+ ) }),
3339
+ overflow ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TabListMenu, { tabs, activeItemId }) : null
3254
3340
  ]
3255
3341
  }
3256
3342
  );
3257
3343
  }
3258
- function TabListMenu({ tabs, activeItemId, hidden }) {
3344
+ function TabListMenu({ tabs, activeItemId }) {
3259
3345
  const canvas = useCanvas();
3260
3346
  const { ports, layer } = useCanvasInternals();
3261
3347
  const blocking = (0, import_react9.useRef)(null);
3262
3348
  (0, import_react9.useEffect)(() => () => blocking.current?.(), []);
3263
3349
  const label = `All tabs (${tabs.length})`;
3264
- const trigger = (props) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(CanvasHeaderButton, { className: "mxc-tab-list", label, "data-hidden-count": hidden, ...props, children: [
3265
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChevronDownIcon, {}),
3266
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "mxc-tab-list-count", "aria-hidden": "true", children: hidden })
3267
- ] });
3350
+ const trigger = (props) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CanvasHeaderButton, { className: "mxc-tab-list", label, ...props, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChevronDownIcon, {}) });
3268
3351
  const showMenu = ports.showMenu;
3269
3352
  if (showMenu) {
3270
3353
  return trigger({
@@ -3320,7 +3403,13 @@ function TabListEntry({ itemId, active }) {
3320
3403
  }
3321
3404
  );
3322
3405
  }
3323
- function ItemTab({ itemId, paneId, index, active }) {
3406
+ function ItemTab({
3407
+ itemId,
3408
+ paneId,
3409
+ index,
3410
+ active,
3411
+ current = false
3412
+ }) {
3324
3413
  const canvas = useCanvas();
3325
3414
  const item = useCanvasState((s) => s.items[itemId]);
3326
3415
  const kind = useItemKind(item?.kind ?? "");
@@ -3334,6 +3423,8 @@ function ItemTab({ itemId, paneId, index, active }) {
3334
3423
  title,
3335
3424
  icon: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Icon, {}),
3336
3425
  active,
3426
+ "data-mxc-tab": itemId,
3427
+ "data-current": current ? "" : void 0,
3337
3428
  onSelect: () => canvas.activate(itemId),
3338
3429
  onClose: () => canvas.close(itemId),
3339
3430
  ...drag,
@@ -3519,6 +3610,7 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
3519
3610
  };
3520
3611
  }, [layer]);
3521
3612
  if (!layout) return null;
3613
+ const windowTabs = !singlePane && layout.type === "split";
3522
3614
  const fitted = fitCanvasWidth(width, region, controller.rules);
3523
3615
  const shown = dragWidth ?? fitted;
3524
3616
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
@@ -3546,10 +3638,13 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
3546
3638
  }
3547
3639
  }
3548
3640
  ) : null,
3549
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CanvasSlotsContext.Provider, { value: slots, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(CanvasSinglePaneContext.Provider, { value: singlePane, children: [
3550
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "mxc-layout", "data-single-pane": singlePane ? "" : void 0, children: singlePane ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CanvasPaneView, { paneId: presentedPaneId ?? listPaneIds(layout)[0] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(LayoutNodeView, { node: layout }) }),
3641
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CanvasSlotsContext.Provider, { value: slots, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CanvasSinglePaneContext.Provider, { value: singlePane, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(CanvasWindowTabsContext.Provider, { value: windowTabs, children: [
3642
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "mxc-layout", "data-single-pane": singlePane ? "" : void 0, "data-window-tabs": windowTabs ? "" : void 0, children: [
3643
+ windowTabs ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CanvasWindowTabs, {}) : null,
3644
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "mxc-layout-panes", children: singlePane ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CanvasPaneView, { paneId: presentedPaneId ?? listPaneIds(layout)[0] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(LayoutNodeView, { node: layout }) })
3645
+ ] }),
3551
3646
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CanvasBodyLayer, {})
3552
- ] }) })
3647
+ ] }) }) })
3553
3648
  ] })
3554
3649
  }
3555
3650
  );