@1agh/maude 0.40.0 → 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.
- package/apps/studio/ai-banner.tsx +2 -2
- package/apps/studio/annotations-layer.tsx +53 -2
- package/apps/studio/api.ts +504 -3
- package/apps/studio/artboard-marquee.tsx +1 -1
- package/apps/studio/bin/_canvas-rects-playwright.mjs +55 -0
- package/apps/studio/bin/_canvas-rects-static.mjs +166 -0
- package/apps/studio/bin/_fetch-asset.mjs +556 -0
- package/apps/studio/bin/annotate.mjs +576 -34
- package/apps/studio/bin/canvas-rects.sh +152 -0
- package/apps/studio/bin/fetch-asset.sh +34 -0
- package/apps/studio/bin/read-annotations.mjs +138 -7
- package/apps/studio/bin/smoke.sh +42 -6
- package/apps/studio/build.ts +21 -0
- package/apps/studio/canvas-comment-mount.tsx +138 -4
- package/apps/studio/canvas-edit.ts +744 -11
- package/apps/studio/canvas-lib.tsx +219 -2
- package/apps/studio/canvas-shell.tsx +487 -20
- package/apps/studio/client/app.jsx +1451 -22
- package/apps/studio/client/comments-overlay.css +130 -126
- package/apps/studio/client/github.js +8 -0
- package/apps/studio/client/styles/3-shell-maude.css +48 -0
- package/apps/studio/comments-overlay.tsx +148 -41
- package/apps/studio/context-menu.tsx +15 -5
- package/apps/studio/contextual-toolbar.tsx +262 -4
- package/apps/studio/cursors-overlay.tsx +4 -4
- package/apps/studio/dist/client.bundle.js +20 -20
- package/apps/studio/dist/comment-mount.js +59 -1
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +127 -1
- package/apps/studio/drag-state.ts +24 -0
- package/apps/studio/equal-spacing-detector.ts +205 -0
- package/apps/studio/export-dialog.tsx +1 -1
- package/apps/studio/history.ts +47 -1
- package/apps/studio/http.ts +223 -0
- package/apps/studio/input-router.tsx +12 -0
- package/apps/studio/marquee-overlay.tsx +1 -1
- package/apps/studio/measure-overlay.tsx +241 -0
- package/apps/studio/participants-chrome.tsx +3 -3
- package/apps/studio/sizing-mode.ts +117 -0
- package/apps/studio/spacing-handles.ts +166 -0
- package/apps/studio/test/annotate-write.test.ts +890 -0
- package/apps/studio/test/camera-reveal.test.tsx +173 -0
- package/apps/studio/test/canvas-edit.test.ts +50 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +18 -0
- package/apps/studio/test/canvas-rects.test.ts +198 -0
- package/apps/studio/test/comments-overlay.test.ts +117 -0
- package/apps/studio/test/dom-selection.test.ts +130 -0
- package/apps/studio/test/edit-css-occurrence.test.ts +81 -0
- package/apps/studio/test/edit-scope-api.test.ts +115 -0
- package/apps/studio/test/element-resize.test.ts +136 -0
- package/apps/studio/test/element-structural-api.test.ts +360 -0
- package/apps/studio/test/element-structural-edit.test.ts +233 -0
- package/apps/studio/test/equal-spacing-detector.test.ts +165 -1
- package/apps/studio/test/history-rollback.test.ts +26 -0
- package/apps/studio/test/knob-props-authored.test.ts +87 -0
- package/apps/studio/test/read-annotations.test.ts +154 -0
- package/apps/studio/test/sizing-mode.test.ts +102 -0
- package/apps/studio/test/spacing-handles.test.ts +138 -0
- package/apps/studio/test/specimen-select.test.ts +88 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +84 -0
- package/apps/studio/test/undo-sequence-byte-compare.test.ts +211 -0
- package/apps/studio/tool-palette.tsx +122 -2
- package/apps/studio/undo-hud.tsx +2 -2
- package/apps/studio/use-element-resize.tsx +732 -0
- package/apps/studio/use-keyboard-discipline.tsx +205 -15
- package/apps/studio/use-selection-set.tsx +14 -0
- package/apps/studio/use-spacing-handles.tsx +388 -0
- package/apps/studio/whats-new.json +28 -0
- package/cli/commands/design.mjs +6 -1
- package/cli/commands/design.test.mjs +49 -1
- package/cli/lib/fetch-asset.test.mjs +213 -0
- package/package.json +8 -8
- package/plugins/design/dependencies.json +10 -2
|
@@ -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 {
|
|
@@ -246,6 +257,28 @@ const ENGINE_CSS = `
|
|
|
246
257
|
display: flex;
|
|
247
258
|
flex-direction: column;
|
|
248
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;
|
|
249
282
|
}
|
|
250
283
|
.dc-canvas .dc-artboard.dc-positioned { position: absolute; }
|
|
251
284
|
.dc-canvas .dc-artboard-label {
|
|
@@ -685,6 +718,15 @@ function fitRectIntoHost(rect: ArtboardRect, hostEl: HTMLElement, pad = 24): Vie
|
|
|
685
718
|
// then. Written on every applyViewport (exact — no settle/publish lag).
|
|
686
719
|
let liveViewport: ViewportState | null = null;
|
|
687
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
|
+
|
|
688
730
|
export function useViewportController(opts: ViewportControllerOptions): ViewportControllerHandle {
|
|
689
731
|
const {
|
|
690
732
|
hostRef,
|
|
@@ -704,6 +746,13 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
704
746
|
const [isInteracting, setIsInteracting] = useState(false);
|
|
705
747
|
const interactingRef = useRef(false);
|
|
706
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);
|
|
707
756
|
|
|
708
757
|
// Throttle / settle timers.
|
|
709
758
|
const publishTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -788,6 +837,9 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
788
837
|
interactingRef.current = true;
|
|
789
838
|
if (!isInteractingStateRef.current) {
|
|
790
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;
|
|
791
843
|
setIsInteracting(true);
|
|
792
844
|
}
|
|
793
845
|
if (interactEndTimerRef.current != null) clearTimeout(interactEndTimerRef.current);
|
|
@@ -799,7 +851,12 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
799
851
|
// Motion has stopped — re-write the world transform in crisp mode so the
|
|
800
852
|
// WebKit path drops its compositor layer and re-rasterizes text sharply at
|
|
801
853
|
// the settled scale. No-op on the Blink path (CSS `zoom` is already crisp).
|
|
802
|
-
|
|
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
|
+
}
|
|
803
860
|
}, 220);
|
|
804
861
|
}, [writeTransform]);
|
|
805
862
|
|
|
@@ -957,6 +1014,7 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
957
1014
|
const initial = getInitial();
|
|
958
1015
|
if (initial) {
|
|
959
1016
|
vpRef.current = { ...initial };
|
|
1017
|
+
liveViewport = { ...initial }; // mirror pre-first-gesture so headless capture sees it
|
|
960
1018
|
writeTransform(vpRef.current, { crisp: true });
|
|
961
1019
|
setViewportPublished({ ...vpRef.current });
|
|
962
1020
|
}
|
|
@@ -970,6 +1028,7 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
970
1028
|
hadSize = true;
|
|
971
1029
|
const refit = computeFitRef.current();
|
|
972
1030
|
vpRef.current = { ...refit };
|
|
1031
|
+
liveViewport = { ...refit };
|
|
973
1032
|
writeTransform(vpRef.current, { crisp: true });
|
|
974
1033
|
setViewportPublished({ ...vpRef.current });
|
|
975
1034
|
}
|
|
@@ -1791,7 +1850,23 @@ export function DCArtboard({
|
|
|
1791
1850
|
className={`dc-artboard dc-positioned${isInDrag ? ' dc-dragging' : ''}`}
|
|
1792
1851
|
data-dc-screen={id}
|
|
1793
1852
|
aria-current={isActive ? 'true' : undefined}
|
|
1794
|
-
style={{
|
|
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
|
+
}}
|
|
1795
1870
|
{...handleProps}
|
|
1796
1871
|
>
|
|
1797
1872
|
<button type="button" className="dc-artboard-label sku" aria-label={`Artboard ${label}`}>
|
|
@@ -1812,6 +1887,148 @@ export function DCArtboard({
|
|
|
1812
1887
|
}
|
|
1813
1888
|
DCArtboard.displayName = 'DCArtboard';
|
|
1814
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
|
+
|
|
1815
2032
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1816
2033
|
// DrawProof (Phase 25) — the render/verify harness for the draw engine. Renders
|
|
1817
2034
|
// ONE vector mark across a size ladder × {light, dark, single-color flatten} as
|