@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
@@ -16,7 +16,9 @@ import { useEffect, useRef, useState } from 'react';
16
16
 
17
17
  import {
18
18
  IconChevronDown,
19
+ IconLetterA,
19
20
  IconPresentation,
21
+ IconSquare,
20
22
  SHAPE_KIND_ICONS,
21
23
  TOOL_ICONS,
22
24
  } from './canvas-icons.tsx';
@@ -214,6 +216,35 @@ const PALETTE_CSS = `
214
216
  background: color-mix(in oklab, var(--maude-hud-accent, #d63b1f) 14%, transparent);
215
217
  color: var(--maude-hud-accent, #d63b1f);
216
218
  }
219
+ /* Stage I3 tail — "+ Element" insert popover. A vertical labeled list (not the
220
+ shape popover's icon grid): Div/Text/Image have no recognizable glyph on
221
+ their own, and this is a rare, must-be-legible action, not a recalled tool. */
222
+ .dc-tool-palette .dc-tp-insert { position: relative; display: inline-flex; }
223
+ .dc-tp-insert-popover {
224
+ position: absolute;
225
+ bottom: calc(100% + 8px);
226
+ left: 50%;
227
+ transform: translateX(-50%);
228
+ display: flex;
229
+ flex-direction: column;
230
+ gap: 2px;
231
+ padding: 4px;
232
+ min-width: 128px;
233
+ background: var(--maude-chrome-bg-0, #ffffff);
234
+ border: 1px solid var(--maude-chrome-fg-0, #1c1917);
235
+ border-radius: 8px;
236
+ box-shadow: 0 6px 24px var(--maude-chrome-shadow, color-mix(in oklab, #1c1917 10%, transparent));
237
+ z-index: 8;
238
+ }
239
+ .dc-tp-insert-popover button {
240
+ width: auto;
241
+ height: auto;
242
+ justify-content: flex-start;
243
+ gap: 8px;
244
+ padding: 6px 8px;
245
+ border-radius: 4px;
246
+ }
247
+ .dc-tp-insert-popover button span.dc-tp-insert-label { font-size: 12px; }
217
248
  `.trim();
218
249
 
219
250
  function ensurePaletteStyles(): void {
@@ -250,6 +281,34 @@ const SHAPE_KINDS: ReadonlyArray<{ kind: ShapeKind; label: string }> = [
250
281
  { kind: 'triangle-down', label: 'Triangle down' },
251
282
  ];
252
283
 
284
+ /**
285
+ * Stage I3 tail — resolve the insert anchor for the tool-palette "+ Element"
286
+ * affordance's "or appends to the active artboard" mode (the plan's fallback
287
+ * when the user hasn't right-clicked a specific element). Growth-only: an
288
+ * artboard's `<article data-dc-screen>` carries NO `data-cd-id` of its own
289
+ * (`DCArtboard` doesn't forward it to the DOM — only its SOURCE JSX tag is
290
+ * stamped, unresolvable from here), so `applyInsertElement`'s `refId` can't
291
+ * target the artboard directly. Anchor on the artboard's LAST direct child
292
+ * that DOES carry a `data-cd-id` and insert `position:'after'` it — the exact
293
+ * relation the context-menu's element-relative insert already uses. Returns
294
+ * `null` for an empty (or artboard-less) canvas — a documented v1 limit; the
295
+ * per-element context-menu "Insert ▸ …" still works there once a first
296
+ * element exists. `doc` is injectable for testing without a real iframe.
297
+ */
298
+ export function resolveInsertAnchor(
299
+ doc: Document = document
300
+ ): { refId: string; position: 'after' } | null {
301
+ const current = doc.querySelector('[data-dc-screen][aria-current="true"]');
302
+ const artboardEl = current ?? doc.querySelector('[data-dc-screen]');
303
+ if (!artboardEl) return null;
304
+ const body = artboardEl.querySelector(':scope > .dc-artboard-body');
305
+ if (!body) return null;
306
+ const kids = Array.from(body.children).reverse();
307
+ const last = kids.find((el) => el.hasAttribute('data-cd-id'));
308
+ const refId = last?.getAttribute('data-cd-id');
309
+ return refId ? { refId, position: 'after' } : null;
310
+ }
311
+
253
312
  // ─────────────────────────────────────────────────────────────────────────────
254
313
  // Component
255
314
 
@@ -259,19 +318,22 @@ export function ToolPalette() {
259
318
  const chrome = useChromeVisibility();
260
319
  const [mounted, setMounted] = useState(false);
261
320
  const [shapeOpen, setShapeOpen] = useState(false);
321
+ const [insertOpen, setInsertOpen] = useState(false);
262
322
  const containerRef = useRef<HTMLDivElement | null>(null);
263
323
 
264
324
  useEffect(() => setMounted(true), []);
265
325
  useEffect(() => {
266
- if (!shapeOpen) return;
326
+ if (!shapeOpen && !insertOpen) return;
267
327
  const onDown = (e: PointerEvent) => {
268
328
  if (!containerRef.current?.contains(e.target as Node)) {
269
329
  setShapeOpen(false);
330
+ setInsertOpen(false);
270
331
  }
271
332
  };
272
333
  const onKey = (e: KeyboardEvent) => {
273
334
  if (e.key === 'Escape') {
274
335
  setShapeOpen(false);
336
+ setInsertOpen(false);
275
337
  }
276
338
  };
277
339
  document.addEventListener('pointerdown', onDown, true);
@@ -280,12 +342,38 @@ export function ToolPalette() {
280
342
  document.removeEventListener('pointerdown', onDown, true);
281
343
  document.removeEventListener('keydown', onKey, true);
282
344
  };
283
- }, [shapeOpen]);
345
+ }, [shapeOpen, insertOpen]);
284
346
  // Close the shape popover whenever the tool changes away from Shape.
