@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.cjs CHANGED
@@ -78,6 +78,7 @@ var import_react2 = require("react");
78
78
  var CANVAS_MAIN_WINDOW = "main";
79
79
  var CANVAS_LAUNCHER_KIND = "canvas.launcher";
80
80
  var CANVAS_HISTORY_LIMIT = 50;
81
+ var CANVAS_KEEP_ALIVE_DEPTH = 5;
81
82
  var CANVAS_MIN_WIDTH = 360;
82
83
  var CANVAS_DEFAULT_WIDTH = 640;
83
84
  var CANVAS_MIN_SPLIT_FRACTION = 0.12;
@@ -436,8 +437,10 @@ function sendHome(state, itemId, home, activate) {
436
437
  next = insertInto(next, homeFrame.focusedPaneId, itemId, activate);
437
438
  return activate ? withFrame(next, target, { focusedPaneId: homeFrame.focusedPaneId }) : next;
438
439
  }
439
- function newItem(kind, key, title, data, now, history) {
440
+ function newItem(kind, key, title, data, now, steps) {
440
441
  const id = canvasItemId(kind, key);
442
+ const history = steps?.history ?? [];
443
+ const kept = steps?.kept ?? [];
441
444
  return {
442
445
  id,
443
446
  kind,
@@ -446,12 +449,14 @@ function newItem(kind, key, title, data, now, history) {
446
449
  data,
447
450
  openedAt: now,
448
451
  updatedAt: now,
449
- instance: `${now.toString(36)}:${id}`,
450
- ...history && history.length > 0 ? { history: history.slice(-CANVAS_HISTORY_LIMIT) } : {}
452
+ // A reused instance is the SAME live body coming back (Back, or returning to a kept step).
453
+ instance: steps?.instance ?? `${now.toString(36)}:${id}`,
454
+ ...history.length > 0 ? { history: history.slice(-CANVAS_HISTORY_LIMIT) } : {},
455
+ ...kept.length > 0 ? { kept: kept.slice(-CANVAS_KEEP_ALIVE_DEPTH) } : {}
451
456
  };
452
457
  }
453
458
  function historyEntry(item) {
454
- return { kind: item.kind, key: item.key, title: item.title, data: item.data };
459
+ return { kind: item.kind, key: item.key, title: item.title, data: item.data, ...item.instance ? { instance: item.instance } : {} };
455
460
  }
456
461
  function replaceInPlace(state, current, next) {
457
462
  const pane = paneOf(state, current.id);
@@ -480,17 +485,25 @@ function sanitizeHistory(raw) {
480
485
  if (!entry || typeof entry !== "object") continue;
481
486
  const e = entry;
482
487
  if (typeof e.kind !== "string" || typeof e.key !== "string") continue;
483
- out.push({ kind: e.kind, key: e.key, title: typeof e.title === "string" ? e.title : null, data: e.data ?? null });
488
+ out.push({
489
+ kind: e.kind,
490
+ key: e.key,
491
+ title: typeof e.title === "string" ? e.title : null,
492
+ data: e.data ?? null,
493
+ ...typeof e.instance === "string" && e.instance ? { instance: e.instance } : {}
494
+ });
484
495
  }
485
496
  return out.slice(-CANVAS_HISTORY_LIMIT);
486
497
  }
487
498
  function sanitizeItem(item) {
488
499
  const history = sanitizeHistory(item.history);
489
- const { history: _history, instance, ...rest } = item;
500
+ const kept = sanitizeHistory(item.kept).slice(-CANVAS_KEEP_ALIVE_DEPTH);
501
+ const { history: _history, kept: _kept, instance, ...rest } = item;
490
502
  return {
491
503
  ...rest,
492
504
  ...typeof instance === "string" && instance ? { instance } : {},
493
- ...history.length > 0 ? { history } : {}
505
+ ...history.length > 0 ? { history } : {},
506
+ ...kept.length > 0 ? { kept } : {}
494
507
  };
495
508
  }
496
509
  function clampWidth(width) {
@@ -677,7 +690,22 @@ function canvasReducer(state = createInitialCanvasState(), action) {
677
690
  return current.kind === CANVAS_LAUNCHER_KIND ? canvasReducer(focused, canvasActions.closeItem(itemId)) : focused;
678
691
  }
679
692
  const history = [...current.history ?? [], historyEntry(current)];
680
- return replaceInPlace(state, current, newItem(to.kind, to.key, to.title ?? null, to.data ?? null, now, history));
693
+ const keptAll = current.kept ?? [];
694
+ const keptIndex = keptAll.findIndex((entry) => canvasItemId(entry.kind, entry.key) === targetId);
695
+ const revived = keptIndex >= 0 ? keptAll[keptIndex] : void 0;
696
+ const kept = keptAll.filter((_, i) => i !== keptIndex);
697
+ return replaceInPlace(
698
+ state,
699
+ current,
700
+ newItem(
701
+ to.kind,
702
+ to.key,
703
+ to.title !== void 0 ? to.title : revived?.title ?? null,
704
+ to.data !== void 0 ? to.data : revived?.data ?? null,
705
+ now,
706
+ { history, kept, instance: revived?.instance }
707
+ )
708
+ );
681
709
  }
682
710
  case `${P}back`: {
683
711
  const { itemId, now } = action.payload;
@@ -690,7 +718,12 @@ function canvasReducer(state = createInitialCanvasState(), action) {
690
718
  if (state.items[targetId]) {
691
719
  return focusExisting(canvasReducer(state, canvasActions.closeItem(itemId)), targetId);
692
720
  }
693
- return replaceInPlace(state, current, newItem(entry.kind, entry.key, entry.title, entry.data, now, history.slice(0, -1)));
721
+ const kept = [...(current.kept ?? []).filter((k) => canvasItemId(k.kind, k.key) !== itemId), historyEntry(current)];
722
+ return replaceInPlace(
723
+ state,
724
+ current,
725
+ newItem(entry.kind, entry.key, entry.title, entry.data, now, { history: history.slice(0, -1), kept, instance: entry.instance })
726
+ );
694
727
  }
695
728
  case `${P}closeItem`: {
696
729
  const { itemId } = action.payload;
@@ -1425,6 +1458,7 @@ var NO_HISTORY = Object.freeze([]);
1425
1458
  var selectCanvasCanGoBack = (s, itemId) => (s.items[itemId]?.history?.length ?? 0) > 0;
1426
1459
 
1427
1460
  // src/core/presentation.ts
1461
+ var CANVAS_PHONE_MAX_WIDTH = 767;
1428
1462
  var CANVAS_NARROW_WIDTH = 480;
1429
1463
  var SQUARE_BAND = 1.2;
1430
1464
  function canvasOrientation(width, height) {
@@ -1673,8 +1707,7 @@ function createLauncherEntryStore() {
1673
1707
  }
1674
1708
  };
1675
1709
  }
1676
- var PHONE_MAX_WIDTH = 767;
1677
- var isPhoneViewport = () => typeof window !== "undefined" && window.innerWidth <= PHONE_MAX_WIDTH;
1710
+ var isPhoneViewport = () => typeof window !== "undefined" && window.innerWidth <= CANVAS_PHONE_MAX_WIDTH;
1678
1711
  var isMacPlatform = () => typeof navigator !== "undefined" && /mac|iphone|ipad/i.test(navigator.userAgentData?.platform ?? navigator.platform ?? "");
1679
1712
  function escapeBelongsElsewhere(event) {
1680
1713
  if (event.defaultPrevented) return true;
@@ -1977,9 +2010,26 @@ var LayoutGridIcon = (p) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Glyph,
1977
2010
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { width: "7", height: "7", x: "14", y: "14", rx: "1" }),
1978
2011
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { width: "7", height: "7", x: "3", y: "14", rx: "1" })
1979
2012
  ] });
