@1agh/maude 0.39.1 → 0.41.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.
Files changed (137) hide show
  1. package/README.md +1 -1
  2. package/apps/studio/ai-banner.tsx +2 -2
  3. package/apps/studio/annotations-context-toolbar.tsx +44 -1
  4. package/apps/studio/annotations-layer.tsx +306 -5
  5. package/apps/studio/annotations-model.ts +107 -6
  6. package/apps/studio/api.ts +1316 -67
  7. package/apps/studio/artboard-marquee.tsx +1 -1
  8. package/apps/studio/bin/_canvas-rects-playwright.mjs +55 -0
  9. package/apps/studio/bin/_canvas-rects-static.mjs +166 -0
  10. package/apps/studio/bin/_fetch-asset.mjs +556 -0
  11. package/apps/studio/bin/_html-playwright.mjs +9 -1
  12. package/apps/studio/bin/_pdf-playwright.mjs +8 -1
  13. package/apps/studio/bin/_png-playwright.mjs +8 -1
  14. package/apps/studio/bin/_pw-launch.mjs +54 -0
  15. package/apps/studio/bin/_svg-playwright.mjs +8 -1
  16. package/apps/studio/bin/_video-playwright.mjs +452 -0
  17. package/apps/studio/bin/annotate.mjs +576 -34
  18. package/apps/studio/bin/canvas-rects.sh +152 -0
  19. package/apps/studio/bin/fetch-asset.sh +34 -0
  20. package/apps/studio/bin/prep.sh +8 -1
  21. package/apps/studio/bin/read-annotations.mjs +138 -7
  22. package/apps/studio/bin/smoke.sh +42 -6
  23. package/apps/studio/build.ts +21 -0
  24. package/apps/studio/canvas-comment-mount.tsx +138 -4
  25. package/apps/studio/canvas-edit.ts +2625 -111
  26. package/apps/studio/canvas-lib.tsx +238 -2
  27. package/apps/studio/canvas-list-watch.ts +6 -2
  28. package/apps/studio/canvas-shell.tsx +514 -20
  29. package/apps/studio/client/app.jsx +2480 -52
  30. package/apps/studio/client/comments-overlay.css +130 -126
  31. package/apps/studio/client/github.js +15 -0
  32. package/apps/studio/client/panels/TimelinePanel.jsx +860 -0
  33. package/apps/studio/client/panels/timeline-parse.js +229 -0
  34. package/apps/studio/client/panels/timeline-snap.js +55 -0
  35. package/apps/studio/client/styles/3-shell-maude.css +155 -0
  36. package/apps/studio/comments-overlay.tsx +148 -41
  37. package/apps/studio/config.schema.json +14 -0
  38. package/apps/studio/context-menu.tsx +16 -6
  39. package/apps/studio/context.ts +113 -1
  40. package/apps/studio/contextual-toolbar.tsx +262 -4
  41. package/apps/studio/cursors-overlay.tsx +4 -4
  42. package/apps/studio/dist/client.bundle.js +89 -17
  43. package/apps/studio/dist/comment-mount.js +59 -1
  44. package/apps/studio/dist/runtime/.min-sizes.json +11 -1
  45. package/apps/studio/dist/runtime/@remotion_media.js +491 -0
  46. package/apps/studio/dist/runtime/@remotion_player.js +56 -0
  47. package/apps/studio/dist/runtime/@remotion_transitions.js +300 -0
  48. package/apps/studio/dist/runtime/@remotion_transitions_clock-wipe.js +1 -0
  49. package/apps/studio/dist/runtime/@remotion_transitions_fade.js +1 -0
  50. package/apps/studio/dist/runtime/@remotion_transitions_flip.js +1 -0
  51. package/apps/studio/dist/runtime/@remotion_transitions_none.js +1 -0
  52. package/apps/studio/dist/runtime/@remotion_transitions_slide.js +1 -0
  53. package/apps/studio/dist/runtime/@remotion_transitions_wipe.js +1 -0
  54. package/apps/studio/dist/runtime/REMOTION-LICENSE.md +28 -0
  55. package/apps/studio/dist/runtime/remotion.js +42 -0
  56. package/apps/studio/dist/styles.css +1 -1
  57. package/apps/studio/dom-selection.ts +127 -1
  58. package/apps/studio/drag-state.ts +24 -0
  59. package/apps/studio/equal-spacing-detector.ts +205 -0
  60. package/apps/studio/export-dialog.tsx +1 -1
  61. package/apps/studio/exporters/_browser-bundles.ts +117 -0
  62. package/apps/studio/exporters/_runtime.ts +69 -0
  63. package/apps/studio/exporters/html.ts +4 -3
  64. package/apps/studio/exporters/index.ts +28 -2
  65. package/apps/studio/exporters/pdf.ts +4 -3
  66. package/apps/studio/exporters/png.ts +5 -3
  67. package/apps/studio/exporters/pptx.ts +6 -4
  68. package/apps/studio/exporters/svg.ts +9 -5
  69. package/apps/studio/exporters/video-encode-lib.ts +200 -0
  70. package/apps/studio/exporters/video-render-lib.ts +108 -0
  71. package/apps/studio/exporters/video.ts +184 -0
  72. package/apps/studio/history.ts +47 -1
  73. package/apps/studio/http.ts +758 -27
  74. package/apps/studio/input-router.tsx +19 -1
  75. package/apps/studio/marquee-overlay.tsx +1 -1
  76. package/apps/studio/measure-overlay.tsx +241 -0
  77. package/apps/studio/participants-chrome.tsx +3 -3
  78. package/apps/studio/runtime-bundle.ts +30 -0
  79. package/apps/studio/server.ts +34 -9
  80. package/apps/studio/sizing-mode.ts +117 -0
  81. package/apps/studio/spacing-handles.ts +166 -0
  82. package/apps/studio/test/annotate-write.test.ts +890 -0
  83. package/apps/studio/test/annotations-roundtrip.test.ts +47 -0
  84. package/apps/studio/test/camera-reveal.test.tsx +173 -0
  85. package/apps/studio/test/canvas-create-api.test.ts +76 -0
  86. package/apps/studio/test/canvas-edit.test.ts +140 -0
  87. package/apps/studio/test/canvas-list-watch.test.ts +49 -0
  88. package/apps/studio/test/canvas-media-drop.test.ts +30 -1
  89. package/apps/studio/test/canvas-origin-gate.test.ts +54 -0
  90. package/apps/studio/test/canvas-rects.test.ts +198 -0
  91. package/apps/studio/test/clip-addressing.test.ts +732 -0
  92. package/apps/studio/test/comments-overlay.test.ts +117 -0
  93. package/apps/studio/test/config-reload.test.ts +230 -0
  94. package/apps/studio/test/dns-rebinding-guard.test.ts +74 -0
  95. package/apps/studio/test/dom-selection.test.ts +130 -0
  96. package/apps/studio/test/edit-css-occurrence.test.ts +81 -0
  97. package/apps/studio/test/edit-persistence.test.ts +91 -0
  98. package/apps/studio/test/edit-scope-api.test.ts +115 -0
  99. package/apps/studio/test/element-resize.test.ts +136 -0
  100. package/apps/studio/test/element-structural-api.test.ts +360 -0
  101. package/apps/studio/test/element-structural-edit.test.ts +233 -0
  102. package/apps/studio/test/equal-spacing-detector.test.ts +165 -1
  103. package/apps/studio/test/exporters/runtime.test.ts +59 -0
  104. package/apps/studio/test/file-lock.test.ts +84 -0
  105. package/apps/studio/test/fixtures/video-comp-fixture.tsx +82 -0
  106. package/apps/studio/test/history-rollback.test.ts +26 -0
  107. package/apps/studio/test/knob-props-authored.test.ts +87 -0
  108. package/apps/studio/test/read-annotations.test.ts +154 -0
  109. package/apps/studio/test/sizing-mode.test.ts +102 -0
  110. package/apps/studio/test/spacing-handles.test.ts +138 -0
  111. package/apps/studio/test/specimen-select.test.ts +88 -0
  112. package/apps/studio/test/timeline-parse.test.ts +127 -0
  113. package/apps/studio/test/timeline-snap.test.ts +85 -0
  114. package/apps/studio/test/tool-palette-insert-anchor.test.ts +84 -0
  115. package/apps/studio/test/undo-sequence-byte-compare.test.ts +211 -0
  116. package/apps/studio/test/video-asset.test.ts +163 -0
  117. package/apps/studio/test/video-comp-fixture.test.ts +50 -0
  118. package/apps/studio/test/video-comp.test.ts +168 -0
  119. package/apps/studio/test/video-render-bridge.test.ts +149 -0
  120. package/apps/studio/tool-palette.tsx +122 -2
  121. package/apps/studio/undo-hud.tsx +2 -2
  122. package/apps/studio/use-annotation-resize.tsx +6 -3
  123. package/apps/studio/use-canvas-media-drop.tsx +66 -4
  124. package/apps/studio/use-element-resize.tsx +732 -0
  125. package/apps/studio/use-keyboard-discipline.tsx +205 -15
  126. package/apps/studio/use-selection-set.tsx +14 -0
  127. package/apps/studio/use-spacing-handles.tsx +388 -0
  128. package/apps/studio/video-comp.tsx +444 -0
  129. package/apps/studio/whats-new.json +55 -0
  130. package/apps/studio/ws.ts +5 -0
  131. package/cli/commands/design.mjs +6 -1
  132. package/cli/commands/design.test.mjs +49 -1
  133. package/cli/lib/fetch-asset.test.mjs +213 -0
  134. package/package.json +8 -8
  135. package/plugins/design/dependencies.json +10 -2
  136. package/plugins/design/templates/_shell.html +25 -2
  137. package/plugins/design/templates/design-system-inspiration/_MAPPING.md +1 -1
