@ai-matrx/canvas 0.5.3 → 0.6.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.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 { N as CanvasRect, d as CanvasPaneId, C as CanvasItemId, a as CanvasWindowId, b as CanvasJson, i as CanvasItem, h as CanvasController, L as CanvasPresentation, Q as CanvasSessionEvent, O as CanvasSessionAttachReason, P as CanvasSessionDetachReason, K as CanvasPreferredTarget, e as CanvasStoreBinding, J as CanvasPersistencePort, A as CanvasErrorSink, E as CanvasLayoutRules, X as ToggleKindInput, D as CanvasKindVisibility, f as CanvasState } from './toggleKind-BD78LnMB.cjs';
3
+ import { P as CanvasRect, d as CanvasPaneId, C as CanvasItemId, a as CanvasWindowId, b as CanvasJson, i as CanvasItem, h as CanvasController, N as CanvasPresentation, S as CanvasSessionEvent, Q as CanvasSessionAttachReason, R as CanvasSessionDetachReason, M as CanvasPreferredTarget, e as CanvasStoreBinding, L as CanvasPersistencePort, D as CanvasErrorSink, G as CanvasLayoutRules, Z as ToggleKindInput, F as CanvasKindVisibility, f as CanvasState } from './toggleKind-DhDmWExm.cjs';
4
4
 
5
5
  /** One floating thing. Plain data, so a host can draw it in another process. */
