@1agh/maude 0.40.0 → 0.42.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 (102) 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 -44
  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/_html-playwright.mjs +26 -4
  9. package/apps/studio/bin/_pdf-playwright.mjs +13 -2
  10. package/apps/studio/bin/_png-playwright.mjs +15 -2
  11. package/apps/studio/bin/_pptx-playwright.mjs +17 -4
  12. package/apps/studio/bin/_screenshot-playwright.mjs +17 -2
  13. package/apps/studio/bin/_svg-playwright.mjs +26 -4
  14. package/apps/studio/bin/annotate.mjs +576 -34
  15. package/apps/studio/bin/canvas-rects.sh +152 -0
  16. package/apps/studio/bin/fetch-asset.sh +34 -0
  17. package/apps/studio/bin/read-annotations.mjs +138 -7
  18. package/apps/studio/bin/screenshot.sh +53 -4
  19. package/apps/studio/bin/smoke.sh +42 -6
  20. package/apps/studio/build.ts +21 -0
  21. package/apps/studio/canvas-comment-mount.tsx +138 -4
  22. package/apps/studio/canvas-edit.ts +744 -11
  23. package/apps/studio/canvas-lib.tsx +219 -2
  24. package/apps/studio/canvas-shell.tsx +487 -20
  25. package/apps/studio/client/app.jsx +1476 -76
  26. package/apps/studio/client/comments-overlay.css +130 -126
  27. package/apps/studio/client/export-center.jsx +426 -0
  28. package/apps/studio/client/github.js +8 -0
  29. package/apps/studio/client/styles/3-shell-maude.css +65 -0
  30. package/apps/studio/client/styles/4-components.css +150 -0
  31. package/apps/studio/comments-overlay.tsx +148 -41
  32. package/apps/studio/config.schema.json +2 -2
  33. package/apps/studio/context-menu.tsx +15 -5
  34. package/apps/studio/contextual-toolbar.tsx +262 -4
  35. package/apps/studio/cursors-overlay.tsx +4 -4
  36. package/apps/studio/dist/client.bundle.js +20 -20
  37. package/apps/studio/dist/comment-mount.js +59 -1
  38. package/apps/studio/dist/styles.css +1 -1
  39. package/apps/studio/dom-selection.ts +127 -1
  40. package/apps/studio/drag-state.ts +24 -0
  41. package/apps/studio/equal-spacing-detector.ts +205 -0
  42. package/apps/studio/export-dialog.tsx +19 -26
  43. package/apps/studio/exporters/_runtime.ts +104 -0
  44. package/apps/studio/exporters/html.ts +12 -20
  45. package/apps/studio/exporters/index.ts +14 -2
  46. package/apps/studio/exporters/jobs.ts +334 -0
  47. package/apps/studio/exporters/pdf.ts +16 -20
  48. package/apps/studio/exporters/png.ts +12 -20
  49. package/apps/studio/exporters/pptx.ts +22 -23
  50. package/apps/studio/exporters/scope.ts +1 -0
  51. package/apps/studio/exporters/svg.ts +14 -22
  52. package/apps/studio/exporters/video.ts +15 -17
  53. package/apps/studio/git/service.ts +3 -1
  54. package/apps/studio/history.ts +47 -1
  55. package/apps/studio/http.ts +368 -50
  56. package/apps/studio/input-router.tsx +12 -0
  57. package/apps/studio/marquee-overlay.tsx +1 -1
  58. package/apps/studio/measure-overlay.tsx +241 -0
  59. package/apps/studio/participants-chrome.tsx +3 -3
  60. package/apps/studio/server.ts +3 -1
  61. package/apps/studio/sizing-mode.ts +117 -0
  62. package/apps/studio/spacing-handles.ts +166 -0
  63. package/apps/studio/test/annotate-write.test.ts +890 -0
  64. package/apps/studio/test/camera-reveal.test.tsx +173 -0
  65. package/apps/studio/test/canvas-edit.test.ts +50 -0
  66. package/apps/studio/test/canvas-origin-gate.test.ts +24 -0
  67. package/apps/studio/test/canvas-rects.test.ts +198 -0
  68. package/apps/studio/test/comments-overlay.test.ts +117 -0
  69. package/apps/studio/test/dns-rebinding-guard.test.ts +27 -0
  70. package/apps/studio/test/dom-selection.test.ts +130 -0
  71. package/apps/studio/test/edit-css-occurrence.test.ts +81 -0
  72. package/apps/studio/test/edit-scope-api.test.ts +115 -0
  73. package/apps/studio/test/element-resize.test.ts +136 -0
  74. package/apps/studio/test/element-structural-api.test.ts +360 -0
  75. package/apps/studio/test/element-structural-edit.test.ts +233 -0
  76. package/apps/studio/test/equal-spacing-detector.test.ts +165 -1
  77. package/apps/studio/test/export-center.test.tsx +287 -0
  78. package/apps/studio/test/export-shim-multi-capture.test.ts +83 -0
  79. package/apps/studio/test/exporters/history.test.ts +32 -3
  80. package/apps/studio/test/exporters/jobs.test.ts +263 -0
  81. package/apps/studio/test/history-rollback.test.ts +26 -0
  82. package/apps/studio/test/knob-props-authored.test.ts +87 -0
  83. package/apps/studio/test/read-annotations.test.ts +154 -0
  84. package/apps/studio/test/sizing-mode.test.ts +102 -0
  85. package/apps/studio/test/spacing-handles.test.ts +138 -0
  86. package/apps/studio/test/specimen-select.test.ts +88 -0
  87. package/apps/studio/test/tool-palette-insert-anchor.test.ts +84 -0
  88. package/apps/studio/test/undo-sequence-byte-compare.test.ts +211 -0
  89. package/apps/studio/tool-palette.tsx +122 -2
  90. package/apps/studio/undo-hud.tsx +2 -2
  91. package/apps/studio/use-element-resize.tsx +732 -0
  92. package/apps/studio/use-keyboard-discipline.tsx +205 -15
  93. package/apps/studio/use-selection-set.tsx +14 -0
  94. package/apps/studio/use-spacing-handles.tsx +388 -0
  95. package/apps/studio/whats-new.json +45 -0
  96. package/apps/studio/ws.ts +6 -0
  97. package/cli/commands/design.mjs +6 -1
  98. package/cli/commands/design.test.mjs +49 -1
  99. package/cli/lib/fetch-asset.test.mjs +213 -0
  100. package/cli/lib/gitignore-block.mjs +1 -0
  101. package/package.json +8 -8
  102. package/plugins/design/dependencies.json +10 -2