@@ -47,6 +47,16 @@
47
47
  * useTheme() Current theme + setter, syncs to <html data-theme>.
48
48
  * useArtboardBounds(ref) ResizeObserver wrapper returning {width,height}.
49
49
  *
50
+ * Whiteboard toolkit (feature-whiteboard-ai-toolkit) ────────────────────
51
+ * window.__maudeCanvasRects() Installed as a side effect on module load.
52
+ * Returns { artboards, elements, elementsTruncated } in
53
+ * WORLD coords — the geometry manifest `maude design
54
+ * canvas-rects` reads via headless Chromium so the
55
+ * annotation read/write verbs never hand-compute a
56
+ * coordinate. See the section right after DCArtboard.
57
+ * getLiveViewport() The live pan/zoom, readable outside the React tree —
58
+ * what the hook above converts screen rects through.
59
+ *
50
60
  * Authoring vocabulary. Lift these before re-implementing equivalents. The
51
61
  * surface intentionally mirrors the .html-era specimen idioms one-for-one
52
62
  * (`.specimen-hd`, `.specimen-meta`, `.sku`, `.swatch`, `.stamp`) so existing
@@ -95,6 +105,7 @@ import {
95
105
  buildMoveArtboardsRecord,
96
106
  diffLayoutPositions,
97
107
  } from './commands/move-artboards-command.ts';