2013
+ var ChevronLeftIcon = (p) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Glyph, { ...p, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "m15 18-6-6 6-6" }) });
2014
+ var ChevronRightIcon = (p) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Glyph, { ...p, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "m9 18 6-6-6-6" }) });
1980
2015
 
1981
2016
  // src/react/chrome.tsx
1982
2017
  var import_jsx_runtime4 = require("react/jsx-runtime");
2018
+ var useIsoLayoutEffect = typeof window === "undefined" ? import_react3.useEffect : import_react3.useLayoutEffect;
2019
+ function useTruncated(title) {
2020
+ const ref = (0, import_react3.useRef)(null);
2021
+ const [truncated, setTruncated] = (0, import_react3.useState)(false);
2022
+ useIsoLayoutEffect(() => {
2023
+ const el = ref.current;
2024
+ if (!el) return void 0;
2025
+ const check = () => setTruncated(el.scrollWidth - el.clientWidth > 1);
2026
+ check();
2027
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(check);
2028
+ observer?.observe(el);
2029
+ return () => observer?.disconnect();
2030
+ }, [title]);
2031
+ return { ref, truncated };
2032
+ }
1983
2033
  var cx = (...names) => names.filter(Boolean).join(" ");
1984
2034
  function useTooltip(text) {
1985
2035
  const internals = useOptionalCanvasInternals();
@@ -2119,6 +2169,7 @@ function CanvasPanelEmpty({ children, className }) {
2119
2169
  }
2120
2170
  var CanvasTab = (0, import_react3.forwardRef)(function CanvasTab2({ title, icon, note, active, onSelect, onClose, className, onKeyDown, onClick, ...rest }, ref) {
2121
2171
  const shown = title || "New tab";
2172
+ const name = useTruncated(shown);
2122
2173
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2123
2174
  "div",
2124
2175
  {
@@ -2128,6 +2179,7 @@ var CanvasTab = (0, import_react3.forwardRef)(function CanvasTab2({ title, icon,
2128
2179
  tabIndex: active ? 0 : -1,
2129
2180
  "data-slot": "browser-tab",
2130
2181
  "data-active": active ? "" : void 0,
2182
+ "data-truncated": name.truncated ? "" : void 0,
2131
2183
  title: shown,
2132
2184
  className: cx("mxc-tab", className),
2133
2185
  ...rest,
@@ -2145,7 +2197,7 @@ var CanvasTab = (0, import_react3.forwardRef)(function CanvasTab2({ title, icon,
2145
2197
  },
2146
2198
  children: [
2147
2199
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "mxc-tab-icon", children: icon }),
2148
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "mxc-tab-title", children: shown }),
2200
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { ref: name.ref, className: "mxc-tab-title", children: shown }),
2149
2201
  note ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "mxc-tab-note", "data-slot": "browser-tab-note", children: note }) : null,
2150
2202
  onClose ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2151
2203
  "button",
@@ -2312,6 +2364,21 @@ function useChoices() {
2312
2364
  }
2313
2365
  return { contextual, global };
2314
2366
  }
2367
+ var FOLLOW_UP_MS = 600;
2368
+ function swallowFollowUpClicks() {
2369
+ if (typeof window === "undefined") return;
2370
+ const types = ["pointerdown", "mousedown", "pointerup", "mouseup", "click", "dblclick"];
2371
+ const swallow = (event) => {
2372
+ if (event.detail >= 2 || event.type === "dblclick") {
2373
+ event.preventDefault();
2374
+ event.stopPropagation();
2375
+ }
2376
+ };
2377
+ for (const type of types) window.addEventListener(type, swallow, true);
2378
+ setTimeout(() => {
2379
+ for (const type of types) window.removeEventListener(type, swallow, true);
2380
+ }, FOLLOW_UP_MS);
2381
+ }
2315
2382
  function LauncherGrid({ onChoose }) {
2316
2383
  const { contextual, global } = useChoices();
2317
2384
  if (contextual.length === 0 && global.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CanvasPanelEmpty, { children: "Nothing to open" });
@@ -2319,10 +2386,23 @@ function LauncherGrid({ onChoose }) {
2319
2386
  label ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h3", { className: "mxc-launcher-heading", children: label }) : null,
2320
2387
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "mxc-launcher-grid", children: choices.map((choice) => {
2321
2388
  const Icon = choice.icon;
2322
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("button", { type: "button", className: "mxc-launcher-item", onClick: () => onChoose(choice.input), children: [
2323
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Icon, { className: "mxc-launcher-icon" }),
2324
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: choice.label })
2325
- ] }, choice.id);
2389
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2390
+ "button",
2391
+ {
2392
+ type: "button",
2393
+ className: "mxc-launcher-item",
2394
+ onClick: (event) => {
2395
+ if (event.detail >= 2) return;
2396
+ swallowFollowUpClicks();
2397
+ onChoose(choice.input);
2398
+ },
2399
+ children: [
2400
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Icon, { className: "mxc-launcher-icon" }),
2401
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: choice.label })
2402
+ ]
2403
+ },
2404
+ choice.id
2405
+ );
2326
2406
  }) })