@@ -28,10 +28,19 @@
28
28
  * the 180 ms position tween per the plan note.
29
29
  */
30
30
 
31
- import { useEffect, useRef } from 'react';
32
-
31
+ import { type ReactNode, useEffect, useRef } from 'react';
33
32
  import { resolveSelectionEl } from './dom-selection.ts';
34
- import { useSelectionSet } from './use-selection-set.tsx';
33
+ import { isElementDragActive } from './drag-state.ts';
34
+ import {
35
+ type AlignAxisMode,
36
+ computeAlign,
37
+ computeDistribute,
38
+ computeTidyGrid,
39
+ type KeyedRect,
40
+ type MovedRect,
41
+ type SpacingAxis,
42
+ } from './equal-spacing-detector.ts';
43
+ import { type Selection, useSelectionSet } from './use-selection-set.tsx';
35
44
 
36
45
  const CTX_TOOLBAR_CSS = `
37
46
  .dc-elem-ctx-tb {
@@ -66,11 +75,17 @@ const CTX_TOOLBAR_CSS = `
66
75
  font: inherit;
67
76
  cursor: pointer;
68
77
  color: inherit;
78
+ display: inline-flex;
79
+ align-items: center;
69
80
  transition: background-color 80ms linear;
70
81
  }
71
- .dc-elem-ctx-tb button:hover {
82
+ .dc-elem-ctx-tb button:hover:not(:disabled) {
72
83
  background: color-mix(in oklab, var(--maude-hud-accent, #d63b1f) 8%, transparent);
73
84
  }
85
+ .dc-elem-ctx-tb button:disabled {
86
+ cursor: default;
87
+ opacity: 0.4;
88
+ }
74
89
  .dc-elem-ctx-tb .dc-elem-ctx-count {
75
90
  padding: 4px 8px 4px 10px;
76
91
  color: var(--maude-chrome-fg-1, rgba(40,30,20,0.7));
@@ -78,6 +93,17 @@ const CTX_TOOLBAR_CSS = `
78
93
  margin-right: 2px;
79
94
  font-variant-numeric: tabular-nums;
80
95
  }
96
+ .dc-elem-ctx-tb .dc-elem-ctx-icon {
97
+ display: inline-flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ }
101
+ .dc-elem-ctx-tb .dc-elem-ctx-divider {
102
+ width: 1px;
103
+ align-self: stretch;
104
+ background: var(--maude-chrome-border, rgba(0,0,0,0.10));
105
+ margin: 0 4px;
106
+ }
81
107
  @media (prefers-reduced-motion: reduce) {
82
108
  .dc-elem-ctx-tb, .dc-elem-ctx-tb button { transition: none; }
83
109
  }
@@ -133,6 +159,98 @@ function openComposerForSelection(
133
159
  }
134
160
  }
135
161
 
162
+ interface AlignTarget {
163
+ sel: Selection;
164
+ x: number;
165
+ y: number;
166
+ w: number;
167
+ h: number;
168
+ }
169
+
170
+ /**
171
+ * Resolve every selection to its current world-space box, gated on
172
+ * absolute/fixed positioning — align/distribute/tidy-up write `left`/`top`,
173
+ * which is only meaningful for out-of-flow elements (this codebase
174
+ * deliberately never does an implicit convert-to-absolute-on-drag; see the
175
+ * Stage-D dogfood notes). Returns `null` (all-or-nothing) if any selection
176
+ * doesn't resolve, or isn't out-of-flow, or has a non-numeric `left`/`top`
177
+ * (e.g. positioned via `right`/`bottom` instead — a known v1 limit).
178
+ */
179
+ function resolveAlignTargets(sels: Selection[]): AlignTarget[] | null {
180
+ const out: AlignTarget[] = [];
181
+ for (const sel of sels) {
182
+ const node = resolveSelectionEl(document, sel);
183
+ if (!node) return null;
184
+ const cs = getComputedStyle(node as HTMLElement);
185
+ if (cs.position !== 'absolute' && cs.position !== 'fixed') return null;
186
+ const left = Number.parseFloat(cs.left);
187
+ const top = Number.parseFloat(cs.top);
188
+ if (!Number.isFinite(left) || !Number.isFinite(top)) return null;
189
+ out.push({
190
+ sel,
191
+ x: left,
192
+ y: top,
193
+ w: (node as HTMLElement).offsetWidth,
194
+ h: (node as HTMLElement).offsetHeight,
195
+ });
196
+ }
197
+ return out;
198
+ }
199
+
200
+ /** Post one `reposition-request` per moved rect — the same message shape +
201
+ * write lane (`repositionElement` → `/_api/edit-css` ×2, per-prop undo) an
202
+ * in-canvas element drag already posts. No new endpoint, no new trust
203
+ * boundary; Cmd+Z steps back one element at a time (same N-step precedent
204
+ * as Task L4's paste-style). */
205
+ function commitMoves(targets: AlignTarget[], moved: MovedRect[]): void {
206
+ for (const m of moved) {
207
+ const t = targets[Number(m.key)];
208
+ if (!t?.sel.id) continue;
209
+ try {
210
+ window.parent.postMessage(
211
+ {
212
+ dgn: 'reposition-request',
213
+ id: t.sel.id,
214
+ left: m.x,
215
+ top: m.y,
216
+ beforeLeft: Math.round(t.x),
217
+ beforeTop: Math.round(t.y),
218
+ idIndex: t.sel.index ?? 0,
219
+ },
220
+ '*'
221
+ );
222
+ } catch {
223
+ /* detached / cross-origin */
224
+ }
225
+ }
226
+ }
227
+
228
+ function toKeyedRects(targets: AlignTarget[]): KeyedRect[] {
229
+ return targets.map((t, i) => ({ key: String(i), x: t.x, y: t.y, w: t.w, h: t.h }));
230
+ }
231
+
232
+ function CtxIconButton({
233
+ label,
234
+ title,
235
+ onClick,
236
+ disabled,
237
+ children,
238
+ }: {
239
+ label: string;
240
+ title: string;
241
+ onClick: () => void;
242
+ disabled?: boolean;
243
+ children: ReactNode;
244
+ }) {
245
+ return (
246
+ <button type="button" title={title} aria-label={label} onClick={onClick} disabled={disabled}>
247
+ <span className="dc-elem-ctx-icon" aria-hidden="true">
248
+ {children}
249
+ </span>
250
+ </button>
251
+ );
252
+ }
253
+
136
254
  export function ContextualToolbar() {
137
255
  ensureStyles();
138
256
  const { selected } = useSelectionSet();
@@ -154,6 +272,18 @@ export function ContextualToolbar() {
154
272
  }
155
273
  const tick = () => {
156
274
  rafRef.current = null;
275
+ // Dogfood 2026-07-07 — "hrozně zavaší ten toolbar" during a reorder drag:
276
+ // the floating pill has nothing useful to say mid-drag anyway (Inspect /
277
+ // Copy CSS / Copy ID on an element that's about to move), and its own
278
+ // per-frame resolveSelectionEl + getBoundingClientRect work was
279
+ // compounding with the drag's own DOM churn + the resize/spacing
280
+ // overlays' rAF loops. Hide it for the duration, like Figma/Webflow do.
281
+ if (isElementDragActive()) {
282
+ div.style.display = 'none';
283
+ div.setAttribute('data-on', 'false');
284
+ rafRef.current = requestAnimationFrame(tick);
285
+ return;
286
+ }
157
287
  let xMin = Number.POSITIVE_INFINITY;
158
288
  let yMin = Number.POSITIVE_INFINITY;
159
289
  let xMax = Number.NEGATIVE_INFINITY;
@@ -204,6 +334,30 @@ export function ContextualToolbar() {
204
334
  const primary = elementSelections[0];
205
335
  const count = elementSelections.length;
206
336
 
337
+ // Task L5 — align (≥2) / distribute (≥3) / tidy-up (≥2) for a multi-element
338
+ // selection. Button enablement is count-gated (matching MultiArtboardToolbar's
339
+ // G7 cluster); the absolute/fixed-positioning eligibility check happens lazily
340
+ // on click via `resolveAlignTargets` (a silent no-op otherwise, rather than a
341
+ // per-frame DOM scan just to grey out a button).
342
+ const alignOk = count >= 2;
343
+ const distributeOk = count >= 3;
344
+
345
+ const runAlign = (mode: AlignAxisMode) => {
346
+ const targets = resolveAlignTargets(elementSelections);
347
+ if (!targets) return;
348
+ commitMoves(targets, computeAlign(toKeyedRects(targets), mode));
349
+ };
350
+ const runDistribute = (axis: SpacingAxis) => {
351
+ const targets = resolveAlignTargets(elementSelections);
352
+ if (!targets) return;
353
+ commitMoves(targets, computeDistribute(toKeyedRects(targets), axis));
354
+ };
355
+ const runTidyUp = () => {
356
+ const targets = resolveAlignTargets(elementSelections);
357
+ if (!targets) return;
358
+ commitMoves(targets, computeTidyGrid(toKeyedRects(targets)));
359
+ };
360
+
207
361
  return (
208
362
  <div ref={ref} className="dc-elem-ctx-tb" role="toolbar" aria-label="Element actions">
209
363
  <span className="dc-elem-ctx-count">{count === 1 ? '1 element' : `${count} elements`}</span>
@@ -247,6 +401,110 @@ export function ContextualToolbar() {
247
401
  >
248
402
  Comment
249
403
  </button>
404
+ {alignOk && (
405
+ <>
406
+ <span className="dc-elem-ctx-divider" aria-hidden="true" />
407
+ <CtxIconButton label="Align left" title="Align left" onClick={() => runAlign('left')}>
408
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
409
+ <line x1="0.5" y1="0.5" x2="0.5" y2="13.5" stroke="currentColor" />
410
+ <rect x="1" y="2" width="7" height="3" fill="currentColor" />
411
+ <rect x="1" y="9" width="11" height="3" fill="currentColor" />
412
+ </svg>
413
+ </CtxIconButton>
414
+ <CtxIconButton
415
+ label="Align center horizontally"
416
+ title="Align center (horizontal)"
417
+ onClick={() => runAlign('center-x')}
418
+ >
419
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
420
+ <line x1="7" y1="0.5" x2="7" y2="13.5" stroke="currentColor" />
421
+ <rect x="3.5" y="2" width="7" height="3" fill="currentColor" />
422
+ <rect x="1.5" y="9" width="11" height="3" fill="currentColor" />
423
+ </svg>
424
+ </CtxIconButton>
425
+ <CtxIconButton label="Align right" title="Align right" onClick={() => runAlign('right')}>
426
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
427
+ <line x1="13.5" y1="0.5" x2="13.5" y2="13.5" stroke="currentColor" />
428
+ <rect x="6" y="2" width="7" height="3" fill="currentColor" />
429
+ <rect x="2" y="9" width="11" height="3" fill="currentColor" />
430
+ </svg>
431
+ </CtxIconButton>
432
+ <CtxIconButton label="Align top" title="Align top" onClick={() => runAlign('top')}>
433
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
434
+ <line x1="0.5" y1="0.5" x2="13.5" y2="0.5" stroke="currentColor" />
435
+ <rect x="2" y="1" width="3" height="7" fill="currentColor" />
436
+ <rect x="9" y="1" width="3" height="11" fill="currentColor" />
437
+ </svg>
438
+ </CtxIconButton>
439
+ <CtxIconButton
440
+ label="Align center vertically"
441
+ title="Align center (vertical)"
442
+ onClick={() => runAlign('center-y')}
443
+ >
444
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
445
+ <line x1="0.5" y1="7" x2="13.5" y2="7" stroke="currentColor" />
446
+ <rect x="2" y="3.5" width="3" height="7" fill="currentColor" />
447
+ <rect x="9" y="1.5" width="3" height="11" fill="currentColor" />
448
+ </svg>
449
+ </CtxIconButton>
450
+ <CtxIconButton
451
+ label="Align bottom"
452
+ title="Align bottom"
453
+ onClick={() => runAlign('bottom')}
454
+ >
455
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
456
+ <line x1="0.5" y1="13.5" x2="13.5" y2="13.5" stroke="currentColor" />
457
+ <rect x="2" y="6" width="3" height="7" fill="currentColor" />
458
+ <rect x="9" y="2" width="3" height="11" fill="currentColor" />
459
+ </svg>
460
+ </CtxIconButton>
461
+ <span className="dc-elem-ctx-divider" aria-hidden="true" />
462
+ <CtxIconButton
463
+ label="Distribute horizontally"
464
+ title={
465
+ distributeOk
466
+ ? 'Distribute horizontally — equal gaps between elements'
467
+ : 'Select at least 3 elements to distribute'
468
+ }
469
+ disabled={!distributeOk}
470
+ onClick={() => runDistribute('x')}
471
+ >
472
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
473
+ <rect x="0.5" y="3" width="3" height="8" fill="currentColor" />
474
+ <rect x="5.5" y="3" width="3" height="8" fill="currentColor" />
475
+ <rect x="10.5" y="3" width="3" height="8" fill="currentColor" />
476
+ </svg>
477
+ </CtxIconButton>
478
+ <CtxIconButton
479
+ label="Distribute vertically"
480
+ title={
481
+ distributeOk
482
+ ? 'Distribute vertically — equal gaps between elements'
483
+ : 'Select at least 3 elements to distribute'
484
+ }
485
+ disabled={!distributeOk}
486
+ onClick={() => runDistribute('y')}
487
+ >
488
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
489
+ <rect x="3" y="0.5" width="8" height="3" fill="currentColor" />
490
+ <rect x="3" y="5.5" width="8" height="3" fill="currentColor" />
491
+ <rect x="3" y="10.5" width="8" height="3" fill="currentColor" />
492
+ </svg>
493
+ </CtxIconButton>
494
+ <CtxIconButton
495
+ label="Tidy up into a grid"
496
+ title="Tidy up — snap into a clean grid"
497
+ onClick={runTidyUp}
498
+ >
499
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
500
+ <rect x="0.5" y="0.5" width="5.5" height="5.5" fill="currentColor" />
501
+ <rect x="8" y="0.5" width="5.5" height="5.5" fill="currentColor" />
502
+ <rect x="0.5" y="8" width="5.5" height="5.5" fill="currentColor" />
503
+ <rect x="8" y="8" width="5.5" height="5.5" fill="currentColor" />
504
+ </svg>
505
+ </CtxIconButton>
506
+ </>
507
+ )}
250
508
  </div>
251
509
  );
252
510
  }
@@ -55,7 +55,7 @@ const CURSOR_CSS = `
55
55
  top: 14px;
56
56
  left: 10px;
57
57
  padding: 1px 8px;
58
- font-family: var(--font-mono, ui-monospace, monospace);
58
+ font-family: var(--maude-chrome-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
59
59
  font-weight: 500;
60
60
  font-size: 10px;
61
61
  line-height: 1.4;
@@ -72,7 +72,7 @@ const CURSOR_CSS = `
72
72
  left: 0;
73
73
  pointer-events: none;
74
74
  border: 2px solid;
75
- border-radius: var(--radius-sm, 2px);
75
+ border-radius: 2px;
76
76
  box-sizing: border-box;
77
77
  will-change: transform, width, height;
78
78
  }
@@ -81,12 +81,12 @@ const CURSOR_CSS = `
81
81
  top: -18px;
82
82
  left: -2px;
83
83
  padding: 1px 5px;
84
- font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
84
+ font-family: system-ui, -apple-system, sans-serif;
85
85
  font-weight: 600;
86
86
  font-size: 10px;
87
87
  line-height: 1.3;
88
88
  color: var(--maude-hud-accent-fg, #fff);
89
- border-radius: var(--radius-sm, 2px) var(--radius-sm, 2px) 0 0;
89
+ border-radius: 2px 2px 0 0;
90
90
  white-space: nowrap;
91
91
  }
92
92
  /* Phase 30 / DDR-120 — soft editing-presence. When a peer (human or a bridged