6
6
  type CanvasFloating = {
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 { N as CanvasRect, d as CanvasPaneId, C as CanvasItemId, a as CanvasWindowId, b as CanvasJson, i as CanvasItem, h as CanvasController, L as CanvasPresentation, Q as CanvasSessionEvent, O as CanvasSessionAttachReason, P as CanvasSessionDetachReason, K as CanvasPreferredTarget, e as CanvasStoreBinding, J as CanvasPersistencePort, A as CanvasErrorSink, E as CanvasLayoutRules, X as ToggleKindInput, D as CanvasKindVisibility, f as CanvasState } from './toggleKind-BD78LnMB.js';
3
+ import { P as CanvasRect, d as CanvasPaneId, C as CanvasItemId, a as CanvasWindowId, b as CanvasJson, i as CanvasItem, h as CanvasController, N as CanvasPresentation, S as CanvasSessionEvent, Q as CanvasSessionAttachReason, R as CanvasSessionDetachReason, M as CanvasPreferredTarget, e as CanvasStoreBinding, L as CanvasPersistencePort, D as CanvasErrorSink, G as CanvasLayoutRules, Z as ToggleKindInput, F as CanvasKindVisibility, f as CanvasState } from './toggleKind-DhDmWExm.js';
4
4
 
5
5
  /** One floating thing. Plain data, so a host can draw it in another process. */
6
6
  type CanvasFloating = {
package/dist/react.js CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  var CANVAS_MAIN_WINDOW = "main";
17
17
  var CANVAS_LAUNCHER_KIND = "canvas.launcher";
18
18
  var CANVAS_HISTORY_LIMIT = 50;
19
+ var CANVAS_KEEP_ALIVE_DEPTH = 5;
19
20
  var CANVAS_MIN_WIDTH = 360;
20
21
  var CANVAS_DEFAULT_WIDTH = 640;
21
22
  var CANVAS_MIN_SPLIT_FRACTION = 0.12;
@@ -374,8 +375,10 @@ function sendHome(state, itemId, home, activate) {
374
375
  next = insertInto(next, homeFrame.focusedPaneId, itemId, activate);
375
376
  return activate ? withFrame(next, target, { focusedPaneId: homeFrame.focusedPaneId }) : next;
376
377
  }
377
- function newItem(kind, key, title, data, now, history) {
378
+ function newItem(kind, key, title, data, now, steps) {
378
379
  const id = canvasItemId(kind, key);
380
+ const history = steps?.history ?? [];
381
+ const kept = steps?.kept ?? [];
379
382
  return {
380
383
  id,
381
384
  kind,
@@ -384,12 +387,14 @@ function newItem(kind, key, title, data, now, history) {
384
387
  data,
385
388
  openedAt: now,
386
389
  updatedAt: now,
387
- instance: `${now.toString(36)}:${id}`,
388
- ...history && history.length > 0 ? { history: history.slice(-CANVAS_HISTORY_LIMIT) } : {}
390
+ // A reused instance is the SAME live body coming back (Back, or returning to a kept step).
391
+ instance: steps?.instance ?? `${now.toString(36)}:${id}`,
392
+ ...history.length > 0 ? { history: history.slice(-CANVAS_HISTORY_LIMIT) } : {},
393
+ ...kept.length > 0 ? { kept: kept.slice(-CANVAS_KEEP_ALIVE_DEPTH) } : {}
389
394
  };
390
395
  }
391
396
  function historyEntry(item) {
392
- return { kind: item.kind, key: item.key, title: item.title, data: item.data };
397
+ return { kind: item.kind, key: item.key, title: item.title, data: item.data, ...item.instance ? { instance: item.instance } : {} };
393
398
  }
394
399
  function replaceInPlace(state, current, next) {
395
400
  const pane = paneOf(state, current.id);
@@ -418,17 +423,25 @@ function sanitizeHistory(raw) {
418
423
  if (!entry || typeof entry !== "object") continue;
419
424
  const e = entry;
420
425
  if (typeof e.kind !== "string" || typeof e.key !== "string") continue;
421
- out.push({ kind: e.kind, key: e.key, title: typeof e.title === "string" ? e.title : null, data: e.data ?? null });
426
+ out.push({
427
+ kind: e.kind,
428
+ key: e.key,
429
+ title: typeof e.title === "string" ? e.title : null,
430
+ data: e.data ?? null,
431
+ ...typeof e.instance === "string" && e.instance ? { instance: e.instance } : {}
432
+ });
422
433
  }
423
434
  return out.slice(-CANVAS_HISTORY_LIMIT);
424
435
  }
425
436
  function sanitizeItem(item) {
426
437
  const history = sanitizeHistory(item.history);
427
- const { history: _history, instance, ...rest } = item;
438
+ const kept = sanitizeHistory(item.kept).slice(-CANVAS_KEEP_ALIVE_DEPTH);
439
+ const { history: _history, kept: _kept, instance, ...rest } = item;
428
440
  return {
429
441
  ...rest,
430
442
  ...typeof instance === "string" && instance ? { instance } : {},
431
- ...history.length > 0 ? { history } : {}
443
+ ...history.length > 0 ? { history } : {},
444
+ ...kept.length > 0 ? { kept } : {}
432
445
  };
433
446
  }
434
447
  function clampWidth(width) {
@@ -615,7 +628,22 @@ function canvasReducer(state = createInitialCanvasState(), action) {
615
628
  return current.kind === CANVAS_LAUNCHER_KIND ? canvasReducer(focused, canvasActions.closeItem(itemId)) : focused;
616
629
  }
617
630
  const history = [...current.history ?? [], historyEntry(current)];
618
- return replaceInPlace(state, current, newItem(to.kind, to.key, to.title ?? null, to.data ?? null, now, history));
631
+ const keptAll = current.kept ?? [];
632
+ const keptIndex = keptAll.findIndex((entry) => canvasItemId(entry.kind, entry.key) === targetId);
633
+ const revived = keptIndex >= 0 ? keptAll[keptIndex] : void 0;
634
+ const kept = keptAll.filter((_, i) => i !== keptIndex);
635
+ return replaceInPlace(
636
+ state,
637
+ current,
638
+ newItem(
639
+ to.kind,
640
+ to.key,
641
+ to.title !== void 0 ? to.title : revived?.title ?? null,
642
+ to.data !== void 0 ? to.data : revived?.data ?? null,
643
+ now,
644
+ { history, kept, instance: revived?.instance }
645
+ )
646
+ );
619
647
  }
620
648
  case `${P}back`: {
621
649
  const { itemId, now } = action.payload;
@@ -628,7 +656,12 @@ function canvasReducer(state = createInitialCanvasState(), action) {
628
656
  if (state.items[targetId]) {
629
657
  return focusExisting(canvasReducer(state, canvasActions.closeItem(itemId)), targetId);
630
658
  }
631
- return replaceInPlace(state, current, newItem(entry.kind, entry.key, entry.title, entry.data, now, history.slice(0, -1)));
659
+ const kept = [...(current.kept ?? []).filter((k) => canvasItemId(k.kind, k.key) !== itemId), historyEntry(current)];
660
+ return replaceInPlace(
661
+ state,
662
+ current,
663
+ newItem(entry.kind, entry.key, entry.title, entry.data, now, { history: history.slice(0, -1), kept, instance: entry.instance })
664
+ );
632
665
  }
633
666
  case `${P}closeItem`: {
634
667
  const { itemId } = action.payload;
@@ -1363,6 +1396,7 @@ var NO_HISTORY = Object.freeze([]);
1363
1396
  var selectCanvasCanGoBack = (s, itemId) => (s.items[itemId]?.history?.length ?? 0) > 0;
1364
1397
 
1365
1398
  // src/core/presentation.ts
1399
+ var CANVAS_PHONE_MAX_WIDTH = 767;
1366
1400
  var CANVAS_NARROW_WIDTH = 480;
1367
1401
  var SQUARE_BAND = 1.2;
1368
1402
  function canvasOrientation(width, height) {
@@ -1611,8 +1645,7 @@ function createLauncherEntryStore() {
1611
1645
  }
1612
1646
  };
1613
1647
  }
1614
- var PHONE_MAX_WIDTH = 767;
1615
- var isPhoneViewport = () => typeof window !== "undefined" && window.innerWidth <= PHONE_MAX_WIDTH;
1648
+ var isPhoneViewport = () => typeof window !== "undefined" && window.innerWidth <= CANVAS_PHONE_MAX_WIDTH;
1616
1649
  var isMacPlatform = () => typeof navigator !== "undefined" && /mac|iphone|ipad/i.test(navigator.userAgentData?.platform ?? navigator.platform ?? "");
1617
1650
  function escapeBelongsElsewhere(event) {
1618
1651
  if (event.defaultPrevented) return true;
@@ -1936,6 +1969,8 @@ var LayoutGridIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
1936
1969
  /* @__PURE__ */ jsx3("rect", { width: "7", height: "7", x: "14", y: "14", rx: "1" }),
1937
1970
  /* @__PURE__ */ jsx3("rect", { width: "7", height: "7", x: "3", y: "14", rx: "1" })
1938
1971
  ] });
1972
+ var ChevronLeftIcon = (p) => /* @__PURE__ */ jsx3(Glyph, { ...p, children: /* @__PURE__ */ jsx3("path", { d: "m15 18-6-6 6-6" }) });
1973
+ var ChevronRightIcon = (p) => /* @__PURE__ */ jsx3(Glyph, { ...p, children: /* @__PURE__ */ jsx3("path", { d: "m9 18 6-6-6-6" }) });
1939
1974
 
1940
1975
  // src/react/chrome.tsx
1941
1976
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -2271,6 +2306,21 @@ function useChoices() {
2271
2306
  }
2272
2307
  return { contextual, global };
2273
2308
  }
2309
+ var FOLLOW_UP_MS = 600;
2310
+ function swallowFollowUpClicks() {
2311
+ if (typeof window === "undefined") return;
2312
+ const types = ["pointerdown", "mousedown", "pointerup", "mouseup", "click", "dblclick"];
2313
+ const swallow = (event) => {
2314
+ if (event.detail >= 2 || event.type === "dblclick") {
2315
+ event.preventDefault();
2316
+ event.stopPropagation();
2317
+ }
2318
+ };
2319
+ for (const type of types) window.addEventListener(type, swallow, true);
2320
+ setTimeout(() => {
2321
+ for (const type of types) window.removeEventListener(type, swallow, true);
2322
+ }, FOLLOW_UP_MS);
2323
+ }
2274
2324
  function LauncherGrid({ onChoose }) {
2275
2325
  const { contextual, global } = useChoices();
2276
2326
  if (contextual.length === 0 && global.length === 0) return /* @__PURE__ */ jsx5(CanvasPanelEmpty, { children: "Nothing to open" });
@@ -2278,10 +2328,23 @@ function LauncherGrid({ onChoose }) {
2278
2328
  label ? /* @__PURE__ */ jsx5("h3", { className: "mxc-launcher-heading", children: label }) : null,
2279
2329
  /* @__PURE__ */ jsx5("div", { className: "mxc-launcher-grid", children: choices.map((choice) => {
2280
2330
  const Icon = choice.icon;
2281
- return /* @__PURE__ */ jsxs4("button", { type: "button", className: "mxc-launcher-item", onClick: () => onChoose(choice.input), children: [
2282
- /* @__PURE__ */ jsx5(Icon, { className: "mxc-launcher-icon" }),
2283
- /* @__PURE__ */ jsx5("span", { children: choice.label })
2284
- ] }, choice.id);
2331
+ return /* @__PURE__ */ jsxs4(
2332
+ "button",
2333
+ {
2334
+ type: "button",
2335
+ className: "mxc-launcher-item",
2336
+ onClick: (event) => {
2337
+ if (event.detail >= 2) return;
2338
+ swallowFollowUpClicks();
2339
+ onChoose(choice.input);
2340
+ },
2341
+ children: [
2342
+ /* @__PURE__ */ jsx5(Icon, { className: "mxc-launcher-icon" }),
2343
+ /* @__PURE__ */ jsx5("span", { children: choice.label })
2344
+ ]
2345
+ },
2346
+ choice.id
2347
+ );
2285
2348
  }) })
2286
2349
  ] });
2287
2350
  return /* @__PURE__ */ jsxs4("div", { className: "mxc-launcher", children: [
@@ -2394,6 +2457,10 @@ var CanvasSlotsContext = createContext3(null);
2394
2457
  function useCanvasSlots() {
2395
2458
  return useContext3(CanvasSlotsContext);
2396
2459
  }
2460
+ var CanvasSinglePaneContext = createContext3(false);
2461
+ function useCanvasSinglePane() {
2462
+ return useContext3(CanvasSinglePaneContext);
2463
+ }
2397
2464
  function useSlotSize(slots, paneId) {
2398
2465
  return useSyncExternalStore3(
2399
2466
  slots.subscribe,
@@ -2425,19 +2492,36 @@ function usePanePresentation(slots, paneId, isFullscreen, paneCount) {
2425
2492
  var hostKey = (item) => item.instance ?? item.id;
2426
2493
  var SEP2 = "";
2427
2494
  var ROW = "";
2495
+ function bornAt(instance, fallback) {
2496
+ const stamp = instance ? Number.parseInt(instance.slice(0, instance.indexOf(":")), 36) : Number.NaN;
2497
+ return Number.isFinite(stamp) ? stamp : fallback;
2498
+ }
2499
+ function keptCanvasSteps(item) {
2500
+ const back = (item.history ?? []).slice(-CANVAS_KEEP_ALIVE_DEPTH);
2501
+ const forward = (item.kept ?? []).slice(-CANVAS_KEEP_ALIVE_DEPTH);
2502
+ return [...back, ...forward].filter((entry) => !!entry.instance);
2503
+ }
2428
2504
  function layerSignature(state, windowId, onlyPane) {
2429
2505
  const frame2 = canvasWindowFrame(state, windowId);
2430
2506
  if (!frame2) return "";
2431
2507
  const rows = [];
2508
+ const seen = /* @__PURE__ */ new Set();
2509
+ const add = (hostId, born, fields) => {
2510
+ if (seen.has(hostId)) return;
2511
+ seen.add(hostId);
2512
+ rows.push({ born, hostId, row: [hostId, ...fields].join(SEP2) });
2513
+ };
2432
2514
  const paneIds = listPaneIds(frame2.layout);
2433
2515
  for (const paneId of onlyPane ? [onlyPane] : paneIds) {
2434
2516
  const pane = state.panes[paneId];
2435
2517
  for (const itemId of pane?.itemIds ?? []) {
2436
2518
  const item = state.items[itemId];
2437
- if (item) rows.push({ item, row: [hostKey(item), itemId, paneId, pane?.activeItemId === itemId ? "1" : "0"].join(SEP2) });
2519
+ if (!item) continue;
2520
+ add(hostKey(item), bornAt(item.instance, item.openedAt), [itemId, paneId, pane?.activeItemId === itemId ? "1" : "0", "0"]);
2521
+ for (const step of keptCanvasSteps(item)) add(step.instance, bornAt(step.instance, item.openedAt), [itemId, paneId, "0", "1"]);
2438
2522
  }
2439
2523
  }
2440
- rows.sort((a, b) => a.item.openedAt - b.item.openedAt || hostKey(a.item).localeCompare(hostKey(b.item)));
2524
+ rows.sort((a, b) => a.born - b.born || a.hostId.localeCompare(b.hostId));
2441
2525
  const head = [frame2.isOpen ? "1" : "0", frame2.isFullscreen ? "1" : "0", frame2.focusedPaneId, String(paneIds.length)].join(SEP2);
2442
2526
  return [head, ...rows.map((r) => r.row)].join(ROW);
2443
2527
  }
@@ -2451,8 +2535,8 @@ function parseSignature(signature) {
2451
2535
  focusedPaneId,
2452
2536
  paneCount: Number(count) || 1,
2453
2537
  placed: rows.map((row) => {
2454
- const [hostId = "", itemId = "", paneId = "", active] = row.split(SEP2);
2455
- return { hostId, itemId, paneId, active: active === "1" };
2538
+ const [hostId = "", itemId = "", paneId = "", active, kept] = row.split(SEP2);
2539
+ return { hostId, itemId, paneId, active: active === "1", kept: kept === "1" };
2456
2540
  })
2457
2541
  };
2458
2542
  }
@@ -2499,6 +2583,7 @@ function CanvasBodyLayer() {
2499
2583
  const { windowId, layer } = useCanvasInternals();
2500
2584
  const slots = useCanvasSlots();
2501
2585
  const signature = useCanvasState((s) => layerSignature(s, windowId));
2586
+ const single = useCanvasSinglePane();
2502
2587
  const root = useRef5(null);
2503
2588
  const [placement] = useState4(
2504
2589
  () => slots ? createPlacement(() => root.current, slots, () => layer.native.remeasure()) : null
@@ -2507,13 +2592,14 @@ function CanvasBodyLayer() {
2507
2592
  useIsoLayoutEffect(() => slots && placement ? slots.subscribe(placement.place) : void 0, [slots, placement]);
2508
2593
  const shape = parseSignature(signature);
2509
2594
  if (!slots || !placement || !shape) return null;
2510
- return /* @__PURE__ */ jsx6("div", { ref: root, className: "mxc-bodies", "data-mxc-bodies": windowId, children: shape.placed.map(({ hostId, itemId, paneId, active }) => /* @__PURE__ */ jsx6(
2595
+ return /* @__PURE__ */ jsx6("div", { ref: root, className: "mxc-bodies", "data-mxc-bodies": windowId, children: shape.placed.map(({ hostId, itemId, paneId, active, kept }) => /* @__PURE__ */ jsx6(
2511
2596
  BodyHost,
2512
2597
  {
2513
2598
  hostId,
2514
2599
  itemId,
2600
+ kept,
2515
2601
  paneId,
2516
- visible: shape.isOpen && active,
2602
+ visible: shape.isOpen && active && (!single || paneId === shape.focusedPaneId),
2517
2603
  focused: active && shape.focusedPaneId === paneId,
2518
2604
  isFullscreen: shape.isFullscreen,
2519
2605
  paneCount: shape.paneCount,
@@ -2527,11 +2613,12 @@ function CanvasPaneInlineBodies({ paneId, slots }) {
2527
2613
  const { windowId } = useCanvasInternals();
2528
2614
  const shape = parseSignature(useCanvasState((s) => layerSignature(s, windowId, paneId)));
2529
2615
  if (!shape) return null;
2530
- return /* @__PURE__ */ jsx6(Fragment, { children: shape.placed.map(({ hostId, itemId, active }) => /* @__PURE__ */ jsx6(
2616
+ return /* @__PURE__ */ jsx6(Fragment, { children: shape.placed.map(({ hostId, itemId, active, kept }) => /* @__PURE__ */ jsx6(
2531
2617
  BodyHost,
2532
2618
  {
2533
2619
  hostId,
2534
2620
  itemId,
2621
+ kept,
2535
2622
  paneId,
2536
2623
  visible: active,
2537
2624
  focused: active && shape.focusedPaneId === paneId,
@@ -2543,9 +2630,22 @@ function CanvasPaneInlineBodies({ paneId, slots }) {
2543
2630
  hostId
2544
2631
  )) });
2545
2632
  }
2633
+ function stepAsItem(step, openedAt) {
2634
+ return {
2635
+ id: canvasItemId(step.kind, step.key),
2636
+ kind: step.kind,
2637
+ key: step.key,
2638
+ title: step.title,
2639
+ data: step.data,
2640
+ openedAt,
2641
+ updatedAt: openedAt,
2642
+ ...step.instance ? { instance: step.instance } : {}
2643
+ };
2644
+ }
2546
2645
  var BodyHost = memo(function BodyHost2({
2547
2646
  hostId,
2548
2647
  itemId,
2648
+ kept,
2549
2649
  paneId,
2550
2650
  visible,
2551
2651
  focused,
@@ -2556,7 +2656,14 @@ var BodyHost = memo(function BodyHost2({
2556
2656
  }) {
2557
2657
  const canvas = useCanvas();
2558
2658
  const { windowId, ports } = useCanvasInternals();
2559
- const item = useCanvasState((s) => s.items[itemId]);
2659
+ const live = useCanvasState((s) => kept ? void 0 : s.items[itemId]);
2660
+ const step = useCanvasState((s) => {
2661
+ const ownerItem = kept ? s.items[itemId] : void 0;
2662
+ return ownerItem ? keptCanvasSteps(ownerItem).find((e) => e.instance === hostId) : void 0;
2663
+ });
2664
+ const owner = useCanvasState((s) => kept ? s.items[itemId]?.openedAt : void 0);
2665
+ const keptItem = useMemo2(() => step ? stepAsItem(step, owner ?? 0) : void 0, [step, owner]);
2666
+ const item = kept ? keptItem : live;
2560
2667
  const kind = useItemKind(item?.kind ?? "");
2561
2668
  const element = useRef5(null);
2562
2669
  const [everShown, setEverShown] = useState4(visible);
@@ -2580,6 +2687,7 @@ var BodyHost = memo(function BodyHost2({
2580
2687
  "data-visible": visible ? "true" : "false",
2581
2688
  "data-kind": item.kind,
2582
2689
  "data-item": item.id,
2690
+ "data-kept": kept ? "" : void 0,
2583
2691
  "data-pane": paneId,
2584
2692
  inert: !visible,
2585
2693
  "aria-hidden": visible ? void 0 : true,
@@ -2732,6 +2840,25 @@ function itemTitle(item, kind) {
2732
2840
  return kind?.label ?? item.kind;
2733
2841
  }
2734
2842
  var isMac = () => typeof navigator !== "undefined" && /mac|iphone|ipad/i.test(navigator.platform ?? "");
2843
+ function windowTabsSignature(state, windowId) {
2844
+ const frame2 = canvasWindowFrame(state, windowId);
2845
+ if (!frame2) return "";
2846
+ const rows = [];
2847
+ for (const paneId of listPaneIds(frame2.layout)) {
2848
+ for (const itemId of state.panes[paneId]?.itemIds ?? []) rows.push(`${itemId}${paneId}`);
2849
+ }
2850
+ return rows.join("");
2851
+ }
2852
+ function parseWindowTabs(signature) {
2853
+ if (!signature) return [];
2854
+ const perPane = /* @__PURE__ */ new Map();
2855
+ return signature.split("").map((row) => {
2856
+ const [itemId = "", paneId = ""] = row.split("");
2857
+ const index = perPane.get(paneId) ?? 0;
2858
+ perPane.set(paneId, index + 1);
2859
+ return { itemId, paneId, index };
2860
+ });
2861
+ }
2735
2862
  function CanvasPaneView({ paneId }) {
2736
2863
  const canvas = useCanvas();
2737
2864
  const { windowId, tabStrip } = useCanvasInternals();
@@ -2750,6 +2877,8 @@ function CanvasPaneView({ paneId }) {
2750
2877
  const role = useCanvasState((s) => windowId === CANVAS_MAIN_WINDOW ? "main" : s.windows[windowId]?.role ?? "window");
2751
2878
  const canGoBack = useCanvasState((s) => active ? selectCanvasCanGoBack(s, active.id) : false);
2752
2879
  const kind = useItemKind(active?.kind ?? "");
2880
+ const singlePane = useCanvasSinglePane();
2881
+ const windowTabs = useCanvasState((s) => singlePane ? windowTabsSignature(s, windowId) : "");
2753
2882
  const sectionRef = useRef6(null);
2754
2883
  const bodyRef = useRef6(null);
2755
2884
  const presentation = usePanePresentation(slots, paneId, isFullscreen, paneCount);
@@ -2769,7 +2898,8 @@ function CanvasPaneView({ paneId }) {
2769
2898
  const Leading = kind?.HeaderLeading;
2770
2899
  const Action = kind?.HeaderAction;
2771
2900
  const Toolbar = kind?.Toolbar;
2772
- const single = tabStrip === "auto" && pane.itemIds.length === 1 && active;
2901
+ const tabs = singlePane ? parseWindowTabs(windowTabs) : pane.itemIds.map((itemId, index) => ({ itemId, paneId, index }));
2902
+ const single = tabStrip === "auto" && tabs.length === 1 && active;
2773
2903
  const closeLabel = role === "pop-out" && paneCount === 1 ? "Close window" : paneCount > 1 ? "Close pane" : "Close canvas";
2774
2904
  return /* @__PURE__ */ jsxs6(
2775
2905
  CanvasPanel,
@@ -2782,7 +2912,7 @@ function CanvasPaneView({ paneId }) {
2782
2912
  "data-mxc-pane": paneId,
2783
2913
  header: /* @__PURE__ */ jsxs6(Fragment2, { children: [
2784
2914
  canGoBack && active ? /* @__PURE__ */ jsx7(CanvasHeaderButton, { className: "mxc-back", label: "Back", onClick: () => void canvas.back(active.id), children: /* @__PURE__ */ jsx7(ArrowLeftIcon, {}) }) : null,
2785
- single && active ? /* @__PURE__ */ jsx7(SingleTitle, { item: active, kind }) : pane.itemIds.length > 0 ? /* @__PURE__ */ jsx7(TabStrip, { paneId, itemIds: pane.itemIds, activeItemId: pane.activeItemId }) : /* @__PURE__ */ jsx7("span", { className: "mxc-panel-title", children: "New tab" }),
2915
+ single && active ? /* @__PURE__ */ jsx7(SingleTitle, { item: active, kind }) : tabs.length > 0 ? /* @__PURE__ */ jsx7(TabStrip, { tabs, activeItemId: pane.activeItemId }) : /* @__PURE__ */ jsx7("span", { className: "mxc-panel-title", children: "New tab" }),
2786
2916
  /* @__PURE__ */ jsx7(CanvasHeaderButton, { className: "mxc-new-tab", label: "New tab", onClick: () => void canvas.newTab(paneId), children: /* @__PURE__ */ jsx7(PlusIcon, {}) })
2787
2917
  ] }),
2788
2918
  leading: Leading && kindProps ? /* @__PURE__ */ jsx7(Leading, { ...kindProps }) : null,
@@ -2831,38 +2961,71 @@ function SingleTitle({ item, kind }) {
2831
2961
  /* @__PURE__ */ jsx7("button", { type: "button", className: "mxc-tab-close", "data-slot": "browser-tab-close", "aria-label": `Close ${title}`, onClick: () => canvas.close(item.id), children: /* @__PURE__ */ jsx7(CloseIcon, {}) })
2832
2962
  ] });
2833
2963
  }
2834
- function TabStrip({ paneId, itemIds, activeItemId }) {
2964
+ function TabStrip({ tabs, activeItemId }) {
2835
2965
  const strip = useRef6(null);
2966
+ const [overflow, setOverflow] = useState5({ start: false, end: false });
2967
+ const measure = useRef6(() => void 0);
2968
+ measure.current = () => {
2969
+ const el = strip.current;
2970
+ if (!el) return;
2971
+ const start = el.scrollLeft > 1;
2972
+ const end = el.scrollLeft + el.clientWidth < el.scrollWidth - 1;
2973
+ setOverflow((prev) => prev.start === start && prev.end === end ? prev : { start, end });
2974
+ };
2975
+ const tabKey = tabs.map((tab) => tab.itemId).join("|");
2836
2976
  useIsoLayoutEffect2(() => {
2837
2977
  const el = strip.current;
2838
2978
  if (!el) return void 0;
2839
2979
  const reveal = () => {
2840
2980
  const tab = el.querySelector('[role="tab"][aria-selected="true"]');
2841
- if (!tab) return;
2842
- const left = tab.offsetLeft;
2843
- const right = left + tab.offsetWidth;
2844
- if (left < el.scrollLeft) el.scrollLeft = left;
2845
- else if (right > el.scrollLeft + el.clientWidth) el.scrollLeft = right - el.clientWidth;
2981
+ if (tab) {
2982
+ const left = tab.offsetLeft;
2983
+ const right = left + tab.offsetWidth;
2984
+ if (left < el.scrollLeft) el.scrollLeft = left;
2985
+ else if (right > el.scrollLeft + el.clientWidth) el.scrollLeft = right - el.clientWidth;
2986
+ }
2987
+ measure.current();
2846
2988
  };
2847
2989
  reveal();
2848
2990
  const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(reveal);
2849
2991
  observer?.observe(el);
2850
- const activeTab = el.querySelector('[role="tab"][aria-selected="true"]');
2851
- if (activeTab) observer?.observe(activeTab);
2992
+ for (const tab of el.querySelectorAll('[role="tab"]')) observer?.observe(tab);
2852
2993
  return () => observer?.disconnect();
2853
- }, [activeItemId, itemIds.length]);
2854
- return /* @__PURE__ */ jsx7(
2994
+ }, [activeItemId, tabKey]);
2995
+ const page = (direction) => {
2996
+ const el = strip.current;
2997
+ if (!el) return;
2998
+ const by = Math.max(48, Math.round(el.clientWidth * 0.8)) * direction;
2999
+ if (typeof el.scrollBy === "function") el.scrollBy({ left: by, behavior: "smooth" });
3000
+ else el.scrollLeft += by;
3001
+ };
3002
+ const overflowing = overflow.start || overflow.end;
3003
+ return /* @__PURE__ */ jsxs6(
2855
3004
  "div",
2856
3005
  {
2857
- ref: strip,
2858
- className: "mxc-tabs",
2859
- role: "tablist",
2860
- "aria-label": "Canvas tabs",
2861
- onWheel: (event) => {
2862
- const el = strip.current;
2863
- if (el && Math.abs(event.deltaY) > Math.abs(event.deltaX)) el.scrollLeft += event.deltaY;
2864
- },
2865
- children: itemIds.map((itemId, index) => /* @__PURE__ */ jsx7(ItemTab, { itemId, paneId, index, active: itemId === activeItemId }, itemId))
3006
+ className: "mxc-tabstrip",
3007
+ "data-overflow": overflowing ? "" : void 0,
3008
+ "data-overflow-start": overflow.start ? "" : void 0,
3009
+ "data-overflow-end": overflow.end ? "" : void 0,
3010
+ children: [
3011
+ overflowing ? /* @__PURE__ */ jsx7(CanvasHeaderButton, { className: "mxc-tabs-scroll", label: "Scroll tabs left", available: overflow.start, onClick: () => page(-1), children: /* @__PURE__ */ jsx7(ChevronLeftIcon, {}) }) : null,
3012
+ /* @__PURE__ */ jsx7(
3013
+ "div",
3014
+ {
3015
+ ref: strip,
3016
+ className: "mxc-tabs",
3017
+ role: "tablist",
3018
+ "aria-label": "Canvas tabs",
3019
+ onScroll: () => measure.current(),
3020
+ onWheel: (event) => {
3021
+ const el = strip.current;
3022
+ if (el && Math.abs(event.deltaY) > Math.abs(event.deltaX)) el.scrollLeft += event.deltaY;
3023
+ },
3024
+ children: tabs.map((tab) => /* @__PURE__ */ jsx7(ItemTab, { itemId: tab.itemId, paneId: tab.paneId, index: tab.index, active: tab.itemId === activeItemId }, tab.itemId))
3025
+ }
3026
+ ),
3027
+ overflowing ? /* @__PURE__ */ jsx7(CanvasHeaderButton, { className: "mxc-tabs-scroll", label: "Scroll tabs right", available: overflow.end, onClick: () => page(1), children: /* @__PURE__ */ jsx7(ChevronRightIcon, {}) }) : null
3028
+ ]
2866
3029
  }
2867
3030
  );
2868
3031
  }
@@ -3015,7 +3178,10 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
3015
3178
  const isFullscreen = useCanvasState((s) => selectCanvasIsFullscreen(s, windowId));
3016
3179
  const width = useCanvasState((s) => selectCanvasWidth(s, windowId));
3017
3180
  const layout = useCanvasState((s) => selectCanvasLayout(s, windowId));
3181
+ const focusedPaneId = useCanvasState((s) => selectCanvasFocusedPaneId(s, windowId));
3018
3182
  const region = useRegionWidth();
3183
+ const viewport = useViewportWidth();
3184
+ const singlePane = placement === "docked" && viewport > 0 && viewport <= CANVAS_PHONE_MAX_WIDTH;
3019
3185
  const [dragWidth, setDragWidth] = useState6(null);
3020
3186
  const [slots] = useState6(createCanvasSlots);
3021
3187
  const ref = useRef7(null);
@@ -3080,10 +3246,10 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
3080
3246
  }
3081
3247
  }
3082
3248
  ) : null,
3083
- /* @__PURE__ */ jsxs7(CanvasSlotsContext.Provider, { value: slots, children: [
3084
- /* @__PURE__ */ jsx8("div", { className: "mxc-layout", children: /* @__PURE__ */ jsx8(LayoutNodeView, { node: layout }) }),
3249
+ /* @__PURE__ */ jsx8(CanvasSlotsContext.Provider, { value: slots, children: /* @__PURE__ */ jsxs7(CanvasSinglePaneContext.Provider, { value: singlePane, children: [
3250
+ /* @__PURE__ */ jsx8("div", { className: "mxc-layout", "data-single-pane": singlePane ? "" : void 0, children: singlePane ? /* @__PURE__ */ jsx8(CanvasPaneView, { paneId: presentedPane(layout, focusedPaneId) }) : /* @__PURE__ */ jsx8(LayoutNodeView, { node: layout }) }),
3085
3251
  /* @__PURE__ */ jsx8(CanvasBodyLayer, {})
3086
- ] })
3252
+ ] }) })
3087
3253
  ] })
3088
3254
  }
3089
3255
  );
@@ -3131,6 +3297,10 @@ function WidthHandle({ width, region, onDrag }) {
3131
3297
  }
3132
3298
  );
3133
3299
  }
3300
+ function presentedPane(layout, focused) {
3301
+ const ids = listPaneIds(layout);
3302
+ return ids.find((id) => id === focused) ?? ids[0];
3303
+ }
3134
3304
  function LayoutNodeView({ node }) {
3135
3305
  if (node.type === "pane") return /* @__PURE__ */ jsx8(CanvasPaneView, { paneId: node.paneId });
3136
3306
  return /* @__PURE__ */ jsx8(SplitView, { node });
@@ -3233,6 +3403,7 @@ function CanvasToggle({
3233
3403
  ariaLabel: isOpen ? "Hide canvas" : "Show canvas",
3234
3404
  tooltip: `${isOpen ? "Hide canvas" : "Show canvas"}${hint ? ` (${hint})` : ""}`,
3235
3405
  icon: /* @__PURE__ */ jsx8(PanelRightIcon, {}),
3406
+ pressed: isOpen,
3236
3407
  onClick: () => controller.toggle()
3237
3408
  }
3238
3409
  ),