@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
@@ -42,7 +42,14 @@ import { createRoot } from 'react-dom/client';
42
42
 
43
43
  import { CommentsOverlay } from './comments-overlay.tsx';
44
44
  import { deriveFile, hoverTargetToSelection } from './dom-selection.ts';
45
- import { type RouterAction, resolveHoverTarget, useInputRouter } from './input-router.tsx';
45
+ import {
46
+ type HoverTarget,
47
+ isOverlayTarget,
48
+ type RouterAction,
49
+ resolveHoverTarget,
50
+ useInputRouter,
51
+ } from './input-router.tsx';
52
+ import { ElementResizeOverlay } from './use-element-resize.tsx';
46
53
  import {
47
54
  MaybeSelectionSetProvider,
48
55
  type Selection,
@@ -83,7 +90,7 @@ const COMMENT_CLAIMS: ReadonlySet<RouterAction['kind']> = new Set<RouterAction['
83
90
  // bare DS specimen. On a UI canvas (`.dc-canvas` present) the inner shell owns
84
91
  // hover-halo painting + `resolveHoverTarget` element anchoring, so the lite
85
92
  // layer defers to it.
86
- function isBareSpecimen(): boolean {
93
+ export function isBareSpecimen(): boolean {
87
94
  return typeof document !== 'undefined' && !document.querySelector('.dc-canvas');
88
95
  }
89
96
 
@@ -94,8 +101,12 @@ function pickSpecimenEl(clientX: number, clientY: number): HTMLElement | null {
94
101
  if (typeof document === 'undefined') return null;
95
102
  const hit = document.elementFromPoint(clientX, clientY) as HTMLElement | null;
96
103
  if (!hit) return null;
97
- // Never anchor to comment chrome or the document root.
98
- if (hit.closest('.cm-composer, .cm-thread, .cm-mention-popup, .cm-pin, [data-mc-hover-halo]')) {
104
+ // Never anchor to comment chrome, the selection/resize overlay, or the root.
105
+ if (
106
+ hit.closest(
107
+ '.cm-composer, .cm-thread, .cm-mention-popup, .cm-pin, [data-mc-hover-halo], [data-mc-selection-halo], .dc-el-resize-handle'
108
+ )
109
+ ) {
99
110
  return null;
100
111
  }
101
112
  const tag = hit.tagName;
@@ -103,6 +114,20 @@ function pickSpecimenEl(clientX: number, clientY: number): HTMLElement | null {
103
114
  return hit;
104
115
  }
105
116
 
117
+ // feature-element-editing-robustness Stage E — the SELECT anchor for a bare
118
+ // specimen. Generalizes `pickSpecimenEl` (the comment anchor) by climbing to the
119
+ // hit element's own `data-cd-id`, else its nearest stamped ancestor — every JSX
120
+ // element the pipeline stamps unconditionally (canvas-pipeline.ts), so a
121
+ // specimen element always resolves to a cd-id the Inspector's `edit-css`/
122
+ // `edit-attr` can target. Falls back to the bare hit when (defensively) nothing
123
+ // is stamped, in which case the selection degrades to a `cssPath` selector.
124
+ export function pickSpecimenSelectEl(clientX: number, clientY: number): HTMLElement | null {
125
+ const hit = pickSpecimenEl(clientX, clientY);
126
+ if (!hit) return null;
127
+ const stamped = hit.closest('[data-cd-id]') as HTMLElement | null;
128
+ return stamped ?? hit;
129
+ }
130
+
106
131
  function CommentHost({ children, file }: { children: ReactNode; file: string | undefined }) {
107
132
  const { tool, setTool } = useToolMode();
108
133
  const selSet = useSelectionSet();
@@ -120,6 +145,57 @@ function CommentHost({ children, file }: { children: ReactNode; file: string | u
120
145
  if (tool !== 'comment') setHoverEl(null);
121
146
  }, [tool]);
122
147
 
148
+ // feature-element-editing-robustness Stage E — element SELECT on a bare
149
+ // specimen. `selectedEl` drives the selection halo; `isSpecimen` gates the
150
+ // resize overlay so it mounts on specimens ONLY (a UI canvas already mounts
151
+ // its own inside CanvasShell — mounting a second here would double the handles).
152
+ const [selectedEl, setSelectedEl] = useState<HTMLElement | null>(null);
153
+ const [isSpecimen, setIsSpecimen] = useState(false);
154
+ useEffect(() => {
155
+ // The inner DesignCanvas (`.dc-canvas`) mounts a beat after this layer, so
156
+ // re-check on the next frame + a short timeout before deciding.
157
+ const check = () => setIsSpecimen(isBareSpecimen());
158
+ check();
159
+ const raf = requestAnimationFrame(check);
160
+ const t = setTimeout(check, 120);
161
+ return () => {
162
+ cancelAnimationFrame(raf);
163
+ clearTimeout(t);
164
+ };
165
+ }, []);
166
+
167
+ // Specimen select — a capture-phase Cmd/Ctrl-click. Self-gated on
168
+ // `isBareSpecimen()` at event time: a UI canvas has its own CanvasShell select
169
+ // router (specimens have none), so this is the ONLY select handler on a
170
+ // specimen and a pure no-op on a UI canvas — no double-handling, no need to
171
+ // touch the shared COMMENT_CLAIMS (which would preventDefault a UI canvas's
172
+ // own select). `selSet.replace` posts `dgn:'select-set'` to the parent shell,
173
+ // so the Inspector opens (Stage C) + edits persist via `edit-css` (Stage E3).
174
+ useEffect(() => {
175
+ if (typeof document === 'undefined') return;
176
+ const onDown = (e: PointerEvent) => {
177
+ if (e.button !== 0 || !(e.metaKey || e.ctrlKey)) return; // select gesture only
178
+ if (!isBareSpecimen()) return; // UI canvas → CanvasShell owns select
179
+ if (isOverlayTarget(e.target)) return; // comment chrome owns its clicks
180
+ const el = pickSpecimenSelectEl(e.clientX, e.clientY);
181
+ if (!el) return;
182
+ e.preventDefault();
183
+ e.stopImmediatePropagation();
184
+ const cdId = el.getAttribute('data-cd-id');
185
+ const sel = hoverTargetToSelection({ el, cdId, artboardId: null } as HoverTarget);
186
+ if (e.shiftKey) selSet.add(sel);
187
+ else selSet.replace(sel);
188
+ setSelectedEl(el);
189
+ };
190
+ document.addEventListener('pointerdown', onDown, true);
191
+ return () => document.removeEventListener('pointerdown', onDown, true);
192
+ }, [selSet]);
193
+
194
+ // Drop the selection halo when the selection clears (Esc / parent force-clear).
195
+ useEffect(() => {
196
+ if (selSet.selected.length === 0) setSelectedEl(null);
197
+ }, [selSet.selected]);
198
+
123
199
  // Reflect the active tool onto the host (and body, since the host is
124
200
  // display:contents and can't carry a paintable cursor). Comment-mode CSS
125
201
  // keys off `[data-active-tool="comment"]`.
@@ -187,11 +263,69 @@ function CommentHost({ children, file }: { children: ReactNode; file: string | u
187
263
  <div data-mc-host ref={hostRef} style={{ display: 'contents' }}>
188
264
  {children}
189
265
  {hoverEl ? <MountHoverHalo el={hoverEl} /> : null}
266
+ {selectedEl ? <MountSelectionHalo el={selectedEl} /> : null}
267
+ {isSpecimen ? <ElementResizeOverlay /> : null}
190
268
  <CommentsOverlay />
191
269
  </div>
192
270
  );
193
271
  }
194
272
 
273
+ // ─────────────────────────────────────────────────────────────────────────────
274
+ // MountSelectionHalo — Stage E. A steadier accent outline around the SELECTED
275
+ // specimen element (vs the lighter hover halo). rAF-follows the element's screen
276
+ // box; inline-styled (a bare specimen doesn't load canvas-lib's HALO_CSS).
277
+
278
+ function MountSelectionHalo({ el }: { el: HTMLElement }) {
279
+ const ref = useRef<HTMLDivElement | null>(null);
280
+ const targetRef = useRef<HTMLElement>(el);
281
+ targetRef.current = el;
282
+ const rafRef = useRef<number | null>(null);
283
+
284
+ useEffect(() => {
285
+ const tick = () => {
286
+ rafRef.current = null;
287
+ const div = ref.current;
288
+ const t = targetRef.current;
289
+ if (div && t?.isConnected) {
290
+ const r = t.getBoundingClientRect();
291
+ if (r.width === 0 && r.height === 0) {
292
+ div.style.display = 'none';
293
+ } else {
294
+ div.style.display = 'block';
295
+ div.style.left = `${Math.round(r.left)}px`;
296
+ div.style.top = `${Math.round(r.top)}px`;
297
+ div.style.width = `${Math.round(r.width)}px`;
298
+ div.style.height = `${Math.round(r.height)}px`;
299
+ }
300
+ } else if (div) {
301
+ div.style.display = 'none';
302
+ }
303
+ rafRef.current = requestAnimationFrame(tick);
304
+ };
305
+ rafRef.current = requestAnimationFrame(tick);
306
+ return () => {
307
+ if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
308
+ };
309
+ }, []);
310
+
311
+ return (
312
+ <div
313
+ ref={ref}
314
+ aria-hidden="true"
315
+ data-mc-selection-halo=""
316
+ style={{
317
+ position: 'fixed',
318
+ display: 'none',
319
+ pointerEvents: 'none',
320
+ zIndex: 2147483645,
321
+ border: '1.5px solid var(--maude-hud-accent, oklch(0.680 0.180 268))',
322
+ borderRadius: '3px',
323
+ boxSizing: 'border-box',
324
+ }}
325
+ />
326
+ );
327
+ }
328
+
195
329
  // ─────────────────────────────────────────────────────────────────────────────
196
330
  // MountHoverHalo — fixed-position outline tracking the hovered element's screen
197
331
  // bounds via rAF. Inline-styled (no dependency on canvas-lib's HALO_CSS, which