@ai-matrx/canvas 0.5.2 → 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.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,6 +2010,8 @@ 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");
@@ -2312,6 +2347,21 @@ function useChoices() {
2312
2347
  }
2313
2348
  return { contextual, global };
2314
2349
  }
2350
+ var FOLLOW_UP_MS = 600;
2351
+ function swallowFollowUpClicks() {
2352
+ if (typeof window === "undefined") return;
2353
+ const types = ["pointerdown", "mousedown", "pointerup", "mouseup", "click", "dblclick"];
2354
+ const swallow = (event) => {
2355
+ if (event.detail >= 2 || event.type === "dblclick") {
2356
+ event.preventDefault();
2357
+ event.stopPropagation();
2358
+ }
2359
+ };
2360
+ for (const type of types) window.addEventListener(type, swallow, true);
2361
+ setTimeout(() => {
2362
+ for (const type of types) window.removeEventListener(type, swallow, true);
2363
+ }, FOLLOW_UP_MS);
2364
+ }
2315
2365
  function LauncherGrid({ onChoose }) {
2316
2366
  const { contextual, global } = useChoices();
2317
2367
  if (contextual.length === 0 && global.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CanvasPanelEmpty, { children: "Nothing to open" });
@@ -2319,10 +2369,23 @@ function LauncherGrid({ onChoose }) {
2319
2369
  label ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h3", { className: "mxc-launcher-heading", children: label }) : null,
2320
2370
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "mxc-launcher-grid", children: choices.map((choice) => {
2321
2371
  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);
2372
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2373
+ "button",
2374
+ {
2375
+ type: "button",
2376
+ className: "mxc-launcher-item",
2377
+ onClick: (event) => {
2378
+ if (event.detail >= 2) return;
2379
+ swallowFollowUpClicks();
2380
+ onChoose(choice.input);
2381
+ },
2382
+ children: [
2383
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Icon, { className: "mxc-launcher-icon" }),
2384
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: choice.label })
2385
+ ]
2386
+ },
2387
+ choice.id
2388
+ );
2326
2389
  }) })
2327
2390
  ] });
