@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.
Files changed (73) hide show
  1. package/apps/studio/ai-banner.tsx +2 -2
  2. package/apps/studio/annotations-layer.tsx +53 -2
  3. package/apps/studio/api.ts +504 -3
  4. package/apps/studio/artboard-marquee.tsx +1 -1
  5. package/apps/studio/bin/_canvas-rects-playwright.mjs +55 -0
  6. package/apps/studio/bin/_canvas-rects-static.mjs +166 -0
  7. package/apps/studio/bin/_fetch-asset.mjs +556 -0
  8. package/apps/studio/bin/annotate.mjs +576 -34
  9. package/apps/studio/bin/canvas-rects.sh +152 -0
  10. package/apps/studio/bin/fetch-asset.sh +34 -0
  11. package/apps/studio/bin/read-annotations.mjs +138 -7
  12. package/apps/studio/bin/smoke.sh +42 -6
  13. package/apps/studio/build.ts +21 -0
  14. package/apps/studio/canvas-comment-mount.tsx +138 -4
  15. package/apps/studio/canvas-edit.ts +744 -11
  16. package/apps/studio/canvas-lib.tsx +219 -2
  17. package/apps/studio/canvas-shell.tsx +487 -20
  18. package/apps/studio/client/app.jsx +1451 -22
  19. package/apps/studio/client/comments-overlay.css +130 -126
  20. package/apps/studio/client/github.js +8 -0
  21. package/apps/studio/client/styles/3-shell-maude.css +48 -0
  22. package/apps/studio/comments-overlay.tsx +148 -41
  23. package/apps/studio/context-menu.tsx +15 -5
  24. package/apps/studio/contextual-toolbar.tsx +262 -4
  25. package/apps/studio/cursors-overlay.tsx +4 -4
  26. package/apps/studio/dist/client.bundle.js +20 -20
  27. package/apps/studio/dist/comment-mount.js +59 -1
  28. package/apps/studio/dist/styles.css +1 -1
  29. package/apps/studio/dom-selection.ts +127 -1
  30. package/apps/studio/drag-state.ts +24 -0
  31. package/apps/studio/equal-spacing-detector.ts +205 -0
  32. package/apps/studio/export-dialog.tsx +1 -1
  33. package/apps/studio/history.ts +47 -1
  34. package/apps/studio/http.ts +223 -0
  35. package/apps/studio/input-router.tsx +12 -0
  36. package/apps/studio/marquee-overlay.tsx +1 -1
  37. package/apps/studio/measure-overlay.tsx +241 -0
  38. package/apps/studio/participants-chrome.tsx +3 -3
  39. package/apps/studio/sizing-mode.ts +117 -0
  40. package/apps/studio/spacing-handles.ts +166 -0
  41. package/apps/studio/test/annotate-write.test.ts +890 -0
  42. package/apps/studio/test/camera-reveal.test.tsx +173 -0
  43. package/apps/studio/test/canvas-edit.test.ts +50 -0
  44. package/apps/studio/test/canvas-origin-gate.test.ts +18 -0
  45. package/apps/studio/test/canvas-rects.test.ts +198 -0
  46. package/apps/studio/test/comments-overlay.test.ts +117 -0
  47. package/apps/studio/test/dom-selection.test.ts +130 -0
  48. package/apps/studio/test/edit-css-occurrence.test.ts +81 -0
  49. package/apps/studio/test/edit-scope-api.test.ts +115 -0
  50. package/apps/studio/test/element-resize.test.ts +136 -0
  51. package/apps/studio/test/element-structural-api.test.ts +360 -0
  52. package/apps/studio/test/element-structural-edit.test.ts +233 -0
  53. package/apps/studio/test/equal-spacing-detector.test.ts +165 -1
  54. package/apps/studio/test/history-rollback.test.ts +26 -0
  55. package/apps/studio/test/knob-props-authored.test.ts +87 -0
  56. package/apps/studio/test/read-annotations.test.ts +154 -0
  57. package/apps/studio/test/sizing-mode.test.ts +102 -0
  58. package/apps/studio/test/spacing-handles.test.ts +138 -0
  59. package/apps/studio/test/specimen-select.test.ts +88 -0
  60. package/apps/studio/test/tool-palette-insert-anchor.test.ts +84 -0
  61. package/apps/studio/test/undo-sequence-byte-compare.test.ts +211 -0
  62. package/apps/studio/tool-palette.tsx +122 -2
  63. package/apps/studio/undo-hud.tsx +2 -2
  64. package/apps/studio/use-element-resize.tsx +732 -0
  65. package/apps/studio/use-keyboard-discipline.tsx +205 -15
  66. package/apps/studio/use-selection-set.tsx +14 -0
  67. package/apps/studio/use-spacing-handles.tsx +388 -0
  68. package/apps/studio/whats-new.json +28 -0
  69. package/cli/commands/design.mjs +6 -1
  70. package/cli/commands/design.test.mjs +49 -1
  71. package/cli/lib/fetch-asset.test.mjs +213 -0
  72. package/package.json +8 -8
  73. 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
- 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
+ }
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={{ 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
+ }}
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