2327
2407
  ] });
2328
2408
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "mxc-launcher", children: [
@@ -2435,6 +2515,10 @@ var CanvasSlotsContext = (0, import_react6.createContext)(null);
2435
2515
  function useCanvasSlots() {
2436
2516
  return (0, import_react6.useContext)(CanvasSlotsContext);
2437
2517
  }
2518
+ var CanvasSinglePaneContext = (0, import_react6.createContext)(false);
2519
+ function useCanvasSinglePane() {
2520
+ return (0, import_react6.useContext)(CanvasSinglePaneContext);
2521
+ }
2438
2522
  function useSlotSize(slots, paneId) {
2439
2523
  return (0, import_react6.useSyncExternalStore)(
2440
2524
  slots.subscribe,
@@ -2445,7 +2529,7 @@ function useSlotSize(slots, paneId) {
2445
2529
 
2446
2530
  // src/react/bodies.tsx
2447
2531
  var import_jsx_runtime6 = require("react/jsx-runtime");
2448
- var useIsoLayoutEffect = typeof window === "undefined" ? import_react7.useEffect : import_react7.useLayoutEffect;
2532
+ var useIsoLayoutEffect2 = typeof window === "undefined" ? import_react7.useEffect : import_react7.useLayoutEffect;
2449
2533
  var LAZY = /* @__PURE__ */ Symbol.for("ai-matrx.canvas.lazy");
2450
2534
  function kindComponent(kind) {
2451
2535
  if (kind.component) return kind.component;
@@ -2466,19 +2550,36 @@ function usePanePresentation(slots, paneId, isFullscreen, paneCount) {
2466
2550
  var hostKey = (item) => item.instance ?? item.id;
2467
2551
  var SEP2 = "";
2468
2552
  var ROW = "";
2553
+ function bornAt(instance, fallback) {
2554
+ const stamp = instance ? Number.parseInt(instance.slice(0, instance.indexOf(":")), 36) : Number.NaN;
2555
+ return Number.isFinite(stamp) ? stamp : fallback;
2556
+ }
2557
+ function keptCanvasSteps(item) {
2558
+ const back = (item.history ?? []).slice(-CANVAS_KEEP_ALIVE_DEPTH);
2559
+ const forward = (item.kept ?? []).slice(-CANVAS_KEEP_ALIVE_DEPTH);
2560
+ return [...back, ...forward].filter((entry) => !!entry.instance);
2561
+ }
2469
2562
  function layerSignature(state, windowId, onlyPane) {
2470
2563
  const frame2 = canvasWindowFrame(state, windowId);
2471
2564
  if (!frame2) return "";
2472
2565
  const rows = [];
2566
+ const seen = /* @__PURE__ */ new Set();
2567
+ const add = (hostId, born, fields) => {
2568
+ if (seen.has(hostId)) return;
2569
+ seen.add(hostId);
2570
+ rows.push({ born, hostId, row: [hostId, ...fields].join(SEP2) });
2571
+ };
2473
2572
  const paneIds = listPaneIds(frame2.layout);
2474
2573
  for (const paneId of onlyPane ? [onlyPane] : paneIds) {
2475
2574
  const pane = state.panes[paneId];
2476
2575
  for (const itemId of pane?.itemIds ?? []) {
2477
2576
  const item = state.items[itemId];
2478
- if (item) rows.push({ item, row: [hostKey(item), itemId, paneId, pane?.activeItemId === itemId ? "1" : "0"].join(SEP2) });
2577
+ if (!item) continue;
2578
+ add(hostKey(item), bornAt(item.instance, item.openedAt), [itemId, paneId, pane?.activeItemId === itemId ? "1" : "0", "0"]);
2579
+ for (const step of keptCanvasSteps(item)) add(step.instance, bornAt(step.instance, item.openedAt), [itemId, paneId, "0", "1"]);
2479
2580
  }
2480
2581
  }
2481
- rows.sort((a, b) => a.item.openedAt - b.item.openedAt || hostKey(a.item).localeCompare(hostKey(b.item)));
2582
+ rows.sort((a, b) => a.born - b.born || a.hostId.localeCompare(b.hostId));
2482
2583
  const head = [frame2.isOpen ? "1" : "0", frame2.isFullscreen ? "1" : "0", frame2.focusedPaneId, String(paneIds.length)].join(SEP2);
2483
2584
  return [head, ...rows.map((r) => r.row)].join(ROW);
2484
2585
  }
@@ -2492,8 +2593,8 @@ function parseSignature(signature) {
2492
2593
  focusedPaneId,
2493
2594
  paneCount: Number(count) || 1,
2494
2595
  placed: rows.map((row) => {
2495
- const [hostId = "", itemId = "", paneId = "", active] = row.split(SEP2);
2496
- return { hostId, itemId, paneId, active: active === "1" };
2596
+ const [hostId = "", itemId = "", paneId = "", active, kept] = row.split(SEP2);
2597
+ return { hostId, itemId, paneId, active: active === "1", kept: kept === "1" };
2497
2598
  })
2498
2599
  };
2499
2600
  }
@@ -2540,21 +2641,23 @@ function CanvasBodyLayer() {
2540
2641
  const { windowId, layer } = useCanvasInternals();
2541
2642
  const slots = useCanvasSlots();
2542
2643
  const signature = useCanvasState((s) => layerSignature(s, windowId));
2644
+ const single = useCanvasSinglePane();
2543
2645
  const root = (0, import_react7.useRef)(null);
2544
2646
  const [placement] = (0, import_react7.useState)(
2545
2647
  () => slots ? createPlacement(() => root.current, slots, () => layer.native.remeasure()) : null
2546
2648
  );
2547
- useIsoLayoutEffect(() => placement?.place());
2548
- useIsoLayoutEffect(() => slots && placement ? slots.subscribe(placement.place) : void 0, [slots, placement]);
2649
+ useIsoLayoutEffect2(() => placement?.place());
2650
+ useIsoLayoutEffect2(() => slots && placement ? slots.subscribe(placement.place) : void 0, [slots, placement]);
2549
2651
  const shape = parseSignature(signature);
2550
2652
  if (!slots || !placement || !shape) return null;
2551
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { ref: root, className: "mxc-bodies", "data-mxc-bodies": windowId, children: shape.placed.map(({ hostId, itemId, paneId, active }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2653
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { ref: root, className: "mxc-bodies", "data-mxc-bodies": windowId, children: shape.placed.map(({ hostId, itemId, paneId, active, kept }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2552
2654
  BodyHost,
2553
2655
  {
2554
2656
  hostId,
2555
2657
  itemId,
2658
+ kept,
2556
2659
  paneId,
2557
- visible: shape.isOpen && active,
2660
+ visible: shape.isOpen && active && (!single || paneId === shape.focusedPaneId),
2558
2661
  focused: active && shape.focusedPaneId === paneId,
2559
2662
  isFullscreen: shape.isFullscreen,
2560
2663
  paneCount: shape.paneCount,
@@ -2568,11 +2671,12 @@ function CanvasPaneInlineBodies({ paneId, slots }) {
2568
2671
  const { windowId } = useCanvasInternals();
2569
2672
  const shape = parseSignature(useCanvasState((s) => layerSignature(s, windowId, paneId)));
2570
2673
  if (!shape) return null;
2571
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: shape.placed.map(({ hostId, itemId, active }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2674
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: shape.placed.map(({ hostId, itemId, active, kept }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2572
2675
  BodyHost,
2573
2676
  {
2574
2677
  hostId,
2575
2678
  itemId,
2679
+ kept,
2576
2680
  paneId,
2577
2681
  visible: active,
2578
2682
  focused: active && shape.focusedPaneId === paneId,
@@ -2584,9 +2688,22 @@ function CanvasPaneInlineBodies({ paneId, slots }) {
2584
2688
  hostId
2585
2689
  )) });
2586
2690
  }
2691
+ function stepAsItem(step, openedAt) {
2692
+ return {
2693
+ id: canvasItemId(step.kind, step.key),
2694
+ kind: step.kind,
2695
+ key: step.key,
2696
+ title: step.title,
2697
+ data: step.data,
2698
+ openedAt,
2699
+ updatedAt: openedAt,
2700
+ ...step.instance ? { instance: step.instance } : {}
2701
+ };
2702
+ }
2587
2703
  var BodyHost = (0, import_react7.memo)(function BodyHost2({
2588
2704
  hostId,
2589
2705
  itemId,
2706
+ kept,
2590
2707
  paneId,
2591
2708
  visible,
2592
2709
  focused,
@@ -2597,7 +2714,14 @@ var BodyHost = (0, import_react7.memo)(function BodyHost2({
2597
2714
  }) {
2598
2715
  const canvas = useCanvas();
2599
2716
  const { windowId, ports } = useCanvasInternals();
2600
- const item = useCanvasState((s) => s.items[itemId]);
2717
+ const live = useCanvasState((s) => kept ? void 0 : s.items[itemId]);
2718
+ const step = useCanvasState((s) => {
2719
+ const ownerItem = kept ? s.items[itemId] : void 0;
2720
+ return ownerItem ? keptCanvasSteps(ownerItem).find((e) => e.instance === hostId) : void 0;
2721
+ });
2722
+ const owner = useCanvasState((s) => kept ? s.items[itemId]?.openedAt : void 0);
2723
+ const keptItem = (0, import_react7.useMemo)(() => step ? stepAsItem(step, owner ?? 0) : void 0, [step, owner]);
2724
+ const item = kept ? keptItem : live;
2601
2725
  const kind = useItemKind(item?.kind ?? "");
2602
2726
  const element = (0, import_react7.useRef)(null);
2603
2727
  const [everShown, setEverShown] = (0, import_react7.useState)(visible);
@@ -2605,7 +2729,7 @@ var BodyHost = (0, import_react7.memo)(function BodyHost2({
2605
2729
  const mounted = kind?.unmountWhenHidden ? visible : everShown;
2606
2730
  const presentation = usePanePresentation(slots, paneId, isFullscreen, paneCount);
2607
2731
  const drop = usePaneDrop(paneId, () => slots.section(paneId));
2608
- useIsoLayoutEffect(() => {
2732
+ useIsoLayoutEffect2(() => {
2609
2733
  const el = element.current;
2610
2734
  if (!placement || !el) return void 0;
2611
2735
  return placement.register(hostId, { element: el, paneId });
@@ -2621,6 +2745,7 @@ var BodyHost = (0, import_react7.memo)(function BodyHost2({
2621
2745
  "data-visible": visible ? "true" : "false",
2622
2746
  "data-kind": item.kind,
2623
2747
  "data-item": item.id,
2748
+ "data-kept": kept ? "" : void 0,
2624
2749
  "data-pane": paneId,
2625
2750
  inert: !visible,
2626
2751
  "aria-hidden": visible ? void 0 : true,
@@ -2702,8 +2827,8 @@ function NativeBody({ itemId, windowId, visible, children }) {
2702
2827
  const rect = { ...toCanvasRect(box), devicePixelRatio: window.devicePixelRatio || 1 };
2703
2828
  layer.native.report(itemId, rect, { visible: !layer.isBlocked(), windowId });
2704
2829
  };
2705
- useIsoLayoutEffect(() => measure.current());
2706
- useIsoLayoutEffect(() => {
2830
+ useIsoLayoutEffect2(() => measure.current());
2831
+ useIsoLayoutEffect2(() => {
2707
2832
  const el = ref.current;
2708
2833
  const run = () => measure.current();
2709
2834
  const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(run);
@@ -2759,7 +2884,7 @@ var ItemBoundary = class extends import_react7.Component {
2759
2884
  var import_react8 = require("react");
2760
2885
  var Menu = __toESM(require("@radix-ui/react-dropdown-menu"), 1);
2761
2886
  var import_jsx_runtime7 = require("react/jsx-runtime");
2762
- var useIsoLayoutEffect2 = typeof window === "undefined" ? import_react8.useEffect : import_react8.useLayoutEffect;
2887
+ var useIsoLayoutEffect3 = typeof window === "undefined" ? import_react8.useEffect : import_react8.useLayoutEffect;
2763
2888
  function itemTitle(item, kind) {
2764
2889
  if (item.title) return item.title;
2765
2890
  if (item.kind === CANVAS_LAUNCHER_KIND) return "New tab";
@@ -2773,6 +2898,25 @@ function itemTitle(item, kind) {
2773
2898
  return kind?.label ?? item.kind;
2774
2899
  }
2775
2900
  var isMac = () => typeof navigator !== "undefined" && /mac|iphone|ipad/i.test(navigator.platform ?? "");
2901
+ function windowTabsSignature(state, windowId) {
2902
+ const frame2 = canvasWindowFrame(state, windowId);
2903
+ if (!frame2) return "";
2904
+ const rows = [];
2905
+ for (const paneId of listPaneIds(frame2.layout)) {
2906
+ for (const itemId of state.panes[paneId]?.itemIds ?? []) rows.push(`${itemId}${paneId}`);
2907
+ }
2908
+ return rows.join("");
2909
+ }
2910
+ function parseWindowTabs(signature) {
2911
+ if (!signature) return [];
2912
+ const perPane = /* @__PURE__ */ new Map();
2913
+ return signature.split("").map((row) => {
2914
+ const [itemId = "", paneId = ""] = row.split("");
2915
+ const index = perPane.get(paneId) ?? 0;
2916
+ perPane.set(paneId, index + 1);
2917
+ return { itemId, paneId, index };
2918
+ });
2919
+ }
2776
2920
  function CanvasPaneView({ paneId }) {
2777
2921
  const canvas = useCanvas();
2778
2922
  const { windowId, tabStrip } = useCanvasInternals();
@@ -2791,11 +2935,13 @@ function CanvasPaneView({ paneId }) {
2791
2935
  const role = useCanvasState((s) => windowId === CANVAS_MAIN_WINDOW ? "main" : s.windows[windowId]?.role ?? "window");
2792
2936
  const canGoBack = useCanvasState((s) => active ? selectCanvasCanGoBack(s, active.id) : false);
2793
2937
  const kind = useItemKind(active?.kind ?? "");
2938
+ const singlePane = useCanvasSinglePane();
2939
+ const windowTabs = useCanvasState((s) => singlePane ? windowTabsSignature(s, windowId) : "");
2794
2940
  const sectionRef = (0, import_react8.useRef)(null);
2795
2941
  const bodyRef = (0, import_react8.useRef)(null);
2796
2942
  const presentation = usePanePresentation(slots, paneId, isFullscreen, paneCount);
2797
2943
  const drop = usePaneDrop(paneId, () => sectionRef.current);
2798
- useIsoLayoutEffect2(() => {
2944
+ useIsoLayoutEffect3(() => {
2799
2945
  const body = bodyRef.current;
2800
2946
  const section = sectionRef.current;
2801
2947
  if (body) slots.set(paneId, body);
@@ -2810,7 +2956,8 @@ function CanvasPaneView({ paneId }) {
2810
2956
  const Leading = kind?.HeaderLeading;
2811
2957
  const Action = kind?.HeaderAction;
2812
2958
  const Toolbar = kind?.Toolbar;
2813
- const single = tabStrip === "auto" && pane.itemIds.length === 1 && active;
2959
+ const tabs = singlePane ? parseWindowTabs(windowTabs) : pane.itemIds.map((itemId, index) => ({ itemId, paneId, index }));
2960
+ const single = tabStrip === "auto" && tabs.length === 1 && active;
2814
2961
  const closeLabel = role === "pop-out" && paneCount === 1 ? "Close window" : paneCount > 1 ? "Close pane" : "Close canvas";
2815
2962
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2816
2963
  CanvasPanel,
@@ -2823,7 +2970,7 @@ function CanvasPaneView({ paneId }) {
2823
2970
  "data-mxc-pane": paneId,
2824
2971
  header: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
2825
2972
  canGoBack && active ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CanvasHeaderButton, { className: "mxc-back", label: "Back", onClick: () => void canvas.back(active.id), children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ArrowLeftIcon, {}) }) : null,
2826
- single && active ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SingleTitle, { item: active, kind }) : pane.itemIds.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TabStrip, { paneId, itemIds: pane.itemIds, activeItemId: pane.activeItemId }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "mxc-panel-title", children: "New tab" }),
2973
+ 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" }),
2827
2974
  /* @__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, {}) })
2828
2975
  ] }),
2829
2976
  leading: Leading && kindProps ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Leading, { ...kindProps }) : null,
@@ -2872,38 +3019,71 @@ function SingleTitle({ item, kind }) {
2872
3019
  /* @__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, {}) })
2873
3020
  ] });
2874
3021
  }
2875
- function TabStrip({ paneId, itemIds, activeItemId }) {
3022
+ function TabStrip({ tabs, activeItemId }) {
2876
3023
  const strip = (0, import_react8.useRef)(null);
2877
- useIsoLayoutEffect2(() => {
3024
+ const [overflow, setOverflow] = (0, import_react8.useState)({ start: false, end: false });
3025
+ const measure = (0, import_react8.useRef)(() => void 0);
3026
+ measure.current = () => {
3027
+ const el = strip.current;
3028
+ if (!el) return;
3029
+ const start = el.scrollLeft > 1;
3030
+ const end = el.scrollLeft + el.clientWidth < el.scrollWidth - 1;
3031
+ setOverflow((prev) => prev.start === start && prev.end === end ? prev : { start, end });
3032
+ };
3033
+ const tabKey = tabs.map((tab) => tab.itemId).join("|");
3034
+ useIsoLayoutEffect3(() => {
2878
3035
  const el = strip.current;
2879
3036
  if (!el) return void 0;
2880
3037
  const reveal = () => {
2881
3038
  const tab = el.querySelector('[role="tab"][aria-selected="true"]');
2882
- if (!tab) return;
2883
- const left = tab.offsetLeft;
2884
- const right = left + tab.offsetWidth;
2885
- if (left < el.scrollLeft) el.scrollLeft = left;
2886
- else if (right > el.scrollLeft + el.clientWidth) el.scrollLeft = right - el.clientWidth;
3039
+ if (tab) {
3040
+ const left = tab.offsetLeft;
3041
+ const right = left + tab.offsetWidth;
3042
+ if (left < el.scrollLeft) el.scrollLeft = left;
3043
+ else if (right > el.scrollLeft + el.clientWidth) el.scrollLeft = right - el.clientWidth;
3044
+ }
3045
+ measure.current();
2887
3046
  };
2888
3047
  reveal();
2889
3048
  const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(reveal);
2890
3049
  observer?.observe(el);
2891
- const activeTab = el.querySelector('[role="tab"][aria-selected="true"]');
2892
- if (activeTab) observer?.observe(activeTab);
3050
+ for (const tab of el.querySelectorAll('[role="tab"]')) observer?.observe(tab);
2893
3051
  return () => observer?.disconnect();
2894
- }, [activeItemId, itemIds.length]);
2895
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3052
+ }, [activeItemId, tabKey]);
3053
+ const page = (direction) => {
3054
+ const el = strip.current;
3055
+ if (!el) return;
3056
+ const by = Math.max(48, Math.round(el.clientWidth * 0.8)) * direction;
3057
+ if (typeof el.scrollBy === "function") el.scrollBy({ left: by, behavior: "smooth" });
3058
+ else el.scrollLeft += by;
3059
+ };
3060
+ const overflowing = overflow.start || overflow.end;
3061
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2896
3062
  "div",
2897
3063
  {
2898
- ref: strip,
2899
- className: "mxc-tabs",
2900
- role: "tablist",
2901
- "aria-label": "Canvas tabs",
2902
- onWheel: (event) => {
2903
- const el = strip.current;
2904
- if (el && Math.abs(event.deltaY) > Math.abs(event.deltaX)) el.scrollLeft += event.deltaY;
2905
- },
2906
- children: itemIds.map((itemId, index) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ItemTab, { itemId, paneId, index, active: itemId === activeItemId }, itemId))
3064
+ className: "mxc-tabstrip",
3065
+ "data-overflow": overflowing ? "" : void 0,
3066
+ "data-overflow-start": overflow.start ? "" : void 0,
3067
+ "data-overflow-end": overflow.end ? "" : void 0,
3068
+ children: [
3069
+ overflowing ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CanvasHeaderButton, { className: "mxc-tabs-scroll", label: "Scroll tabs left", available: overflow.start, onClick: () => page(-1), children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChevronLeftIcon, {}) }) : null,
3070
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3071
+ "div",
3072
+ {
3073
+ ref: strip,
3074
+ className: "mxc-tabs",
3075
+ role: "tablist",
3076
+ "aria-label": "Canvas tabs",
3077
+ onScroll: () => measure.current(),
3078
+ onWheel: (event) => {
3079
+ const el = strip.current;
3080
+ if (el && Math.abs(event.deltaY) > Math.abs(event.deltaX)) el.scrollLeft += event.deltaY;
3081
+ },
3082
+ children: tabs.map((tab) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ItemTab, { itemId: tab.itemId, paneId: tab.paneId, index: tab.index, active: tab.itemId === activeItemId }, tab.itemId))
3083
+ }
3084
+ ),
3085
+ overflowing ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CanvasHeaderButton, { className: "mxc-tabs-scroll", label: "Scroll tabs right", available: overflow.end, onClick: () => page(1), children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChevronRightIcon, {}) }) : null
3086
+ ]
2907
3087
  }
2908
3088
  );
2909
3089
  }
@@ -3027,7 +3207,7 @@ function safeMenu(build) {
3027
3207
 
3028
3208
  // src/react/CanvasColumn.tsx
3029
3209
  var import_jsx_runtime8 = require("react/jsx-runtime");
3030
- var useIsoLayoutEffect3 = typeof window === "undefined" ? import_react9.useEffect : import_react9.useLayoutEffect;
3210
+ var useIsoLayoutEffect4 = typeof window === "undefined" ? import_react9.useEffect : import_react9.useLayoutEffect;
3031
3211
  function subscribeViewport(listener) {
3032
3212
  window.addEventListener("resize", listener);
3033
3213
  return () => window.removeEventListener("resize", listener);
@@ -3056,7 +3236,10 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
3056
3236
  const isFullscreen = useCanvasState((s) => selectCanvasIsFullscreen(s, windowId));
3057
3237
  const width = useCanvasState((s) => selectCanvasWidth(s, windowId));
3058
3238
  const layout = useCanvasState((s) => selectCanvasLayout(s, windowId));
3239
+ const focusedPaneId = useCanvasState((s) => selectCanvasFocusedPaneId(s, windowId));
3059
3240
  const region = useRegionWidth();
3241
+ const viewport = useViewportWidth();
3242
+ const singlePane = placement === "docked" && viewport > 0 && viewport <= CANVAS_PHONE_MAX_WIDTH;
3060
3243
  const [dragWidth, setDragWidth] = (0, import_react9.useState)(null);
3061
3244
  const [slots] = (0, import_react9.useState)(createCanvasSlots);
3062
3245
  const ref = (0, import_react9.useRef)(null);
@@ -3121,10 +3304,10 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
3121
3304
  }
3122
3305
  }
3123
3306
  ) : null,
3124
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(CanvasSlotsContext.Provider, { value: slots, children: [
3125
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "mxc-layout", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(LayoutNodeView, { node: layout }) }),
3307
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CanvasSlotsContext.Provider, { value: slots, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(CanvasSinglePaneContext.Provider, { value: singlePane, children: [
3308
+ /* @__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: presentedPane(layout, focusedPaneId) }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(LayoutNodeView, { node: layout }) }),
3126
3309
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CanvasBodyLayer, {})
3127
- ] })
3310
+ ] }) })
3128
3311
  ] })
3129
3312
  }
3130
3313
  );
@@ -3172,13 +3355,17 @@ function WidthHandle({ width, region, onDrag }) {
3172
3355
  }
3173
3356
  );
3174
3357
  }
3358
+ function presentedPane(layout, focused) {
3359
+ const ids = listPaneIds(layout);
3360
+ return ids.find((id) => id === focused) ?? ids[0];
3361
+ }
3175
3362
  function LayoutNodeView({ node }) {
3176
3363
  if (node.type === "pane") return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CanvasPaneView, { paneId: node.paneId });
3177
3364
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SplitView, { node });
3178
3365
  }
3179
3366
  function useMeasuredWidth(ref, enabled) {
3180
3367
  const [width, setWidth] = (0, import_react9.useState)(0);
3181
- useIsoLayoutEffect3(() => {
3368
+ useIsoLayoutEffect4(() => {
3182
3369
  const el = ref.current;
3183
3370
  if (!enabled || !el) return;
3184
3371
  setWidth(el.getBoundingClientRect().width);
@@ -3274,6 +3461,7 @@ function CanvasToggle({
3274
3461
  ariaLabel: isOpen ? "Hide canvas" : "Show canvas",
3275
3462
  tooltip: `${isOpen ? "Hide canvas" : "Show canvas"}${hint ? ` (${hint})` : ""}`,
3276
3463
  icon: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PanelRightIcon, {}),
3464
+ pressed: isOpen,
3277
3465
  onClick: () => controller.toggle()
3278
3466
  }
3279
3467
  ),