@ai-matrx/canvas 0.5.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.1
4
+
5
+ Measured live slowness (perf lane), fixed at the subscriptions:
6
+
7
+ - **A tab's data change re-renders only that tab.** The body layer subscribes to a structural signature (which tabs are where, active, open, expanded) instead of the whole state; each body host reads its own item and is memoized on primitives; a pane reads its own active item, never `state.items`. Updating tab A re-renders neither tab B's body nor another pane.
8
+ - **Stable context.** `CanvasProvider` memoizes its host ports and its context value (and `CanvasWindowScope` its scope), so a provider re-rendered by its parent no longer re-renders every canvas consumer.
9
+ - The tab strip is the tabs' offset parent, so keeping the active tab in view measures in strip coordinates.
10
+ - (Already in 0.5.0, now guarded too: putting the canvas away hides the column; hide → show remounts nothing.)
11
+
12
+ ### Consumer action
13
+
14
+ - None.
15
+
3
16
  ## 0.5.0
4
17
 
5
18
  Owner feedback after using the canvas live (Arman, 2026-10-02): a stable canvas that never dismounts, no UI traps, readable tabs, the shell's motion, content that knows its shape, and the pieces web and desktop both need.
package/dist/react.cjs CHANGED
@@ -1701,8 +1701,17 @@ function CanvasProvider({
1701
1701
  layoutRules,
1702
1702
  regionWidth = null,
1703
1703
  tabStrip = "auto",
1704
- ...hostPorts
1704
+ itemActions,
1705
+ popOut,
1706
+ dockBack,
1707
+ showMenu,
1708
+ overlay,
1709
+ onNativeBodyFrame
1705
1710
  }) {
1711
+ const hostPorts = (0, import_react2.useMemo)(
1712
+ () => ({ itemActions, popOut, dockBack, showMenu, overlay, onNativeBodyFrame }),
1713
+ [itemActions, popOut, dockBack, showMenu, overlay, onNativeBodyFrame]
1714
+ );
1706
1715
  const portsRef = (0, import_react2.useRef)(hostPorts);
1707
1716
  portsRef.current = hostPorts;
1708
1717
  const [{ controller, layer, launcherEntries }] = (0, import_react2.useState)(() => ({
@@ -1744,7 +1753,10 @@ function CanvasProvider({
1744
1753
  window.addEventListener("keydown", onKey);
1745
1754
  return () => window.removeEventListener("keydown", onKey);
1746
1755
  }, [controller, hotkeys, windowId]);
1747
- const value = { controller, windowId, ports: hostPorts, layer, regionWidth, tabStrip, launcherEntries };
1756
+ const value = (0, import_react2.useMemo)(
1757
+ () => ({ controller, windowId, ports: hostPorts, layer, regionWidth, tabStrip, launcherEntries }),
1758
+ [controller, windowId, hostPorts, layer, regionWidth, tabStrip, launcherEntries]
1759
+ );
1748
1760
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(CanvasContext.Provider, { value, children: [
1749
1761
  children,
1750
1762
  hostPorts.overlay ? null : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CanvasDomOverlay, { layer })
@@ -1752,8 +1764,10 @@ function CanvasProvider({
1752
1764
  }
1753
1765
  function CanvasWindowScope({ windowId, children }) {
1754
1766
  const parent = useCanvasContext();
1755
- const id = canvasWindowId(windowId);
1756
- const value = { ...parent, windowId: id, controller: parent.controller.forWindow(id) };
1767
+ const value = (0, import_react2.useMemo)(() => {
1768
+ const id = canvasWindowId(windowId);
1769
+ return { ...parent, windowId: id, controller: parent.controller.forWindow(id) };
1770
+ }, [parent, windowId]);
1757
1771
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CanvasContext.Provider, { value, children });
1758
1772
  }
1759
1773
  function useCanvasContext() {
@@ -2449,20 +2463,40 @@ function usePanePresentation(slots, paneId, isFullscreen, paneCount) {
2449
2463
  [size.width, size.height, isFullscreen, paneCount]
2450
2464
  );
2451
2465
  }
2452
- function placedItems(state, windowId, onlyPane) {
2466
+ var hostKey = (item) => item.instance ?? item.id;
2467
+ var SEP2 = "";
2468
+ var ROW = "";
2469
+ function layerSignature(state, windowId, onlyPane) {
2453
2470
  const frame2 = canvasWindowFrame(state, windowId);
2454
- if (!frame2) return [];
2455
- const out = [];
2456
- for (const paneId of onlyPane ? [onlyPane] : listPaneIds(frame2.layout)) {
2471
+ if (!frame2) return "";
2472
+ const rows = [];
2473
+ const paneIds = listPaneIds(frame2.layout);
2474
+ for (const paneId of onlyPane ? [onlyPane] : paneIds) {
2457
2475
  const pane = state.panes[paneId];
2458
2476
  for (const itemId of pane?.itemIds ?? []) {
2459
2477
  const item = state.items[itemId];
2460
- if (item) out.push({ item, paneId, active: pane?.activeItemId === itemId });
2478
+ if (item) rows.push({ item, row: [hostKey(item), itemId, paneId, pane?.activeItemId === itemId ? "1" : "0"].join(SEP2) });
2461
2479
  }
2462
2480
  }
2463
- return out.sort((a, b) => a.item.openedAt - b.item.openedAt || hostKey(a.item).localeCompare(hostKey(b.item)));
2481
+ rows.sort((a, b) => a.item.openedAt - b.item.openedAt || hostKey(a.item).localeCompare(hostKey(b.item)));
2482
+ const head = [frame2.isOpen ? "1" : "0", frame2.isFullscreen ? "1" : "0", frame2.focusedPaneId, String(paneIds.length)].join(SEP2);
2483
+ return [head, ...rows.map((r) => r.row)].join(ROW);
2484
+ }
2485
+ function parseSignature(signature) {
2486
+ if (!signature) return null;
2487
+ const [head = "", ...rows] = signature.split(ROW);
2488
+ const [open, full, focusedPaneId = "", count = "1"] = head.split(SEP2);
2489
+ return {
2490
+ isOpen: open === "1",
2491
+ isFullscreen: full === "1",
2492
+ focusedPaneId,
2493
+ paneCount: Number(count) || 1,
2494
+ placed: rows.map((row) => {
2495
+ const [hostId = "", itemId = "", paneId = "", active] = row.split(SEP2);
2496
+ return { hostId, itemId, paneId, active: active === "1" };
2497
+ })
2498
+ };
2464
2499
  }
2465
- var hostKey = (item) => item.instance ?? item.id;
2466
2500
  function createPlacement(getRoot, slots, afterPlace) {
2467
2501
  const hosts = /* @__PURE__ */ new Map();
2468
2502
  let placing = false;
@@ -2505,57 +2539,54 @@ function createPlacement(getRoot, slots, afterPlace) {
2505
2539
  function CanvasBodyLayer() {
2506
2540
  const { windowId, layer } = useCanvasInternals();
2507
2541
  const slots = useCanvasSlots();
2508
- const state = useCanvasState((s) => s);
2542
+ const signature = useCanvasState((s) => layerSignature(s, windowId));
2509
2543
  const root = (0, import_react7.useRef)(null);
2510
2544
  const [placement] = (0, import_react7.useState)(
2511
2545
  () => slots ? createPlacement(() => root.current, slots, () => layer.native.remeasure()) : null
2512
2546
  );
2513
2547
  useIsoLayoutEffect(() => placement?.place());
2514
2548
  useIsoLayoutEffect(() => slots && placement ? slots.subscribe(placement.place) : void 0, [slots, placement]);
2515
- if (!slots || !placement) return null;
2516
- const frame2 = canvasWindowFrame(state, windowId);
2517
- if (!frame2) return null;
2518
- const paneCount = listPaneIds(frame2.layout).length;
2519
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { ref: root, className: "mxc-bodies", "data-mxc-bodies": windowId, children: placedItems(state, windowId).map(({ item, paneId, active }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2549
+ const shape = parseSignature(signature);
2550
+ 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)(
2520
2552
  BodyHost,
2521
2553
  {
2522
- hostId: hostKey(item),
2523
- item,
2554
+ hostId,
2555
+ itemId,
2524
2556
  paneId,
2525
- visible: frame2.isOpen && active,
2526
- focused: active && frame2.focusedPaneId === paneId,
2527
- isFullscreen: frame2.isFullscreen,
2528
- paneCount,
2557
+ visible: shape.isOpen && active,
2558
+ focused: active && shape.focusedPaneId === paneId,
2559
+ isFullscreen: shape.isFullscreen,
2560
+ paneCount: shape.paneCount,
2529
2561
  slots,
2530
2562
  placement
2531
2563
  },
2532
- hostKey(item)
2564
+ hostId
2533
2565
  )) });
2534
2566
  }
2535
2567
  function CanvasPaneInlineBodies({ paneId, slots }) {
2536
2568
  const { windowId } = useCanvasInternals();
2537
- const state = useCanvasState((s) => s);
2538
- const frame2 = canvasWindowFrame(state, windowId);
2539
- const paneCount = frame2 ? listPaneIds(frame2.layout).length : 1;
2540
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: placedItems(state, windowId, paneId).map(({ item, active }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2569
+ const shape = parseSignature(useCanvasState((s) => layerSignature(s, windowId, paneId)));
2570
+ 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)(
2541
2572
  BodyHost,
2542
2573
  {
2543
- hostId: hostKey(item),
2544
- item,
2574
+ hostId,
2575
+ itemId,
2545
2576
  paneId,
2546
2577
  visible: active,
2547
- focused: active && frame2?.focusedPaneId === paneId,
2548
- isFullscreen: frame2?.isFullscreen ?? false,
2549
- paneCount,
2578
+ focused: active && shape.focusedPaneId === paneId,
2579
+ isFullscreen: shape.isFullscreen,
2580
+ paneCount: shape.paneCount,
2550
2581
  slots,
2551
2582
  placement: null
2552
2583
  },
2553
- hostKey(item)
2584
+ hostId
2554
2585
  )) });
2555
2586
  }
2556
- function BodyHost({
2587
+ var BodyHost = (0, import_react7.memo)(function BodyHost2({
2557
2588
  hostId,
2558
- item,
2589
+ itemId,
2559
2590
  paneId,
2560
2591
  visible,
2561
2592
  focused,
@@ -2566,7 +2597,8 @@ function BodyHost({
2566
2597
  }) {
2567
2598
  const canvas = useCanvas();
2568
2599
  const { windowId, ports } = useCanvasInternals();
2569
- const kind = useItemKind(item.kind);
2600
+ const item = useCanvasState((s) => s.items[itemId]);
2601
+ const kind = useItemKind(item?.kind ?? "");
2570
2602
  const element = (0, import_react7.useRef)(null);
2571
2603
  const [everShown, setEverShown] = (0, import_react7.useState)(visible);
2572
2604
  if (visible && !everShown) setEverShown(true);
@@ -2578,6 +2610,7 @@ function BodyHost({
2578
2610
  if (!placement || !el) return void 0;
2579
2611
  return placement.register(hostId, { element: el, paneId });
2580
2612
  }, [placement, hostId, paneId]);
2613
+ if (!item) return null;
2581
2614
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2582
2615
  "div",
2583
2616
  {
@@ -2611,7 +2644,7 @@ function BodyHost({
2611
2644
  ) }) }) : null
2612
2645
  }
2613
2646
  );
2614
- }
2647
+ });
2615
2648
  function ItemContent({
2616
2649
  item,
2617
2650
  kind,
@@ -2747,13 +2780,15 @@ function CanvasPaneView({ paneId }) {
2747
2780
  const [ownSlots] = (0, import_react8.useState)(createCanvasSlots);
2748
2781
  const slots = columnSlots ?? ownSlots;
2749
2782
  const pane = useCanvasState((s) => s.panes[paneId]);
2750
- const items = useCanvasState((s) => s.items);
2783
+ const active = useCanvasState((s) => {
2784
+ const activeId = s.panes[paneId]?.activeItemId;
2785
+ return activeId ? s.items[activeId] : void 0;
2786
+ });
2751
2787
  const isFullscreen = useCanvasState((s) => selectCanvasIsFullscreen(s, windowId));
2752
2788
  const isOpen = useCanvasState((s) => selectCanvasIsOpen(s, windowId));
2753
2789
  const focused = useCanvasState((s) => selectCanvasFocusedPaneId(s, windowId) === paneId);
2754
2790
  const paneCount = useCanvasState((s) => selectCanvasPaneCount(s, windowId));
2755
2791
  const role = useCanvasState((s) => windowId === CANVAS_MAIN_WINDOW ? "main" : s.windows[windowId]?.role ?? "window");
2756
- const active = pane?.activeItemId ? items[pane.activeItemId] : void 0;
2757
2792
  const canGoBack = useCanvasState((s) => active ? selectCanvasCanGoBack(s, active.id) : false);
2758
2793
  const kind = useItemKind(active?.kind ?? "");
2759
2794
  const sectionRef = (0, import_react8.useRef)(null);
@@ -2842,7 +2877,7 @@ function TabStrip({ paneId, itemIds, activeItemId }) {
2842
2877
  const reveal = () => {
2843
2878
  const tab = el.querySelector('[role="tab"][aria-selected="true"]');
2844
2879
  if (!tab) return;
2845
- const left = tab.offsetLeft - el.offsetLeft;
2880
+ const left = tab.offsetLeft;
2846
2881
  const right = left + tab.offsetWidth;
2847
2882
  if (left < el.scrollLeft) el.scrollLeft = left;
2848
2883
  else if (right > el.scrollLeft + el.clientWidth) el.scrollLeft = right - el.clientWidth;