2328
2391
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "mxc-launcher", children: [
@@ -2435,6 +2498,10 @@ var CanvasSlotsContext = (0, import_react6.createContext)(null);
2435
2498
  function useCanvasSlots() {
2436
2499
  return (0, import_react6.useContext)(CanvasSlotsContext);
2437
2500
  }
2501
+ var CanvasSinglePaneContext = (0, import_react6.createContext)(false);
2502
+ function useCanvasSinglePane() {
2503
+ return (0, import_react6.useContext)(CanvasSinglePaneContext);
2504
+ }
2438
2505
  function useSlotSize(slots, paneId) {
2439
2506
  return (0, import_react6.useSyncExternalStore)(
2440
2507
  slots.subscribe,
@@ -2466,19 +2533,36 @@ function usePanePresentation(slots, paneId, isFullscreen, paneCount) {
2466
2533
  var hostKey = (item) => item.instance ?? item.id;
2467
2534
  var SEP2 = "";
2468
2535
  var ROW = "";
2536
+ function bornAt(instance, fallback) {
2537
+ const stamp = instance ? Number.parseInt(instance.slice(0, instance.indexOf(":")), 36) : Number.NaN;
2538
+ return Number.isFinite(stamp) ? stamp : fallback;
2539
+ }
2540
+ function keptCanvasSteps(item) {
2541
+ const back = (item.history ?? []).slice(-CANVAS_KEEP_ALIVE_DEPTH);
2542
+ const forward = (item.kept ?? []).slice(-CANVAS_KEEP_ALIVE_DEPTH);
2543
+ return [...back, ...forward].filter((entry) => !!entry.instance);
2544
+ }
2469
2545
  function layerSignature(state, windowId, onlyPane) {
2470
2546
  const frame2 = canvasWindowFrame(state, windowId);
2471
2547
  if (!frame2) return "";
2472
2548
  const rows = [];
2549
+ const seen = /* @__PURE__ */ new Set();
2550
+ const add = (hostId, born, fields) => {
2551
+ if (seen.has(hostId)) return;
2552
+ seen.add(hostId);
2553
+ rows.push({ born, hostId, row: [hostId, ...fields].join(SEP2) });
2554
+ };
2473
2555
  const paneIds = listPaneIds(frame2.layout);
2474
2556
  for (const paneId of onlyPane ? [onlyPane] : paneIds) {
2475
2557
  const pane = state.panes[paneId];
2476
2558
  for (const itemId of pane?.itemIds ?? []) {
2477
2559
  const item = state.items[itemId];
2478
- if (item) rows.push({ item, row: [hostKey(item), itemId, paneId, pane?.activeItemId === itemId ? "1" : "0"].join(SEP2) });
2560
+ if (!item) continue;
2561
+ add(hostKey(item), bornAt(item.instance, item.openedAt), [itemId, paneId, pane?.activeItemId === itemId ? "1" : "0", "0"]);
2562
+ for (const step of keptCanvasSteps(item)) add(step.instance, bornAt(step.instance, item.openedAt), [itemId, paneId, "0", "1"]);
2479
2563
  }
2480
2564
  }
2481
- rows.sort((a, b) => a.item.openedAt - b.item.openedAt || hostKey(a.item).localeCompare(hostKey(b.item)));
2565
+ rows.sort((a, b) => a.born - b.born || a.hostId.localeCompare(b.hostId));
2482
2566
  const head = [frame2.isOpen ? "1" : "0", frame2.isFullscreen ? "1" : "0", frame2.focusedPaneId, String(paneIds.length)].join(SEP2);
2483
2567
  return [head, ...rows.map((r) => r.row)].join(ROW);
2484
2568
  }
@@ -2492,8 +2576,8 @@ function parseSignature(signature) {
2492
2576
  focusedPaneId,
2493
2577
  paneCount: Number(count) || 1,
2494
2578
  placed: rows.map((row) => {
2495
- const [hostId = "", itemId = "", paneId = "", active] = row.split(SEP2);
2496
- return { hostId, itemId, paneId, active: active === "1" };
2579
+ const [hostId = "", itemId = "", paneId = "", active, kept] = row.split(SEP2);
2580
+ return { hostId, itemId, paneId, active: active === "1", kept: kept === "1" };
2497
2581
  })
2498
2582
  };
2499
2583
  }
@@ -2540,6 +2624,7 @@ function CanvasBodyLayer() {
2540
2624
  const { windowId, layer } = useCanvasInternals();
2541
2625
  const slots = useCanvasSlots();
2542
2626
  const signature = useCanvasState((s) => layerSignature(s, windowId));
2627
+ const single = useCanvasSinglePane();
2543
2628
  const root = (0, import_react7.useRef)(null);
2544
2629
  const [placement] = (0, import_react7.useState)(
2545
2630
  () => slots ? createPlacement(() => root.current, slots, () => layer.native.remeasure()) : null
@@ -2548,13 +2633,14 @@ function CanvasBodyLayer() {
2548
2633
  useIsoLayoutEffect(() => slots && placement ? slots.subscribe(placement.place) : void 0, [slots, placement]);
2549
2634
  const shape = parseSignature(signature);
2550
2635
  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)(
2636
+ 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
2637
  BodyHost,
2553
2638
  {
2554
2639
  hostId,
2555
2640
  itemId,
2641
+ kept,
2556
2642
  paneId,
2557
- visible: shape.isOpen && active,
2643
+ visible: shape.isOpen && active && (!single || paneId === shape.focusedPaneId),
2558
2644
  focused: active && shape.focusedPaneId === paneId,
2559
2645
  isFullscreen: shape.isFullscreen,
2560
2646
  paneCount: shape.paneCount,
@@ -2568,11 +2654,12 @@ function CanvasPaneInlineBodies({ paneId, slots }) {
2568
2654
  const { windowId } = useCanvasInternals();
2569
2655
  const shape = parseSignature(useCanvasState((s) => layerSignature(s, windowId, paneId)));
2570
2656
  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)(
2657
+ 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
2658
  BodyHost,
2573
2659
  {
2574
2660
  hostId,
2575
2661
  itemId,
2662
+ kept,
2576
2663
  paneId,
2577
2664
  visible: active,
2578
2665
  focused: active && shape.focusedPaneId === paneId,
@@ -2584,9 +2671,22 @@ function CanvasPaneInlineBodies({ paneId, slots }) {
2584
2671
  hostId
2585
2672
  )) });
2586
2673
  }
2674
+ function stepAsItem(step, openedAt) {
2675
+ return {
2676
+ id: canvasItemId(step.kind, step.key),
2677
+ kind: step.kind,
2678
+ key: step.key,
2679
+ title: step.title,
2680
+ data: step.data,
2681
+ openedAt,
2682
+ updatedAt: openedAt,
2683
+ ...step.instance ? { instance: step.instance } : {}
2684
+ };
2685
+ }
2587
2686
  var BodyHost = (0, import_react7.memo)(function BodyHost2({
2588
2687
  hostId,
2589
2688
  itemId,
2689
+ kept,
2590
2690
  paneId,
2591
2691
  visible,
2592
2692
  focused,
@@ -2597,7 +2697,14 @@ var BodyHost = (0, import_react7.memo)(function BodyHost2({
2597
2697
  }) {
2598
2698
  const canvas = useCanvas();
2599
2699
  const { windowId, ports } = useCanvasInternals();
2600
- const item = useCanvasState((s) => s.items[itemId]);
2700
+ const live = useCanvasState((s) => kept ? void 0 : s.items[itemId]);
2701
+ const step = useCanvasState((s) => {
2702
+ const ownerItem = kept ? s.items[itemId] : void 0;
2703
+ return ownerItem ? keptCanvasSteps(ownerItem).find((e) => e.instance === hostId) : void 0;
2704
+ });
2705
+ const owner = useCanvasState((s) => kept ? s.items[itemId]?.openedAt : void 0);
2706
+ const keptItem = (0, import_react7.useMemo)(() => step ? stepAsItem(step, owner ?? 0) : void 0, [step, owner]);
2707
+ const item = kept ? keptItem : live;
2601
2708
  const kind = useItemKind(item?.kind ?? "");
2602
2709
  const element = (0, import_react7.useRef)(null);
2603
2710
  const [everShown, setEverShown] = (0, import_react7.useState)(visible);
@@ -2621,6 +2728,7 @@ var BodyHost = (0, import_react7.memo)(function BodyHost2({
2621
2728
  "data-visible": visible ? "true" : "false",
2622
2729
  "data-kind": item.kind,
2623
2730
  "data-item": item.id,
2731
+ "data-kept": kept ? "" : void 0,
2624
2732
  "data-pane": paneId,
2625
2733
  inert: !visible,
2626
2734
  "aria-hidden": visible ? void 0 : true,
@@ -2773,6 +2881,25 @@ function itemTitle(item, kind) {
2773
2881
  return kind?.label ?? item.kind;
2774
2882
  }
2775
2883
  var isMac = () => typeof navigator !== "undefined" && /mac|iphone|ipad/i.test(navigator.platform ?? "");
2884
+ function windowTabsSignature(state, windowId) {
2885
+ const frame2 = canvasWindowFrame(state, windowId);
2886
+ if (!frame2) return "";
2887
+ const rows = [];
2888
+ for (const paneId of listPaneIds(frame2.layout)) {
2889
+ for (const itemId of state.panes[paneId]?.itemIds ?? []) rows.push(`${itemId}${paneId}`);
2890
+ }
2891
+ return rows.join("");
2892
+ }
2893
+ function parseWindowTabs(signature) {
2894
+ if (!signature) return [];
2895
+ const perPane = /* @__PURE__ */ new Map();
2896
+ return signature.split("").map((row) => {
2897
+ const [itemId = "", paneId = ""] = row.split("");
2898
+ const index = perPane.get(paneId) ?? 0;
2899
+ perPane.set(paneId, index + 1);
2900
+ return { itemId, paneId, index };
2901
+ });
2902
+ }
2776
2903
  function CanvasPaneView({ paneId }) {
2777
2904
  const canvas = useCanvas();
2778
2905
  const { windowId, tabStrip } = useCanvasInternals();
@@ -2791,6 +2918,8 @@ function CanvasPaneView({ paneId }) {
2791
2918
  const role = useCanvasState((s) => windowId === CANVAS_MAIN_WINDOW ? "main" : s.windows[windowId]?.role ?? "window");
2792
2919
  const canGoBack = useCanvasState((s) => active ? selectCanvasCanGoBack(s, active.id) : false);
2793
2920
  const kind = useItemKind(active?.kind ?? "");
2921
+ const singlePane = useCanvasSinglePane();
2922
+ const windowTabs = useCanvasState((s) => singlePane ? windowTabsSignature(s, windowId) : "");
2794
2923
  const sectionRef = (0, import_react8.useRef)(null);
2795
2924
  const bodyRef = (0, import_react8.useRef)(null);
2796
2925
  const presentation = usePanePresentation(slots, paneId, isFullscreen, paneCount);
@@ -2810,7 +2939,8 @@ function CanvasPaneView({ paneId }) {
2810
2939
  const Leading = kind?.HeaderLeading;
2811
2940
  const Action = kind?.HeaderAction;
2812
2941
  const Toolbar = kind?.Toolbar;
2813
- const single = tabStrip === "auto" && pane.itemIds.length === 1 && active;
2942
+ const tabs = singlePane ? parseWindowTabs(windowTabs) : pane.itemIds.map((itemId, index) => ({ itemId, paneId, index }));
2943
+ const single = tabStrip === "auto" && tabs.length === 1 && active;
2814
2944
  const closeLabel = role === "pop-out" && paneCount === 1 ? "Close window" : paneCount > 1 ? "Close pane" : "Close canvas";
2815
2945
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2816
2946
  CanvasPanel,
@@ -2823,11 +2953,14 @@ function CanvasPaneView({ paneId }) {
2823
2953
  "data-mxc-pane": paneId,
2824
2954
  header: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
2825
2955
  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" }),
2956
+ 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
2957
  /* @__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
2958
  ] }),
2829
2959
  leading: Leading && kindProps ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Leading, { ...kindProps }) : null,
2830
- actions: Action && kindProps ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Action, { ...kindProps }) : null,
2960
+ actions: Action && kindProps ? (
2961
+ // Its own gapless row: a kind's tap buttons space themselves (the tap-target placement rule).
2962
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "mxc-kind-actions", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Action, { ...kindProps }) })
2963
+ ) : null,
2831
2964
  renderMenu: () => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PaneMenu, { paneId, windowId, activeItem: active, kind, kindProps, tabCount: pane.itemIds.length, role }),
2832
2965
  expanded: isFullscreen,
2833
2966
  onToggleExpand: () => canvas.setFullscreen(!isFullscreen),
@@ -2869,38 +3002,71 @@ function SingleTitle({ item, kind }) {
2869
3002
  /* @__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, {}) })
2870
3003
  ] });
2871
3004
  }
2872
- function TabStrip({ paneId, itemIds, activeItemId }) {
3005
+ function TabStrip({ tabs, activeItemId }) {
2873
3006
  const strip = (0, import_react8.useRef)(null);
3007
+ const [overflow, setOverflow] = (0, import_react8.useState)({ start: false, end: false });
3008
+ const measure = (0, import_react8.useRef)(() => void 0);
3009
+ measure.current = () => {
3010
+ const el = strip.current;
3011
+ if (!el) return;
3012
+ const start = el.scrollLeft > 1;
3013
+ const end = el.scrollLeft + el.clientWidth < el.scrollWidth - 1;
3014
+ setOverflow((prev) => prev.start === start && prev.end === end ? prev : { start, end });
3015
+ };
3016
+ const tabKey = tabs.map((tab) => tab.itemId).join("|");
2874
3017
  useIsoLayoutEffect2(() => {
2875
3018
  const el = strip.current;
2876
3019
  if (!el) return void 0;
2877
3020
  const reveal = () => {
2878
3021
  const tab = el.querySelector('[role="tab"][aria-selected="true"]');
2879
- if (!tab) return;
2880
- const left = tab.offsetLeft;
2881
- const right = left + tab.offsetWidth;
2882
- if (left < el.scrollLeft) el.scrollLeft = left;
2883
- else if (right > el.scrollLeft + el.clientWidth) el.scrollLeft = right - el.clientWidth;
3022
+ if (tab) {
3023
+ const left = tab.offsetLeft;
3024
+ const right = left + tab.offsetWidth;
3025
+ if (left < el.scrollLeft) el.scrollLeft = left;
3026
+ else if (right > el.scrollLeft + el.clientWidth) el.scrollLeft = right - el.clientWidth;
3027
+ }
3028
+ measure.current();
2884
3029
  };
2885
3030
  reveal();
2886
3031
  const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(reveal);
2887
3032
  observer?.observe(el);
2888
- const activeTab = el.querySelector('[role="tab"][aria-selected="true"]');
2889
- if (activeTab) observer?.observe(activeTab);
3033
+ for (const tab of el.querySelectorAll('[role="tab"]')) observer?.observe(tab);
2890
3034
  return () => observer?.disconnect();
2891
- }, [activeItemId, itemIds.length]);
2892
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3035
+ }, [activeItemId, tabKey]);
3036
+ const page = (direction) => {
3037
+ const el = strip.current;
3038
+ if (!el) return;
3039
+ const by = Math.max(48, Math.round(el.clientWidth * 0.8)) * direction;
3040
+ if (typeof el.scrollBy === "function") el.scrollBy({ left: by, behavior: "smooth" });
3041
+ else el.scrollLeft += by;
3042
+ };
3043
+ const overflowing = overflow.start || overflow.end;
3044
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2893
3045
  "div",
2894
3046
  {
2895
- ref: strip,
2896
- className: "mxc-tabs",
2897
- role: "tablist",
2898
- "aria-label": "Canvas tabs",
2899
- onWheel: (event) => {
2900
- const el = strip.current;
2901
- if (el && Math.abs(event.deltaY) > Math.abs(event.deltaX)) el.scrollLeft += event.deltaY;
2902
- },
2903
- children: itemIds.map((itemId, index) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ItemTab, { itemId, paneId, index, active: itemId === activeItemId }, itemId))
3047
+ className: "mxc-tabstrip",
3048
+ "data-overflow": overflowing ? "" : void 0,
3049
+ "data-overflow-start": overflow.start ? "" : void 0,
3050
+ "data-overflow-end": overflow.end ? "" : void 0,
3051
+ children: [
3052
+ 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,
3053
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3054
+ "div",
3055
+ {
3056
+ ref: strip,
3057
+ className: "mxc-tabs",
3058
+ role: "tablist",
3059
+ "aria-label": "Canvas tabs",
3060
+ onScroll: () => measure.current(),
3061
+ onWheel: (event) => {
3062
+ const el = strip.current;
3063
+ if (el && Math.abs(event.deltaY) > Math.abs(event.deltaX)) el.scrollLeft += event.deltaY;
3064
+ },
3065
+ 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))
3066
+ }
3067
+ ),
3068
+ 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
3069
+ ]
2904
3070
  }
2905
3071
  );
2906
3072
  }
@@ -3053,7 +3219,10 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
3053
3219
  const isFullscreen = useCanvasState((s) => selectCanvasIsFullscreen(s, windowId));
3054
3220
  const width = useCanvasState((s) => selectCanvasWidth(s, windowId));
3055
3221
  const layout = useCanvasState((s) => selectCanvasLayout(s, windowId));
3222
+ const focusedPaneId = useCanvasState((s) => selectCanvasFocusedPaneId(s, windowId));
3056
3223
  const region = useRegionWidth();
3224
+ const viewport = useViewportWidth();
3225
+ const singlePane = placement === "docked" && viewport > 0 && viewport <= CANVAS_PHONE_MAX_WIDTH;
3057
3226
  const [dragWidth, setDragWidth] = (0, import_react9.useState)(null);
3058
3227
  const [slots] = (0, import_react9.useState)(createCanvasSlots);
3059
3228
  const ref = (0, import_react9.useRef)(null);
@@ -3118,10 +3287,10 @@ function CanvasColumn({ className, style, onLiveWidth, placement = "docked" }) {
3118
3287
  }
3119
3288
  }
3120
3289
  ) : null,
3121
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(CanvasSlotsContext.Provider, { value: slots, children: [
3122
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "mxc-layout", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(LayoutNodeView, { node: layout }) }),
3290
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CanvasSlotsContext.Provider, { value: slots, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(CanvasSinglePaneContext.Provider, { value: singlePane, children: [
3291
+ /* @__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 }) }),
3123
3292
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CanvasBodyLayer, {})
3124
- ] })
3293
+ ] }) })
3125
3294
  ] })
3126
3295
  }
3127
3296
  );
@@ -3169,6 +3338,10 @@ function WidthHandle({ width, region, onDrag }) {
3169
3338
  }
3170
3339
  );
3171
3340
  }
3341
+ function presentedPane(layout, focused) {
3342
+ const ids = listPaneIds(layout);
3343
+ return ids.find((id) => id === focused) ?? ids[0];
3344
+ }
3172
3345
  function LayoutNodeView({ node }) {
3173
3346
  if (node.type === "pane") return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CanvasPaneView, { paneId: node.paneId });
3174
3347
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SplitView, { node });
@@ -3271,6 +3444,7 @@ function CanvasToggle({
3271
3444
  ariaLabel: isOpen ? "Hide canvas" : "Show canvas",
3272
3445
  tooltip: `${isOpen ? "Hide canvas" : "Show canvas"}${hint ? ` (${hint})` : ""}`,
3273
3446
  icon: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PanelRightIcon, {}),
3447
+ pressed: isOpen,
3274
3448
  onClick: () => controller.toggle()
3275
3449
  }
3276
3450
  ),