@ai-matrx/canvas 0.5.3 → 0.6.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/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;
@@ -1812,7 +1845,7 @@ function useCanvasLauncherEntries() {
1812
1845
  // src/react/CanvasColumn.tsx
1813
1846
  import {
1814
1847
  useEffect as useEffect7,
1815
- useLayoutEffect as useLayoutEffect4,
1848
+ useLayoutEffect as useLayoutEffect5,
1816
1849
  useRef as useRef7,
1817
1850
  useState as useState6,
1818
1851
  useSyncExternalStore as useSyncExternalStore4
@@ -1826,7 +1859,7 @@ import {
1826
1859
  lazy,
1827
1860
  memo,
1828
1861
  useEffect as useEffect5,
1829
- useLayoutEffect as useLayoutEffect2,
1862
+ useLayoutEffect as useLayoutEffect3,
1830
1863
  useMemo as useMemo2,
1831
1864
  useRef as useRef5,
1832
1865
  useState as useState4
@@ -1836,6 +1869,7 @@ import {
1836
1869
  import {
1837
1870
  forwardRef,
1838
1871
  useEffect as useEffect3,
1872
+ useLayoutEffect as useLayoutEffect2,
1839
1873
  useRef as useRef3,
1840
1874
  useState as useState3
1841
1875
  } from "react";
@@ -1936,9 +1970,26 @@ var LayoutGridIcon = (p) => /* @__PURE__ */ jsxs2(Glyph, { ...p, children: [
1936
1970
  /* @__PURE__ */ jsx3("rect", { width: "7", height: "7", x: "14", y: "14", rx: "1" }),
1937
1971
  /* @__PURE__ */ jsx3("rect", { width: "7", height: "7", x: "3", y: "14", rx: "1" })
1938
1972
  ] });
1973
+ var ChevronLeftIcon = (p) => /* @__PURE__ */ jsx3(Glyph, { ...p, children: /* @__PURE__ */ jsx3("path", { d: "m15 18-6-6 6-6" }) });
1974
+ var ChevronRightIcon = (p) => /* @__PURE__ */ jsx3(Glyph, { ...p, children: /* @__PURE__ */ jsx3("path", { d: "m9 18 6-6-6-6" }) });
1939
1975
 
1940
1976
  // src/react/chrome.tsx
1941
1977
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1978
+ var useIsoLayoutEffect = typeof window === "undefined" ? useEffect3 : useLayoutEffect2;
1979
+ function useTruncated(title) {
1980
+ const ref = useRef3(null);
1981
+ const [truncated, setTruncated] = useState3(false);
1982
+ useIsoLayoutEffect(() => {
1983
+ const el = ref.current;
1984
+ if (!el) return void 0;
1985
+ const check = () => setTruncated(el.scrollWidth - el.clientWidth > 1);
1986
+ check();
1987
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(check);
1988
+ observer?.observe(el);
1989
+ return () => observer?.disconnect();
1990
+ }, [title]);
1991
+ return { ref, truncated };
1992
+ }
1942
1993
  var cx = (...names) => names.filter(Boolean).join(" ");
1943
1994
  function useTooltip(text) {
1944
1995
  const internals = useOptionalCanvasInternals();
@@ -2078,6 +2129,7 @@ function CanvasPanelEmpty({ children, className }) {
2078
2129
  }
2079
2130
  var CanvasTab = forwardRef(function CanvasTab2({ title, icon, note, active, onSelect, onClose, className, onKeyDown, onClick, ...rest }, ref) {
2080
2131
  const shown = title || "New tab";
2132
+ const name = useTruncated(shown);
2081
2133
  return /* @__PURE__ */ jsxs3(
2082
2134
  "div",
2083
2135
  {
@@ -2087,6 +2139,7 @@ var CanvasTab = forwardRef(function CanvasTab2({ title, icon, note, active, onSe
2087
2139
  tabIndex: active ? 0 : -1,
2088
2140
  "data-slot": "browser-tab",
2089
2141
  "data-active": active ? "" : void 0,
2142
+ "data-truncated": name.truncated ? "" : void 0,
2090
2143
  title: shown,
2091
2144
  className: cx("mxc-tab", className),
2092
2145
  ...rest,
@@ -2104,7 +2157,7 @@ var CanvasTab = forwardRef(function CanvasTab2({ title, icon, note, active, onSe
2104
2157
  },
2105
2158
  children: [
2106
2159
  /* @__PURE__ */ jsx4("span", { className: "mxc-tab-icon", children: icon }),
2107
- /* @__PURE__ */ jsx4("span", { className: "mxc-tab-title", children: shown }),
2160
+ /* @__PURE__ */ jsx4("span", { ref: name.ref, className: "mxc-tab-title", children: shown }),
2108
2161
  note ? /* @__PURE__ */ jsx4("span", { className: "mxc-tab-note", "data-slot": "browser-tab-note", children: note }) : null,
2109
2162
  onClose ? /* @__PURE__ */ jsx4(
2110
2163
  "button",
@@ -2271,6 +2324,21 @@ function useChoices() {
2271
2324
  }
2272
2325
  return { contextual, global };
2273
2326
  }
2327
+ var FOLLOW_UP_MS = 600;
2328
+ function swallowFollowUpClicks() {
2329
+ if (typeof window === "undefined") return;
2330
+ const types = ["pointerdown", "mousedown", "pointerup", "mouseup", "click", "dblclick"];
2331
+ const swallow = (event) => {
2332
+ if (event.detail >= 2 || event.type === "dblclick") {
2333
+ event.preventDefault();
2334
+ event.stopPropagation();
2335
+ }
2336
+ };
2337
+ for (const type of types) window.addEventListener(type, swallow, true);
2338
+ setTimeout(() => {
2339
+ for (const type of types) window.removeEventListener(type, swallow, true);
2340
+ }, FOLLOW_UP_MS);
2341
+ }
2274
2342
  function LauncherGrid({ onChoose }) {
2275
2343
  const { contextual, global } = useChoices();
2276
2344
  if (contextual.length === 0 && global.length === 0) return /* @__PURE__ */ jsx5(CanvasPanelEmpty, { children: "Nothing to open" });
@@ -2278,10 +2346,23 @@ function LauncherGrid({ onChoose }) {
2278
2346
  label ? /* @__PURE__ */ jsx5("h3", { className: "mxc-launcher-heading", children: label }) : null,
2279
2347
  /* @__PURE__ */ jsx5("div", { className: "mxc-launcher-grid", children: choices.map((choice) => {
2280
2348
  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);
2349
+ return /* @__PURE__ */ jsxs4(
2350
+ "button",
2351
+ {
2352
+ type: "button",
2353
+ className: "mxc-launcher-item",
2354
+ onClick: (event) => {
2355
+ if (event.detail >= 2) return;
2356
+ swallowFollowUpClicks();
2357
+ onChoose(choice.input);
2358
+ },
2359
+ children: [
2360
+ /* @__PURE__ */ jsx5(Icon, { className: "mxc-launcher-icon" }),
2361
+ /* @__PURE__ */ jsx5("span", { children: choice.label })
2362
+ ]
2363
+ },
2364
+ choice.id
2365
+ );
2285
2366
  }) })
2286
2367
  ] });
2287
2368
  return /* @__PURE__ */ jsxs4("div", { className: "mxc-launcher", children: [
@@ -2394,6 +2475,10 @@ var CanvasSlotsContext = createContext3(null);
2394
2475
  function useCanvasSlots() {
2395
2476
  return useContext3(CanvasSlotsContext);
2396
2477
  }
2478
+ var CanvasSinglePaneContext = createContext3(false);
2479
+ function useCanvasSinglePane() {
2480
+ return useContext3(CanvasSinglePaneContext);
2481
+ }
2397
2482
  function useSlotSize(slots, paneId) {
2398
2483
  return useSyncExternalStore3(
2399
2484
  slots.subscribe,
@@ -2404,7 +2489,7 @@ function useSlotSize(slots, paneId) {
2404
2489
 
2405
2490
  // src/react/bodies.tsx
2406
2491
  import { Fragment, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2407
- var useIsoLayoutEffect = typeof window === "undefined" ? useEffect5 : useLayoutEffect2;
2492
+ var useIsoLayoutEffect2 = typeof window === "undefined" ? useEffect5 : useLayoutEffect3;
2408
2493
  var LAZY = /* @__PURE__ */ Symbol.for("ai-matrx.canvas.lazy");
2409
2494
  function kindComponent(kind) {
2410
2495
  if (kind.component) return kind.component;
@@ -2425,19 +2510,36 @@ function usePanePresentation(slots, paneId, isFullscreen, paneCount) {
2425
2510
  var hostKey = (item) => item.instance ?? item.id;
2426
2511
  var SEP2 = "";
2427
2512
  var ROW = "";
2513
+ function bornAt(instance, fallback) {
2514
+ const stamp = instance ? Number.parseInt(instance.slice(0, instance.indexOf(":")), 36) : Number.NaN;
2515
+ return Number.isFinite(stamp) ? stamp : fallback;
2516
+ }
2517
+ function keptCanvasSteps(item) {
2518
+ const back = (item.history ?? []).slice(-CANVAS_KEEP_ALIVE_DEPTH);
2519
+ const forward = (item.kept ?? []).slice(-CANVAS_KEEP_ALIVE_DEPTH);
2520
+ return [...back, ...forward].filter((entry) => !!entry.instance);
2521
+ }
2428
2522
  function layerSignature(state, windowId, onlyPane) {
2429
2523
  const frame2 = canvasWindowFrame(state, windowId);
2430
2524
  if (!frame2) return "";
2431
2525
  const rows = [];
2526
+ const seen = /* @__PURE__ */ new Set();
2527
+ const add = (hostId, born, fields) => {
2528
+ if (seen.has(hostId)) return;
2529
+ seen.add(hostId);
2530
+ rows.push({ born, hostId, row: [hostId, ...fields].join(SEP2) });
2531
+ };
2432
2532
  const paneIds = listPaneIds(frame2.layout);
2433
2533
  for (const paneId of onlyPane ? [onlyPane] : paneIds) {
2434
2534
  const pane = state.panes[paneId];
2435
2535
  for (const itemId of pane?.itemIds ?? []) {
2436
2536
  const item = state.items[itemId];
2437
- if (item) rows.push({ item, row: [hostKey(item), itemId, paneId, pane?.activeItemId === itemId ? "1" : "0"].join(SEP2) });
2537
+ if (!item) continue;
2538
+ add(hostKey(item), bornAt(item.instance, item.openedAt), [itemId, paneId, pane?.activeItemId === itemId ? "1" : "0", "0"]);
2539
+ for (const step of keptCanvasSteps(item)) add(step.instance, bornAt(step.instance, item.openedAt), [itemId, paneId, "0", "1"]);
2438
2540
  }
2439
2541
  }
2440
- rows.sort((a, b) => a.item.openedAt - b.item.openedAt || hostKey(a.item).localeCompare(hostKey(b.item)));
2542
+ rows.sort((a, b) => a.born - b.born || a.hostId.localeCompare(b.hostId));
2441
2543
  const head = [frame2.isOpen ? "1" : "0", frame2.isFullscreen ? "1" : "0", frame2.focusedPaneId, String(paneIds.length)].join(SEP2);
2442
2544
  return [head, ...rows.map((r) => r.row)].join(ROW);
2443
2545
  }
@@ -2451,8 +2553,8 @@ function parseSignature(signature) {
2451
2553
  focusedPaneId,
2452
2554
  paneCount: Number(count) || 1,
2453
2555
  placed: rows.map((row) => {
2454
- const [hostId = "", itemId = "", paneId = "", active] = row.split(SEP2);
2455
- return { hostId, itemId, paneId, active: active === "1" };
2556
+ const [hostId = "", itemId = "", paneId = "", active, kept] = row.split(SEP2);
2557
+ return { hostId, itemId, paneId, active: active === "1", kept: kept === "1" };
2456
2558
  })
2457
2559
  };
2458
2560
  }
@@ -2499,21 +2601,23 @@ function CanvasBodyLayer() {
2499
2601
  const { windowId, layer } = useCanvasInternals();
2500
2602
  const slots = useCanvasSlots();
2501
2603
  const signature = useCanvasState((s) => layerSignature(s, windowId));
2604
+ const single = useCanvasSinglePane();
2502
2605
  const root = useRef5(null);
2503
2606
  const [placement] = useState4(
2504
2607
  () => slots ? createPlacement(() => root.current, slots, () => layer.native.remeasure()) : null
2505
2608
  );
2506
- useIsoLayoutEffect(() => placement?.place());
2507
- useIsoLayoutEffect(() => slots && placement ? slots.subscribe(placement.place) : void 0, [slots, placement]);
2609
+ useIsoLayoutEffect2(() => placement?.place());
2610
+ useIsoLayoutEffect2(() => slots && placement ? slots.subscribe(placement.place) : void 0, [slots, placement]);
2508
2611
  const shape = parseSignature(signature);
2509
2612
  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(
2613
+ 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
2614
  BodyHost,
2512
2615
  {
2513
2616
  hostId,
2514
2617
  itemId,
2618
+ kept,
2515
2619
  paneId,
2516
- visible: shape.isOpen && active,
2620
+ visible: shape.isOpen && active && (!single || paneId === shape.focusedPaneId),
2517
2621
  focused: active && shape.focusedPaneId === paneId,
2518
2622
  isFullscreen: shape.isFullscreen,
2519
2623
  paneCount: shape.paneCount,
@@ -2527,11 +2631,12 @@ function CanvasPaneInlineBodies({ paneId, slots }) {
2527
2631
  const { windowId } = useCanvasInternals();
2528
2632
  const shape = parseSignature(useCanvasState((s) => layerSignature(s, windowId, paneId)));
2529
2633
  if (!shape) return null;
2530
- return /* @__PURE__ */ jsx6(Fragment, { children: shape.placed.map(({ hostId, itemId, active }) => /* @__PURE__ */ jsx6(
2634
+ return /* @__PURE__ */ jsx6(Fragment, { children: shape.placed.map(({ hostId, itemId, active, kept }) => /* @__PURE__ */ jsx6(
2531
2635
  BodyHost,
2532
2636
  {
2533
2637
  hostId,
2534
2638
  itemId,
2639
+ kept,
2535
2640
  paneId,
2536
2641
  visible: active,
2537
2642
  focused: active && shape.focusedPaneId === paneId,
@@ -2543,9 +2648,22 @@ function CanvasPaneInlineBodies({ paneId, slots }) {
2543
2648
  hostId
2544
2649
  )) });
2545
2650
  }
2651
+ function stepAsItem(step, openedAt) {
2652
+ return {
2653
+ id: canvasItemId(step.kind, step.key),
2654
+ kind: step.kind,
2655
+ key: step.key,
2656
+ title: step.title,
2657
+ data: step.data,
2658
+ openedAt,
2659
+ updatedAt: openedAt,
2660
+ ...step.instance ? { instance: step.instance } : {}
2661
+ };
2662
+ }
2546
2663
  var BodyHost = memo(function BodyHost2({
2547
2664
  hostId,
2548
2665
  itemId,
2666
+ kept,
2549
2667
  paneId,
2550
2668
  visible,
2551
2669
  focused,
@@ -2556,7 +2674,14 @@ var BodyHost = memo(function BodyHost2({
2556
2674
  }) {
2557
2675
  const canvas = useCanvas();
2558
2676
  const { windowId, ports } = useCanvasInternals();
2559
- const item = useCanvasState((s) => s.items[itemId]);
2677
+ const live = useCanvasState((s) => kept ? void 0 : s.items[itemId]);
2678
+ const step = useCanvasState((s) => {
2679
+ const ownerItem = kept ? s.items[itemId] : void 0;
2680
+ return ownerItem ? keptCanvasSteps(ownerItem).find((e) => e.instance === hostId) : void 0;
2681
+ });
2682
+ const owner = useCanvasState((s) => kept ? s.items[itemId]?.openedAt : void 0);
2683
+ const keptItem = useMemo2(() => step ? stepAsItem(step, owner ?? 0) : void 0, [step, owner]);
2684
+ const item = kept ? keptItem : live;
2560
2685
  const kind = useItemKind(item?.kind ?? "");
2561
2686
  const element = useRef5(null);
2562
2687
  const [everShown, setEverShown] = useState4(visible);
@@ -2564,7 +2689,7 @@ var BodyHost = memo(function BodyHost2({
2564
2689
  const mounted = kind?.unmountWhenHidden ? visible : everShown;
2565
2690
  const presentation = usePanePresentation(slots, paneId, isFullscreen, paneCount);
2566
2691
  const drop = usePaneDrop(paneId, () => slots.section(paneId));
2567
- useIsoLayoutEffect(() => {
2692
+ useIsoLayoutEffect2(() => {
2568
2693
  const el = element.current;
2569
2694
  if (!placement || !el) return void 0;
2570
2695
  return placement.register(hostId, { element: el, paneId });
@@ -2580,6 +2705,7 @@ var BodyHost = memo(function BodyHost2({
2580
2705
  "data-visible": visible ? "true" : "false",
2581
2706
  "data-kind": item.kind,
2582
2707
  "data-item": item.id,
2708
+ "data-kept": kept ? "" : void 0,
2583
2709
  "data-pane": paneId,
2584
2710
  inert: !visible,
2585
2711
  "aria-hidden": visible ? void 0 : true,
@@ -2661,8 +2787,8 @@ function NativeBody({ itemId, windowId, visible, children }) {
2661
2787
  const rect = { ...toCanvasRect(box), devicePixelRatio: window.devicePixelRatio || 1 };
2662
2788
  layer.native.report(itemId, rect, { visible: !layer.isBlocked(), windowId });
2663
2789
  };
2664
- useIsoLayoutEffect(() => measure.current());
2665
- useIsoLayoutEffect(() => {
2790
+ useIsoLayoutEffect2(() => measure.current());
2791
+ useIsoLayoutEffect2(() => {
2666
2792
  const el = ref.current;
2667
2793
  const run = () => measure.current();
2668
2794
  const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(run);
@@ -2715,10 +2841,10 @@ var ItemBoundary = class extends Component {
2715
2841
  };
2716
2842
 
2717
2843
  // src/react/CanvasPane.tsx
2718
- import { Component as Component2, useEffect as useEffect6, useLayoutEffect as useLayoutEffect3, useRef as useRef6, useState as useState5 } from "react";
2844
+ import { Component as Component2, useEffect as useEffect6, useLayoutEffect as useLayoutEffect4, useRef as useRef6, useState as useState5 } from "react";
2719
2845
  import * as Menu from "@radix-ui/react-dropdown-menu";
2720
2846
  import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2721
- var useIsoLayoutEffect2 = typeof window === "undefined" ? useEffect6 : useLayoutEffect3;
2847
+ var useIsoLayoutEffect3 = typeof window === "undefined" ? useEffect6 : useLayoutEffect4;
2722
2848
  function itemTitle(item, kind) {
2723
2849
  if (item.title) return item.title;
2724
2850
  if (item.kind === CANVAS_LAUNCHER_KIND) return "New tab";
@@ -2732,6 +2858,25 @@ function itemTitle(item, kind) {
2732
2858
  return kind?.label ?? item.kind;
2733
2859
  }
2734
2860
  var isMac = () => typeof navigator !== "undefined" && /mac|iphone|ipad/i.test(navigator.platform ?? "");
2861
+ function windowTabsSignature(state, windowId) {
2862
+ const frame2 = canvasWindowFrame(state, windowId);
2863
+ if (!frame2) return "";
2864
+ const rows = [];
2865
+ for (const paneId of listPaneIds(frame2.layout)) {
2866
+ for (const itemId of state.panes[paneId]?.itemIds ?? []) rows.push(`${itemId}${paneId}`);
2867
+ }
2868
+ return rows.join("");
2869
+ }
2870
+ function parseWindowTabs(signature) {
2871
+ if (!signature) return [];
2872
+ const perPane = /* @__PURE__ */ new Map();
2873
+ return signature.split("").map((row) => {
2874
+ const [itemId = "", paneId = ""] = row.split("");
2875
+ const index = perPane.get(paneId) ?? 0;
2876
+ perPane.set(paneId, index + 1);
2877
+ return { itemId, paneId, index };
2878
+ });
2879
+ }
2735
2880
  function CanvasPaneView({ paneId }) {
2736
2881
  const canvas = useCanvas();
2737
2882
  const { windowId, tabStrip } = useCanvasInternals();
@@ -2750,11 +2895,13 @@ function CanvasPaneView({ paneId }) {
2750
2895
  const role = useCanvasState((s) => windowId === CANVAS_MAIN_WINDOW ? "main" : s.windows[windowId]?.role ?? "window");
2751
2896
  const canGoBack = useCanvasState((s) => active ? selectCanvasCanGoBack(s, active.id) : false);
2752
2897
  const kind = useItemKind(active?.kind ?? "");
2898
+ const singlePane = useCanvasSinglePane();
2899
+ const windowTabs = useCanvasState((s) => singlePane ? windowTabsSignature(s, windowId) : "");
2753
2900
  const sectionRef = useRef6(null);
2754
2901
  const bodyRef = useRef6(null);
2755
2902
  const presentation = usePanePresentation(slots, paneId, isFullscreen, paneCount);
2756
2903
  const drop = usePaneDrop(paneId, () => sectionRef.current);
2757
- useIsoLayoutEffect2(() => {
2904
+ useIsoLayoutEffect3(() => {
2758
2905
  const body = bodyRef.current;
2759
2906
  const section = sectionRef.current;
2760
2907
  if (body) slots.set(paneId, body);
@@ -2769,7 +2916,8 @@ function CanvasPaneView({ paneId }) {
2769
2916
  const Leading = kind?.HeaderLeading;
2770
2917
  const Action = kind?.HeaderAction;
2771
2918
  const Toolbar = kind?.Toolbar;
2772
- const single = tabStrip === "auto" && pane.itemIds.length === 1 && active;
2919
+ const tabs = singlePane ? parseWindowTabs(windowTabs) : pane.itemIds.map((itemId, index) => ({ itemId, paneId, index }));
2920
+ const single = tabStrip === "auto" && tabs.length === 1 && active;
2773
2921
  const closeLabel = role === "pop-out" && paneCount === 1 ? "Close window" : paneCount > 1 ? "Close pane" : "Close canvas";
2774
2922
  return /* @__PURE__ */ jsxs6(
2775
2923
  CanvasPanel,
@@ -2782,7 +2930,7 @@ function CanvasPaneView({ paneId }) {
2782
2930
  "data-mxc-pane": paneId,
2783
2931
  header: /* @__PURE__ */ jsxs6(Fragment2, { children: [
2784
2932
  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" }),
2933
+ 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
2934
  /* @__PURE__ */ jsx7(CanvasHeaderButton, { className: "mxc-new-tab", label: "New tab", onClick: () => void canvas.newTab(paneId), children: /* @__PURE__ */ jsx7(PlusIcon, {}) })
2787
2935
  ] }),
2788
2936
  leading: Leading && kindProps ? /* @__PURE__ */ jsx7(Leading, { ...kindProps }) : null,
@@ -2831,38 +2979,71 @@ function SingleTitle({ item, kind }) {
2831
2979
  /* @__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
2980
  ] });
2833
2981
  }
2834
- function TabStrip({ paneId, itemIds, activeItemId }) {
2982
+ function TabStrip({ tabs, activeItemId }) {
2835
2983
  const strip = useRef6(null);
2836
- useIsoLayoutEffect2(() => {
2984
+ const [overflow, setOverflow] = useState5({ start: false, end: false });
2985
+ const measure = useRef6(() => void 0);
2986
+ measure.current = () => {
2987
+ const el = strip.current;
2988
+ if (!el) return;
2989
+ const start = el.scrollLeft > 1;
2990
+ const end = el.scrollLeft + el.clientWidth < el.scrollWidth - 1;
2991
+ setOverflow((prev) => prev.start === start && prev.end === end ? prev : { start, end });
2992
+ };
2993
+ const tabKey = tabs.map((tab) => tab.itemId).join("|");
2994
+ useIsoLayoutEffect3(() => {
2837
2995
  const el = strip.current;
2838
2996
  if (!el) return void 0;
2839
2997
  const reveal = () => {
2840
2998
  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;
2999
+ if (tab) {
3000
+ const left = tab.offsetLeft;
3001
+ const right = left + tab.offsetWidth;
3002
+ if (left < el.scrollLeft) el.scrollLeft = left;
3003
+ else if (right > el.scrollLeft + el.clientWidth) el.scrollLeft = right - el.clientWidth;
3004
+ }
3005
+ measure.current();
2846
3006
  };
2847
3007
  reveal();
2848
3008
  const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(reveal);
2849
3009
  observer?.observe(el);
2850
- const activeTab = el.querySelector('[role="tab"][aria-selected="true"]');
2851
- if (activeTab) observer?.observe(activeTab);
3010
+ for (const tab of el.querySelectorAll('[role="tab"]')) observer?.observe(tab);
2852
3011
  return () => observer?.disconnect();
2853
- }, [activeItemId, itemIds.length]);
2854
- return /* @__PURE__ */ jsx7(
3012
+ }, [activeItemId, tabKey]);
3013
+ const page = (direction) => {
3014
+ const el = strip.current;
3015
+ if (!el) return;
3016
+ const by = Math.max(48, Math.round(el.clientWidth * 0.8)) * direction;
3017
+ if (typeof el.scrollBy === "function") el.scrollBy({ left: by, behavior: "smooth" });
3018
+ else el.scrollLeft += by;
3019
+ };
3020
+ const overflowing = overflow.start || overflow.end;
3021
+ return /* @__PURE__ */ jsxs6(
2855
3022
  "div",
2856
3023
  {
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))
3024
+ className: "mxc-tabstrip",
3025
+ "data-overflow": overflowing ? "" : void 0,
3026
+ "data-overflow-start": overflow.start ? "" : void 0,
3027
+ "data-overflow-end": overflow.end ? "" : void 0,
3028
+ children: [
3029
+ overflowing ? /* @__PURE__ */ jsx7(CanvasHeaderButton, { className: "mxc-tabs-scroll", label: "Scroll tabs left", available: overflow.start, onClick: () => page(-1), children: /* @__PURE__ */ jsx7(ChevronLeftIcon, {}) }) : null,
3030
+ /* @__PURE__ */ jsx7(
3031
+ "div",
3032
+ {
3033
+ ref: strip,
3034
+ className: "mxc-tabs",
3035
+ role: "tablist",
3036
+ "aria-label": "Canvas tabs",
3037
+ onScroll: () => measure.current(),
3038
+ onWheel: (event) => {
3039
+ const el = strip.current;
3040
+ if (el && Math.abs(event.deltaY) > Math.abs(event.deltaX)) el.scrollLeft += event.deltaY;
3041
+ },
3042
+ children: tabs.map((tab) => /* @__PURE__ */ jsx7(ItemTab, { itemId: tab.itemId, paneId: tab.paneId, index: tab.index, active: tab.itemId === activeItemId }, tab.itemId))
3043
+ }
3044
+ ),
3045
+ overflowing ? /* @__PURE__ */ jsx7(CanvasHeaderButton, { className: "mxc-tabs-scroll", label: "Scroll tabs right", available: overflow.end, onClick: () => page(1), children: /* @__PURE__ */ jsx7(ChevronRightIcon, {}) }) : null
3046
+ ]
2866
3047
  }
2867
3048
  );
2868
3049
  }
@@ -2986,7 +3167,7 @@ function safeMenu(build) {
2986
3167
 
2987
3168
  // src/react/CanvasColumn.tsx
2988
3169
  import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2989
- var useIsoLayoutEffect3 = typeof window === "undefined" ? useEffect7 : useLayoutEffect4;
3170
+ var useIsoLayoutEffect4 = typeof window === "undefined" ? useEffect7 : useLayoutEffect5;
2990
3171
  function subscribeViewport(listener) {
2991
3172
  window.addEventListener("resize", listener);
2992
3173
  return () => window.removeEventListener("resize", listener);
@@ -3015,7 +3196,10 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
3015
3196
  const isFullscreen = useCanvasState((s) => selectCanvasIsFullscreen(s, windowId));
3016
3197
  const width = useCanvasState((s) => selectCanvasWidth(s, windowId));
3017
3198
  const layout = useCanvasState((s) => selectCanvasLayout(s, windowId));
3199
+ const focusedPaneId = useCanvasState((s) => selectCanvasFocusedPaneId(s, windowId));
3018
3200
  const region = useRegionWidth();
3201
+ const viewport = useViewportWidth();
3202
+ const singlePane = placement === "docked" && viewport > 0 && viewport <= CANVAS_PHONE_MAX_WIDTH;
3019
3203
  const [dragWidth, setDragWidth] = useState6(null);
3020
3204
  const [slots] = useState6(createCanvasSlots);
3021
3205
  const ref = useRef7(null);
@@ -3080,10 +3264,10 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
3080
3264
  }
3081
3265
  }
3082
3266
  ) : null,
3083
- /* @__PURE__ */ jsxs7(CanvasSlotsContext.Provider, { value: slots, children: [
3084
- /* @__PURE__ */ jsx8("div", { className: "mxc-layout", children: /* @__PURE__ */ jsx8(LayoutNodeView, { node: layout }) }),
3267
+ /* @__PURE__ */ jsx8(CanvasSlotsContext.Provider, { value: slots, children: /* @__PURE__ */ jsxs7(CanvasSinglePaneContext.Provider, { value: singlePane, children: [
3268
+ /* @__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
3269
  /* @__PURE__ */ jsx8(CanvasBodyLayer, {})
3086
- ] })
3270
+ ] }) })
3087
3271
  ] })
3088
3272
  }
3089
3273
  );
@@ -3131,13 +3315,17 @@ function WidthHandle({ width, region, onDrag }) {
3131
3315
  }
3132
3316
  );
3133
3317
  }
3318
+ function presentedPane(layout, focused) {
3319
+ const ids = listPaneIds(layout);
3320
+ return ids.find((id) => id === focused) ?? ids[0];
3321
+ }
3134
3322
  function LayoutNodeView({ node }) {
3135
3323
  if (node.type === "pane") return /* @__PURE__ */ jsx8(CanvasPaneView, { paneId: node.paneId });
3136
3324
  return /* @__PURE__ */ jsx8(SplitView, { node });
3137
3325
  }
3138
3326
  function useMeasuredWidth(ref, enabled) {
3139
3327
  const [width, setWidth] = useState6(0);
3140
- useIsoLayoutEffect3(() => {
3328
+ useIsoLayoutEffect4(() => {
3141
3329
  const el = ref.current;
3142
3330
  if (!enabled || !el) return;
3143
3331
  setWidth(el.getBoundingClientRect().width);
@@ -3233,6 +3421,7 @@ function CanvasToggle({
3233
3421
  ariaLabel: isOpen ? "Hide canvas" : "Show canvas",
3234
3422
  tooltip: `${isOpen ? "Hide canvas" : "Show canvas"}${hint ? ` (${hint})` : ""}`,
3235
3423
  icon: /* @__PURE__ */ jsx8(PanelRightIcon, {}),
3424
+ pressed: isOpen,
3236
3425
  onClick: () => controller.toggle()
3237
3426
  }
3238
3427
  ),