108
+ import { scopedCdSelector, selectorIndex, shortText } from './dom-selection.ts';
98
109
  import { AgentPresenceProvider, useAgentPresence } from './use-agent-presence.tsx';
99
110
  import { type DragState, useArtboardDrag } from './use-artboard-drag.tsx';
100
111
  import {
@@ -108,6 +119,13 @@ import { useSelectionSetOptional } from './use-selection-set.tsx';
108
119
  import { MaybeToolProvider, useToolModeOptional } from './use-tool-mode.tsx';
109
120
  import { UndoStackProvider, useUndoSinks, useUndoStackOptional } from './use-undo-stack.tsx';
110
121
 
122
+ export type { CompSnapshot, VideoCompMeta, VideoCompProps } from './video-comp.tsx';
123
+ // DDR-148 — video-comp canvas kind. Re-exported so `@maude/canvas-lib` exposes
124
+ // <VideoComp> (Remotion composition mounted in <Player> + the deterministic
125
+ // seek bridge). Its `remotion`/`@remotion/player` imports resolve through the
126
+ // canvas importmap (RUNTIME_PACKAGES), same as react/motion.
127
+ export { VideoComp } from './video-comp.tsx';
128
+
111
129
  // ─────────────────────────────────────────────────────────────────────────────
112
130
  // Module constants
113
131
 
@@ -239,6 +257,28 @@ const ENGINE_CSS = `
239
257
  display: flex;
240
258
  flex-direction: column;
241
259
  overflow: hidden;
260
+ /* Per-board compositor freeze (RCA: large filter/blend-heavy canvases jank on
261
+ pan/zoom). Promote each artboard to its own GPU layer that rasterizes ONCE
262
+ into a fixed bitmap the compositor then translates/scales — instead of
263
+ re-running its paint every frame. Load-bearing on WebKit/WKWebView, which
264
+ does NOT hardware-accelerate SVG url() reference filters or feTurbulence
265
+ (only CSS shorthand filters), so a scale change would otherwise re-RASTER
266
+ every board's poster/grain/blend at once (the fit-all zoom cliff).
267
+ will-change:transform (NOT plain translateZ(0)) is the fix: it freezes the
268
+ rastered bitmap so it never re-rasters under transform updates — fit-all pan
269
+ composites finished textures, zoom SCALES them (sharp when zoomed out).
270
+ Filters stay LIVE in the DOM — nothing baked — so the exact look and full
271
+ editability are preserved; paint just happens once per board, not per frame.
272
+ isolation:isolate bounds each board's blend backdrop so the texture is
273
+ self-contained/cacheable (isolation/contain do not themselves promote —
274
+ will-change does). content-visibility (on .dc-positioned) culls off-screen
275
+ boards so the promoted-layer count + GPU memory stay bounded to what's in
276
+ view. NB: this is the realistic ceiling on WebKit — a Chromium engine (Blink)
277
+ GPU-accelerates url() filters and renders such canvases more smoothly.
278
+ See .ai/logs/rca/issue-canvas-pan-zoom-jank-large-moodboard.md. */
279
+ isolation: isolate;
280
+ contain: paint;
281
+ will-change: transform;
242
282
  }
243
283
  .dc-canvas .dc-artboard.dc-positioned { position: absolute; }
244
284
  .dc-canvas .dc-artboard-label {
@@ -678,6 +718,15 @@ function fitRectIntoHost(rect: ArtboardRect, hostEl: HTMLElement, pad = 24): Vie
678
718
  // then. Written on every applyViewport (exact — no settle/publish lag).
679
719
  let liveViewport: ViewportState | null = null;
680
720
 
721
+ /**
722
+ * The current pan/zoom, read from OUTSIDE the React tree — the
723
+ * `window.__maudeCanvasRects()` hook (below) has no component instance to
724
+ * hook into, so it reads this module-scope mirror instead of WorldContext.
725
+ */
726
+ export function getLiveViewport(): ViewportState | null {
727
+ return liveViewport ? { ...liveViewport } : null;
728
+ }
729
+
681
730
  export function useViewportController(opts: ViewportControllerOptions): ViewportControllerHandle {
682
731
  const {
683
732
  hostRef,
@@ -697,6 +746,13 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
697
746
  const [isInteracting, setIsInteracting] = useState(false);
698
747
  const interactingRef = useRef(false);
699
748
  const isInteractingStateRef = useRef(false);
749
+ // Snapshot of the zoom at the START of each interaction burst. Lets the
750
+ // WebKit settle re-raster (writeTransform crisp) fire ONLY when the gesture
751
+ // actually changed scale — a pure pan is scale-invariant, so re-rasterizing
752
+ // the whole world plane on pan-settle is wasted work (the "seká po dojezdu"
753
+ // tail-spike on large canvases). RCA:
754
+ // .ai/logs/rca/issue-canvas-pan-zoom-jank-large-moodboard.md
755
+ const zoomAtInteractStartRef = useRef(1);
700
756
 
701
757
  // Throttle / settle timers.
702
758
  const publishTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -781,6 +837,9 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
781
837
  interactingRef.current = true;
782
838
  if (!isInteractingStateRef.current) {
783
839
  isInteractingStateRef.current = true;
840
+ // Start of a fresh gesture burst — remember the scale so settle can tell
841
+ // a pure pan (skip the crisp re-raster) from a zoom (needs it).
842
+ zoomAtInteractStartRef.current = vpRef.current.zoom;
784
843
  setIsInteracting(true);
785
844
  }
786
845
  if (interactEndTimerRef.current != null) clearTimeout(interactEndTimerRef.current);
@@ -792,7 +851,12 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
792
851
  // Motion has stopped — re-write the world transform in crisp mode so the
793
852
  // WebKit path drops its compositor layer and re-rasterizes text sharply at
794
853
  // the settled scale. No-op on the Blink path (CSS `zoom` is already crisp).
795
- writeTransform(vpRef.current, { crisp: true });
854
+ // Skip it when the burst never changed scale (pure pan): the layer is
855
+ // already crisp at this zoom, so releasing + repainting the whole world
856
+ // plane would be a wasted main-thread spike on large canvases.
857
+ if (vpRef.current.zoom !== zoomAtInteractStartRef.current) {
858
+ writeTransform(vpRef.current, { crisp: true });
859
+ }
796
860
  }, 220);
797
861
  }, [writeTransform]);
798
862
 
@@ -950,6 +1014,7 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
950
1014
  const initial = getInitial();
951
1015
  if (initial) {
952
1016
  vpRef.current = { ...initial };
1017
+ liveViewport = { ...initial }; // mirror pre-first-gesture so headless capture sees it
953
1018
  writeTransform(vpRef.current, { crisp: true });
954
1019
  setViewportPublished({ ...vpRef.current });
955
1020
  }
@@ -963,6 +1028,7 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
963
1028
  hadSize = true;
964
1029
  const refit = computeFitRef.current();
965
1030
  vpRef.current = { ...refit };
1031
+ liveViewport = { ...refit };
966
1032
  writeTransform(vpRef.current, { crisp: true });
967
1033
  setViewportPublished({ ...vpRef.current });
968
1034
  }
@@ -1497,6 +1563,18 @@ function DesignCanvasInner({ children, controls }: DesignCanvasProps) {
1497
1563
  return bestId;
1498
1564
  }, [artboards, controller.viewport]);
1499
1565
 
1566
+ // DDR-148 — report the viewport-active artboard (nearest the centre) to the
1567
+ // shell so the Timeline panel FOLLOWS whichever artboard the user pans to,
1568
+ // not just the one they cmd+click. Fires on every pan/zoom that changes it.
1569
+ useEffect(() => {
1570
+ if (typeof window === 'undefined' || window.parent === window) return;
1571
+ try {
1572
+ window.parent.postMessage({ dgn: 'active-artboard', id: activeArtboardId }, '*');
1573
+ } catch {
1574
+ /* cross-origin parent without our listener — no-op */
1575
+ }
1576
+ }, [activeArtboardId]);
1577
+
1500
1578
  // The world's transform is owned by useViewportController (writes straight
1501
1579
  // to `worldRef.current.style.transform`). Rendering the transform from
1502
1580
  // React state instead would race: between React's commit and the
@@ -1772,7 +1850,23 @@ export function DCArtboard({
1772
1850
  className={`dc-artboard dc-positioned${isInDrag ? ' dc-dragging' : ''}`}
1773
1851
  data-dc-screen={id}
1774
1852
  aria-current={isActive ? 'true' : undefined}
1775
- style={{ left: liveX, top: liveY, width: rect.w, height: rect.h }}
1853
+ style={{
1854
+ left: liveX,
1855
+ top: liveY,
1856
+ width: rect.w,
1857
+ height: rect.h,
1858
+ // Off-screen artboards skip layout+paint. On a large multi-board
1859
+ // canvas (e.g. an 18-board moodboard) every board otherwise paints
1860
+ // onto one huge .dc-world plane whose device-pixel size exceeds
1861
+ // WebKit's ~16384px texture limit, forcing tile repaints on every
1862
+ // pan/zoom. content-visibility localizes each board's paint and culls
1863
+ // the ones outside the viewport; contain-intrinsic-size preserves the
1864
+ // box so the model-based fit math (computeFit reads ArtboardRects,
1865
+ // never the DOM) and drag hit-testing are unaffected. RCA:
1866
+ // .ai/logs/rca/issue-canvas-pan-zoom-jank-large-moodboard.md
1867
+ contentVisibility: 'auto',
1868
+ containIntrinsicSize: `${rect.w}px ${rect.h}px`,
1869
+ }}
1776
1870
  {...handleProps}
1777
1871
  >
1778
1872
  <button type="button" className="dc-artboard-label sku" aria-label={`Artboard ${label}`}>
@@ -1793,6 +1887,148 @@ export function DCArtboard({
1793
1887
  }
1794
1888
  DCArtboard.displayName = 'DCArtboard';
1795
1889
 
1890
+ // ─────────────────────────────────────────────────────────────────────────────
1891
+ // window.__maudeCanvasRects() — the whiteboard-toolkit geometry manifest hook
1892
+ // (feature-whiteboard-ai-toolkit). `maude design canvas-rects` (a headless
1893
+ // Chromium shim) calls this to get every artboard's + every meaningful
1894
+ // element's WORLD-coordinate rect, so the annotation read/write verbs
1895
+ // (`read-annotations --rects`, `annotate --in/--pin/--board`) never need to
1896
+ // hand-compute a coordinate. Pure DOM + the `liveViewport` mirror above — no
1897
+ // React context needed, so it works from a plain page.evaluate() call with no
1898
+ // regard for which component tree happens to be mounted.
1899
+ //
1900
+ // World-coord conversion mirrors DesignCanvasInner's `activeArtboardId` math
1901
+ // (canvas = translate(vp.x,vp.y) then scale(vp.zoom) applied to `.dc-world`):
1902
+ // worldX = (screenX - hostRect.left - vp.x) / vp.zoom
1903
+ // A `.dc-canvas` host is required (bare DS specimens have no world plane —
1904
+ // this hook is for UI canvases with artboards, so it returns an empty
1905
+ // manifest there rather than guessing).
1906
+
1907
+ export interface CanvasRectsElement {
1908
+ cdId: string | null;
1909
+ selector: string;
1910
+ index: number;
1911
+ artboard: string | null;
1912
+ x: number;
1913
+ y: number;
1914
+ w: number;
1915
+ h: number;
1916
+ tag: string;
1917
+ text: string;
1918
+ }
1919
+
1920
+ export interface CanvasRectsManifest {
1921
+ artboards: ArtboardRect[];
1922
+ elements: CanvasRectsElement[];
1923
+ elementsTruncated: boolean;
1924
+ }
1925
+
1926
+ // Comment/annotation/chrome subtrees never make useful annotation targets —
1927
+ // mirrors the pickSpecimenEl chrome denylist (canvas-comment-mount.tsx) plus
1928
+ // the annotation layer's own root.
1929
+ const RECTS_CHROME_SELECTOR =
1930
+ '.cm-composer, .cm-thread, .cm-mention-popup, .cm-pin, [data-mc-hover-halo], [data-mdcc-annotations], .dc-minimap, .dc-zoom-toolbar';
1931
+
1932
+ const RECTS_INTERACTIVE_TAGS = new Set([
1933
+ 'button',
1934
+ 'a',
1935
+ 'input',
1936
+ 'textarea',
1937
+ 'select',
1938
+ 'option',
1939
+ 'label',
1940
+ 'summary',
1941
+ 'img',
1942
+ 'svg',
1943
+ 'video',
1944
+ 'picture',
1945
+ ]);
1946
+
1947
+ const RECTS_ELEMENT_CAP = 400;
1948
+
1949
+ /** True when `el` has no `[data-cd-id]` descendant — a "leaf" in the pipeline's
1950
+ * stamped tree, i.e. the finest element the JSX source actually distinguishes
1951
+ * (a text run, an icon, a leaf `<div>`) rather than a structural wrapper. */
1952
+ function isCdIdLeaf(el: Element): boolean {
1953
+ return el.querySelector('[data-cd-id]') === null;
1954
+ }
1955
+
1956
+ function elementIsCandidate(el: Element): boolean {
1957
+ const tag = el.tagName.toLowerCase();
1958
+ if (RECTS_INTERACTIVE_TAGS.has(tag)) return true;
1959
+ if (el.hasAttribute('role') && el.getAttribute('role') === 'button') return true;
1960
+ if (el.hasAttribute('tabindex')) return true;
1961
+ if (!isCdIdLeaf(el)) return false; // structural wrapper — a leaf sibling/descendant covers it
1962
+ return shortText(el, 1).length > 0;
1963
+ }
1964
+
1965
+ function buildCanvasRectsManifest(): CanvasRectsManifest {
1966
+ const empty: CanvasRectsManifest = { artboards: [], elements: [], elementsTruncated: false };
1967
+ if (typeof document === 'undefined') return empty;
1968
+ const host = document.querySelector('.dc-canvas') as HTMLElement | null;
1969
+ if (!host) return empty; // bare specimen / no world plane — nothing to resolve against
1970
+
1971
+ const vp = getLiveViewport() ?? { x: 0, y: 0, zoom: 1 };
1972
+ const hostRect = host.getBoundingClientRect();
1973
+ const toWorld = (r: DOMRect): { x: number; y: number; w: number; h: number } => ({
1974
+ x: (r.left - hostRect.left - vp.x) / vp.zoom,
1975
+ y: (r.top - hostRect.top - vp.y) / vp.zoom,
1976
+ w: r.width / vp.zoom,
1977
+ h: r.height / vp.zoom,
1978
+ });
1979
+
1980
+ const artboards: ArtboardRect[] = [];
1981
+ const artboardEls = Array.from(document.querySelectorAll('[data-dc-screen]'));
1982
+ for (const el of artboardEls) {
1983
+ const rect = (el as HTMLElement).getBoundingClientRect();
1984
+ if (rect.width <= 0 || rect.height <= 0) continue;
1985
+ const id = el.getAttribute('data-dc-screen');
1986
+ if (!id) continue;
1987
+ artboards.push({ id, ...toWorld(rect) });
1988
+ }
1989
+
1990
+ const elements: CanvasRectsElement[] = [];
1991
+ let truncated = false;
1992
+ const candidateEls = Array.from(document.querySelectorAll('[data-cd-id]'));
1993
+ for (const el of candidateEls) {
1994
+ if (elements.length >= RECTS_ELEMENT_CAP) {
1995
+ truncated = true;
1996
+ break;
1997
+ }
1998
+ if (el.closest(RECTS_CHROME_SELECTOR)) continue;
1999
+ const artboardEl = el.closest('[data-dc-screen]');
2000
+ if (!artboardEl) continue; // off-artboard chrome (menubar, panels, toolbars)
2001
+ if (!elementIsCandidate(el)) continue;
2002
+ const rect = (el as HTMLElement).getBoundingClientRect();
2003
+ if (rect.width <= 0 || rect.height <= 0) continue;
2004
+ const cdId = el.getAttribute('data-cd-id');
2005
+ const artboardId = artboardEl.getAttribute('data-dc-screen');
2006
+ const selector = cdId ? scopedCdSelector(cdId, artboardId) : '';
2007
+ const index = cdId ? selectorIndex(document, selector, el) : 0;
2008
+ elements.push({
2009
+ cdId,
2010
+ selector,
2011
+ index,
2012
+ artboard: artboardId,
2013
+ ...toWorld(rect),
2014
+ tag: el.tagName.toLowerCase(),
2015
+ text: shortText(el, 120),
2016
+ });
2017
+ }
2018
+
2019
+ return { artboards, elements, elementsTruncated: truncated };
2020
+ }
2021
+
2022
+ declare global {
2023
+ interface Window {
2024
+ __maudeCanvasRects?: () => CanvasRectsManifest;
2025
+ }
2026
+ }
2027
+
2028
+ if (typeof window !== 'undefined') {
2029
+ window.__maudeCanvasRects = buildCanvasRectsManifest;
2030
+ }
2031
+
1796
2032
  // ─────────────────────────────────────────────────────────────────────────────
1797
2033
  // DrawProof (Phase 25) — the render/verify harness for the draw engine. Renders
1798
2034
  // ONE vector mark across a size ladder × {light, dark, single-color flatten} as
@@ -86,7 +86,11 @@ export function createCanvasListWatch(
86
86
  // default config newCanvasDir IS a group; a config pointing it outside
87
87
  // canvasGroups would have the canvas absent from /_index-data too, so the
88
88
  // watcher stays consistent with what the tree can actually show.
89
- const groupPaths = (ctx.cfg.canvasGroups ?? []).map((g) => g.path);
89
+ //
90
+ // Read at USE time, never captured — `reloadConfig` (context.ts) hot-swaps
91
+ // ctx.cfg in place when config.json changes on disk, and a group added
92
+ // mid-session (`/design:setup-ds` adding `system`) must gate correctly.
93
+ const groupPaths = () => (ctx.cfg.canvasGroups ?? []).map((g) => g.path);
90
94
 
91
95
  // null = not yet seeded; the first refresh captures the baseline and emits
92
96
  // nothing (canvases already on disk at boot aren't "added").
@@ -156,7 +160,7 @@ export function createCanvasListWatch(
156
160
  }
157
161
 
158
162
  const off = ctx.bus.on('fs:any', (rel: string) => {
159
- if (!isCanvasCandidate(rel, groupPaths)) return;
163
+ if (!isCanvasCandidate(rel, groupPaths())) return;
160
164
  schedule();
161
165
  });
162
166