285
347
  useEffect(() => {
286
348
  if (tool !== 'shape') setShapeOpen(false);
287
349
  }, [tool]);
288
350
 
351
+ // Stage I3 tail — insert a Div/Text/Image relative to the active artboard's
352
+ // last existing element (`resolveInsertAnchor`). Same request verbs the
353
+ // context-menu's per-element "Insert ▸ …" already posts (`insert-request` /
354
+ // `insert-image-request`, main-origin shell WRITES — DDR-054); a null anchor
355
+ // (empty/artboard-less canvas) is a silent no-op — a documented v1 limit.
356
+ const insertViaPalette = (kind: 'div' | 'text' | 'image') => {
357
+ setInsertOpen(false);
358
+ const anchor = resolveInsertAnchor();
359
+ if (!anchor) return;
360
+ try {
361
+ if (kind === 'image') {
362
+ window.parent.postMessage(
363
+ { dgn: 'insert-image-request', refId: anchor.refId, position: anchor.position },
364
+ '*'
365
+ );
366
+ } else {
367
+ window.parent.postMessage(
368
+ { dgn: 'insert-request', refId: anchor.refId, position: anchor.position, kind },
369
+ '*'
370
+ );
371
+ }
372
+ } catch {
373
+ /* detached / cross-origin teardown */
374
+ }
375
+ };
376
+
289
377
  if (!mounted) return null;
290
378
  // Presentation Mode hides the whole tool palette (clean artboards-only view).
291
379
  if (chrome?.present) return null;
@@ -393,6 +481,38 @@ export function ToolPalette() {
393
481
  </div>
394
482
  <div className="dc-tp-sep" />
395
483
  <div className="dc-tp-group">
484
+ <span className="dc-tp-insert">
485
+ <button
486
+ type="button"
487
+ aria-label="Insert element — Div, Text, or Image"
488
+ aria-haspopup="menu"
489
+ aria-expanded={insertOpen}
490
+ title="Insert element (appends to the active artboard)"
491
+ onClick={() => setInsertOpen((o) => !o)}
492
+ >
493
+ <span aria-hidden="true" style={{ fontSize: 16, lineHeight: 1 }}>
494
+ +
495
+ </span>
496
+ </button>
497
+ {insertOpen ? (
498
+ <div className="dc-tp-insert-popover" role="menu" aria-label="Insert element">
499
+ <button type="button" role="menuitem" onClick={() => insertViaPalette('div')}>
500
+ <IconSquare />
501
+ <span className="dc-tp-insert-label">Div</span>
502
+ </button>
503
+ <button type="button" role="menuitem" onClick={() => insertViaPalette('text')}>
504
+ <IconLetterA />
505
+ <span className="dc-tp-insert-label">Text</span>
506
+ </button>
507
+ <button type="button" role="menuitem" onClick={() => insertViaPalette('image')}>
508
+ <span aria-hidden="true" style={{ fontSize: 13, lineHeight: 1 }}>
509
+
510
+ </span>
511
+ <span className="dc-tp-insert-label">Image</span>
512
+ </button>
513
+ </div>
514
+ ) : null}
515
+ </span>
396
516
  <button
397
517
  type="button"
398
518
  aria-label="Export (⌘E)"
@@ -32,12 +32,12 @@ const HUD_CSS = `
32
32
  user-select: none;
33
33
  opacity: 0;
34
34
  transform: translateY(-4px);
35
- transition: opacity var(--dur-base, 200ms) linear, transform var(--dur-base, 200ms) ease-out;
35
+ transition: opacity 200ms linear, transform 200ms ease-out;
36
36
  }
37
37
  .dc-undo-hud[data-visible="true"] {
38
38
  opacity: 1;
39
39
  transform: translateY(0);
40
- transition-duration: var(--dur-fast, 120ms);
40
+ transition-duration: 120ms;
41
41
  }
42
42
  @media (prefers-reduced-motion: reduce) {
43
43
  .dc-undo-hud {