@1agh/maude 0.19.1 → 0.20.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/package.json +9 -9
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +112 -38
- package/plugins/design/dev-server/annotations-layer.tsx +204 -95
- package/plugins/design/dev-server/artboard-marquee.tsx +8 -4
- package/plugins/design/dev-server/bin/asset-sweep.sh +180 -0
- package/plugins/design/dev-server/bin/visual-sanity.sh +221 -0
- package/plugins/design/dev-server/canvas-lib.tsx +506 -30
- package/plugins/design/dev-server/canvas-shell.tsx +352 -20
- package/plugins/design/dev-server/client/hmr.mjs +28 -0
- package/plugins/design/dev-server/commands/annotation-strokes-command.ts +127 -0
- package/plugins/design/dev-server/commands/equal-spacing-command.ts +28 -0
- package/plugins/design/dev-server/commands/move-artboards-command.ts +158 -0
- package/plugins/design/dev-server/comments-overlay.tsx +12 -4
- package/plugins/design/dev-server/contextual-toolbar.tsx +241 -0
- package/plugins/design/dev-server/dist/client.bundle.js +3 -3
- package/plugins/design/dev-server/dist/runtime/motion.js +165 -0
- package/plugins/design/dev-server/dist/runtime/motion_react.js +409 -0
- package/plugins/design/dev-server/equal-spacing-detector.ts +98 -0
- package/plugins/design/dev-server/equal-spacing-handles.tsx +289 -0
- package/plugins/design/dev-server/handoff.ts +24 -0
- package/plugins/design/dev-server/http.ts +27 -0
- package/plugins/design/dev-server/input-router.tsx +52 -2
- package/plugins/design/dev-server/marquee-overlay.tsx +282 -0
- package/plugins/design/dev-server/runtime-bundle.ts +7 -0
- package/plugins/design/dev-server/test/annotation-strokes-command.test.ts +149 -0
- package/plugins/design/dev-server/test/annotations-layer.test.ts +44 -0
- package/plugins/design/dev-server/test/canvas-lib-motion.test.ts +131 -0
- package/plugins/design/dev-server/test/equal-spacing-detector.test.ts +93 -0
- package/plugins/design/dev-server/test/input-router.test.ts +87 -1
- package/plugins/design/dev-server/test/marquee-overlay.test.ts +94 -0
- package/plugins/design/dev-server/test/move-artboards-command.test.ts +108 -0
- package/plugins/design/dev-server/test/undo-stack.test.ts +211 -0
- package/plugins/design/dev-server/test/use-cursor-modifiers.test.ts +59 -0
- package/plugins/design/dev-server/test/use-keyboard-discipline.test.ts +27 -0
- package/plugins/design/dev-server/test/use-undo-stack.test.tsx +193 -0
- package/plugins/design/dev-server/undo-hud.tsx +95 -0
- package/plugins/design/dev-server/undo-stack.ts +240 -0
- package/plugins/design/dev-server/use-artboard-drag.tsx +6 -3
- package/plugins/design/dev-server/use-cursor-modifiers.tsx +122 -0
- package/plugins/design/dev-server/use-keyboard-discipline.tsx +125 -0
- package/plugins/design/dev-server/use-undo-stack.tsx +355 -0
- package/plugins/design/templates/_shell.html +17 -6
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +245 -0
- package/plugins/design/templates/design-system-inspiration/_MAPPING.md +2 -2
- package/plugins/design/templates/design-system-inspiration/core/preview/_components.css.tpl +129 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +63 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/motion.css.tpl +106 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/motion.tsx.tpl +208 -0
- /package/plugins/design/templates/design-system-inspiration/core/preview/{motion.html → .archive/motion.html} +0 -0
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
useDragStateContext,
|
|
47
47
|
useViewportControllerContext,
|
|
48
48
|
} from './canvas-lib.tsx';
|
|
49
|
+
import { type AlignMode, alignLabel, equalSpacingLabel } from './commands/equal-spacing-command.ts';
|
|
49
50
|
import { CommentsOverlay } from './comments-overlay.tsx';
|
|
50
51
|
import {
|
|
51
52
|
ContextMenuProvider,
|
|
@@ -55,16 +56,23 @@ import {
|
|
|
55
56
|
type MenuItem,
|
|
56
57
|
useContextMenu,
|
|
57
58
|
} from './context-menu.tsx';
|
|
59
|
+
import { ContextualToolbar } from './contextual-toolbar.tsx';
|
|
60
|
+
import { EqualSpacingHandles } from './equal-spacing-handles.tsx';
|
|
58
61
|
import { ExportDialogProvider } from './export-dialog.tsx';
|
|
59
62
|
import { type HoverTarget, resolveHoverTarget, useInputRouter } from './input-router.tsx';
|
|
63
|
+
import { ElementMarqueeOverlay } from './marquee-overlay.tsx';
|
|
60
64
|
import { ToolPalette } from './tool-palette.tsx';
|
|
65
|
+
import { UndoHud } from './undo-hud.tsx';
|
|
61
66
|
import {
|
|
62
67
|
AnnotationSelectionProvider,
|
|
63
68
|
useAnnotationSelection,
|
|
64
69
|
} from './use-annotation-selection.tsx';
|
|
65
70
|
import { AnnotationsVisibilityProvider } from './use-annotations-visibility.tsx';
|
|
71
|
+
import { useCursorModifiers } from './use-cursor-modifiers.tsx';
|
|
72
|
+
import { useKeyboardDiscipline } from './use-keyboard-discipline.tsx';
|
|
66
73
|
import { type Selection, SelectionSetProvider, useSelectionSet } from './use-selection-set.tsx';
|
|
67
74
|
import { useToolMode } from './use-tool-mode.tsx';
|
|
75
|
+
import { useUndoStack } from './use-undo-stack.tsx';
|
|
68
76
|
|
|
69
77
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
70
78
|
// Styles — halos render as `position: fixed` siblings of the canvas. Reading
|
|
@@ -215,6 +223,26 @@ const HALO_CSS = `
|
|
|
215
223
|
.dc-canvas[data-active-tool="eraser"] * {
|
|
216
224
|
cursor: var(--cursor-eraser), cell !important;
|
|
217
225
|
}
|
|
226
|
+
|
|
227
|
+
/* T31 — Level of detail. Below 0.35 zoom we hide pre-attentive chrome that
|
|
228
|
+
becomes visual noise (corner ticks, distance pills, active-artboard ring,
|
|
229
|
+
snap pills). Above 4.0 we coax the browser into crisper text rendering.
|
|
230
|
+
The "normal" band 0.35..4.0 carries the full chrome. */
|
|
231
|
+
.dc-canvas[data-cv-zoom-lod="low"] .dc-cv-halo .tick,
|
|
232
|
+
.dc-canvas[data-cv-zoom-lod="low"] .dc-cv-group-bbox .tick {
|
|
233
|
+
display: none;
|
|
234
|
+
}
|
|
235
|
+
.dc-canvas[data-cv-zoom-lod="low"] .dc-snap-pill,
|
|
236
|
+
.dc-canvas[data-cv-zoom-lod="low"] .dc-cv-eq-pill {
|
|
237
|
+
display: none;
|
|
238
|
+
}
|
|
239
|
+
.dc-canvas[data-cv-zoom-lod="low"] .dc-artboard[aria-current="true"] {
|
|
240
|
+
box-shadow: none;
|
|
241
|
+
}
|
|
242
|
+
.dc-canvas[data-cv-zoom-lod="crisp"] {
|
|
243
|
+
-webkit-font-smoothing: subpixel-antialiased;
|
|
244
|
+
font-smooth: always;
|
|
245
|
+
}
|
|
218
246
|
`.trim();
|
|
219
247
|
|
|
220
248
|
function ensureHaloStyles(): void {
|
|
@@ -279,9 +307,63 @@ function CanvasCore({
|
|
|
279
307
|
};
|
|
280
308
|
}, [hostRef, tool]);
|
|
281
309
|
|
|
310
|
+
// T28 — modifier-aware cursor (Alt → copy on cd-id, Shift → crosshair on
|
|
311
|
+
// body padding). CSS-driven once data-mod-* is reflected on the host.
|
|
312
|
+
useCursorModifiers(hostRef);
|
|
313
|
+
// T29 — arrow nudge (artboards) + Cmd+A select-all (active artboard).
|
|
314
|
+
// Cmd+D duplicate deferred; no live duplicate channel for either artboards
|
|
315
|
+
// or stamped elements yet.
|
|
316
|
+
useKeyboardDiscipline();
|
|
317
|
+
|
|
282
318
|
const artboardsCtx = useArtboardsContext();
|
|
283
319
|
const dragBus = useDragStateContext();
|
|
284
320
|
|
|
321
|
+
// T33 — programmatic-zoom easing via double-click on empty world only.
|
|
322
|
+
// Per post-Wave-3 user feedback, dblclick-on-artboard auto-zoom was
|
|
323
|
+
// surprising (interfered with native dblclick text-select inside chrome
|
|
324
|
+
// and felt magnetic). We keep the dblclick-empty → `fit()` path because
|
|
325
|
+
// it's a discoverable "back to overview" gesture; artboard zoom is still
|
|
326
|
+
// reachable via Cmd+1 and the zoom HUD.
|
|
327
|
+
useEffect(() => {
|
|
328
|
+
if (!controller) return;
|
|
329
|
+
const host = hostRef.current;
|
|
330
|
+
if (!host) return;
|
|
331
|
+
const onDbl = (e: MouseEvent) => {
|
|
332
|
+
const t = e.target as Element | null;
|
|
333
|
+
if (!t || !t.closest) return;
|
|
334
|
+
// Floating chrome / overlays / drawn user content / any artboard
|
|
335
|
+
// surface → leave alone. Only dblclick that lands on the canvas
|
|
336
|
+
// background outside every artboard triggers `fit()`.
|
|
337
|
+
if (
|
|
338
|
+
t.closest(
|
|
339
|
+
'.dc-mm, .dc-zoom-tb, .dc-tool-palette, .dc-context-menu, .dc-annot-svg, .dc-annot-ctx, .cm-composer, .cm-thread, .cm-mention-popup, .cm-pin'
|
|
340
|
+
)
|
|
341
|
+
) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (t.closest('[data-dc-screen]')) return; // any part of an artboard
|
|
345
|
+
e.preventDefault();
|
|
346
|
+
controller.fit();
|
|
347
|
+
};
|
|
348
|
+
host.addEventListener('dblclick', onDbl);
|
|
349
|
+
return () => host.removeEventListener('dblclick', onDbl);
|
|
350
|
+
}, [controller, hostRef]);
|
|
351
|
+
|
|
352
|
+
// T31 — level-of-detail attribute on `.dc-canvas`. CSS rules hide
|
|
353
|
+
// pre-attentive chrome (ticks, distance pills, accent ring) below 0.35
|
|
354
|
+
// zoom and sharpen text above 4.0. tldraw's textShadowLod = 0.35 is the
|
|
355
|
+
// canonical threshold. Reads the published viewport (settle-cadence) so
|
|
356
|
+
// the LOD doesn't flicker between bands mid-zoom — chrome should settle
|
|
357
|
+
// once per gesture, not on every frame.
|
|
358
|
+
const publishedZoom = artboardsCtx?.viewport?.zoom ?? 1;
|
|
359
|
+
useEffect(() => {
|
|
360
|
+
const host = hostRef.current;
|
|
361
|
+
if (!host) return;
|
|
362
|
+
const lod = publishedZoom < 0.35 ? 'low' : publishedZoom > 4 ? 'crisp' : 'normal';
|
|
363
|
+
host.setAttribute('data-cv-zoom-lod', lod);
|
|
364
|
+
return () => host.removeAttribute('data-cv-zoom-lod');
|
|
365
|
+
}, [hostRef, publishedZoom]);
|
|
366
|
+
|
|
285
367
|
/**
|
|
286
368
|
* T24 — distribute the currently-selected artboards evenly on the given
|
|
287
369
|
* axis. Requires ≥ 3 selected artboards. Sort by leading edge, hold the
|
|
@@ -323,11 +405,98 @@ function CanvasCore({
|
|
|
323
405
|
}
|
|
324
406
|
}
|
|
325
407
|
if (moved.length === 0) return;
|
|
326
|
-
dragBus.commitPositions(moved);
|
|
408
|
+
dragBus.commitPositions(moved, { label: equalSpacingLabel(sorted.length) });
|
|
409
|
+
},
|
|
410
|
+
[artboardsCtx, dragBus, selSet.selected]
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* G7 — align selected artboards to a common edge / midline. Requires ≥ 2
|
|
415
|
+
* selected artboards. Six modes:
|
|
416
|
+
* - 'left' → all artboards share the minimum x of the set
|
|
417
|
+
* - 'right' → all artboards share the maximum (x + w) edge
|
|
418
|
+
* - 'center-x' → all artboards share the midpoint of the set's x extent
|
|
419
|
+
* - 'top' → all artboards share the minimum y
|
|
420
|
+
* - 'bottom' → all artboards share the maximum (y + h) edge
|
|
421
|
+
* - 'center-y' → all artboards share the midpoint of the set's y extent
|
|
422
|
+
*
|
|
423
|
+
* Holds the reference edge constant; only the perpendicular axis stays as
|
|
424
|
+
* each artboard already was (alignment doesn't relocate on the orthogonal
|
|
425
|
+
* axis). This matches Figma / Sketch / FigJam align semantics.
|
|
426
|
+
*/
|
|
427
|
+
const alignArtboards = useCallback(
|
|
428
|
+
(mode: AlignMode) => {
|
|
429
|
+
if (!artboardsCtx || !dragBus) return;
|
|
430
|
+
const ids = new Set(
|
|
431
|
+
selSet.selected.filter((s) => !!s.artboardId).map((s) => s.artboardId as string)
|
|
432
|
+
);
|
|
433
|
+
if (ids.size < 2) return;
|
|
434
|
+
const targets: ArtboardRect[] = artboardsCtx.artboards.filter((r) => ids.has(r.id));
|
|
435
|
+
if (targets.length < 2) return;
|
|
436
|
+
|
|
437
|
+
// Union bbox for center modes.
|
|
438
|
+
let xMin = Number.POSITIVE_INFINITY;
|
|
439
|
+
let yMin = Number.POSITIVE_INFINITY;
|
|
440
|
+
let xMax = Number.NEGATIVE_INFINITY;
|
|
441
|
+
let yMax = Number.NEGATIVE_INFINITY;
|
|
442
|
+
for (const r of targets) {
|
|
443
|
+
if (r.x < xMin) xMin = r.x;
|
|
444
|
+
if (r.y < yMin) yMin = r.y;
|
|
445
|
+
if (r.x + r.w > xMax) xMax = r.x + r.w;
|
|
446
|
+
if (r.y + r.h > yMax) yMax = r.y + r.h;
|
|
447
|
+
}
|
|
448
|
+
const cx = (xMin + xMax) / 2;
|
|
449
|
+
const cy = (yMin + yMax) / 2;
|
|
450
|
+
|
|
451
|
+
const moved: { id: string; x: number; y: number }[] = [];
|
|
452
|
+
for (const r of targets) {
|
|
453
|
+
let nx = r.x;
|
|
454
|
+
let ny = r.y;
|
|
455
|
+
switch (mode) {
|
|
456
|
+
case 'left':
|
|
457
|
+
nx = xMin;
|
|
458
|
+
break;
|
|
459
|
+
case 'right':
|
|
460
|
+
nx = xMax - r.w;
|
|
461
|
+
break;
|
|
462
|
+
case 'center-x':
|
|
463
|
+
nx = cx - r.w / 2;
|
|
464
|
+
break;
|
|
465
|
+
case 'top':
|
|
466
|
+
ny = yMin;
|
|
467
|
+
break;
|
|
468
|
+
case 'bottom':
|
|
469
|
+
ny = yMax - r.h;
|
|
470
|
+
break;
|
|
471
|
+
case 'center-y':
|
|
472
|
+
ny = cy - r.h / 2;
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
if (Math.round(nx) === r.x && Math.round(ny) === r.y) continue;
|
|
476
|
+
moved.push({ id: r.id, x: Math.round(nx), y: Math.round(ny) });
|
|
477
|
+
}
|
|
478
|
+
if (moved.length === 0) return;
|
|
479
|
+
dragBus.commitPositions(moved, { label: alignLabel(mode, targets.length) });
|
|
327
480
|
},
|
|
328
481
|
[artboardsCtx, dragBus, selSet.selected]
|
|
329
482
|
);
|
|
330
483
|
|
|
484
|
+
/**
|
|
485
|
+
* G2v2 — "Fit just this artboard" context-menu entry previously bridged
|
|
486
|
+
* via a synthetic click on the label button (which used to call
|
|
487
|
+
* controller.jumpTo). Now that the label is purely a11y, the menu entry
|
|
488
|
+
* needs a direct path: look the rect up from the live artboards list and
|
|
489
|
+
* call jumpTo straight on the controller.
|
|
490
|
+
*/
|
|
491
|
+
const focusArtboard = useCallback(
|
|
492
|
+
(artboardId: string) => {
|
|
493
|
+
if (!controller || !artboardsCtx) return;
|
|
494
|
+
const rect = artboardsCtx.artboards.find((r) => r.id === artboardId);
|
|
495
|
+
if (rect) controller.jumpTo(rect);
|
|
496
|
+
},
|
|
497
|
+
[controller, artboardsCtx]
|
|
498
|
+
);
|
|
499
|
+
|
|
331
500
|
const registry = useMemo<ContextRegistry>(
|
|
332
501
|
() =>
|
|
333
502
|
buildRegistry({
|
|
@@ -335,8 +504,10 @@ function CanvasCore({
|
|
|
335
504
|
clearSelection: selSet.clear,
|
|
336
505
|
selSet,
|
|
337
506
|
distributeArtboards,
|
|
507
|
+
alignArtboards,
|
|
508
|
+
focusArtboard,
|
|
338
509
|
}),
|
|
339
|
-
[controller, selSet, distributeArtboards]
|
|
510
|
+
[controller, selSet, distributeArtboards, alignArtboards, focusArtboard]
|
|
340
511
|
);
|
|
341
512
|
|
|
342
513
|
// Distribute is reached via the MultiArtboardToolbar (floating chrome
|
|
@@ -346,7 +517,11 @@ function CanvasCore({
|
|
|
346
517
|
return (
|
|
347
518
|
<ExportDialogProvider>
|
|
348
519
|
<ContextMenuProvider registry={registry}>
|
|
349
|
-
<CanvasRouter
|
|
520
|
+
<CanvasRouter
|
|
521
|
+
hostRef={hostRef}
|
|
522
|
+
distributeArtboards={distributeArtboards}
|
|
523
|
+
alignArtboards={alignArtboards}
|
|
524
|
+
>
|
|
350
525
|
{children}
|
|
351
526
|
</CanvasRouter>
|
|
352
527
|
</ContextMenuProvider>
|
|
@@ -362,14 +537,19 @@ function buildRegistry(deps: {
|
|
|
362
537
|
clearSelection: () => void;
|
|
363
538
|
selSet: { selected: Selection[] };
|
|
364
539
|
distributeArtboards: (axis: 'x' | 'y') => void;
|
|
540
|
+
alignArtboards: (mode: 'left' | 'right' | 'center-x' | 'top' | 'bottom' | 'center-y') => void;
|
|
541
|
+
focusArtboard: (artboardId: string) => void;
|
|
365
542
|
}): ContextRegistry {
|
|
366
|
-
const { controller, clearSelection, selSet, distributeArtboards } =
|
|
543
|
+
const { controller, clearSelection, selSet, distributeArtboards, alignArtboards, focusArtboard } =
|
|
544
|
+
deps;
|
|
367
545
|
|
|
368
546
|
// T24 — distribute commands are only enabled when ≥ 3 artboards are
|
|
369
547
|
// selected. Below that, the menu items render as `disabled` so the user
|
|
370
548
|
// sees the affordance but understands the precondition.
|
|
549
|
+
// G7 — align commands are enabled at ≥ 2 (alignment is well-defined with 2).
|
|
371
550
|
const selectedArtboardCount = selSet.selected.filter((s) => !!s.artboardId).length;
|
|
372
551
|
const distributeEnabled = selectedArtboardCount >= 3;
|
|
552
|
+
const alignEnabled = selectedArtboardCount >= 2;
|
|
373
553
|
|
|
374
554
|
const copy = (text: string): void => {
|
|
375
555
|
if (typeof navigator === 'undefined' || !navigator.clipboard) return;
|
|
@@ -492,19 +672,54 @@ function buildRegistry(deps: {
|
|
|
492
672
|
id: 'fit-one',
|
|
493
673
|
label: 'Fit just this artboard',
|
|
494
674
|
onSelect: (target) => {
|
|
495
|
-
if (!
|
|
496
|
-
|
|
497
|
-
// same pattern. The artboard label button already wires to
|
|
498
|
-
// onFocus; dispatch a synthetic click as the simplest bridge.
|
|
499
|
-
const btn = (target.el ?? document)
|
|
500
|
-
.closest?.('[data-dc-screen]')
|
|
501
|
-
?.querySelector('button.dc-artboard-label');
|
|
502
|
-
(btn as HTMLButtonElement | null)?.click();
|
|
675
|
+
if (!target.artboardId) return;
|
|
676
|
+
focusArtboard(target.artboardId);
|
|
503
677
|
},
|
|
504
678
|
},
|
|
505
679
|
fitItem,
|
|
506
680
|
resetItem,
|
|
507
681
|
],
|
|
682
|
+
[
|
|
683
|
+
// G7 — align commands. Six modes; gated on ≥ 2 selected artboards
|
|
684
|
+
// (alignment is well-defined with 2). Primary surface is the
|
|
685
|
+
// MultiArtboardToolbar; menu entries are discoverability backup.
|
|
686
|
+
{
|
|
687
|
+
id: 'align-left',
|
|
688
|
+
label: 'Align left',
|
|
689
|
+
disabled: !alignEnabled,
|
|
690
|
+
onSelect: () => alignArtboards('left'),
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
id: 'align-center-x',
|
|
694
|
+
label: 'Align center (horizontal)',
|
|
695
|
+
disabled: !alignEnabled,
|
|
696
|
+
onSelect: () => alignArtboards('center-x'),
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
id: 'align-right',
|
|
700
|
+
label: 'Align right',
|
|
701
|
+
disabled: !alignEnabled,
|
|
702
|
+
onSelect: () => alignArtboards('right'),
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
id: 'align-top',
|
|
706
|
+
label: 'Align top',
|
|
707
|
+
disabled: !alignEnabled,
|
|
708
|
+
onSelect: () => alignArtboards('top'),
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
id: 'align-center-y',
|
|
712
|
+
label: 'Align center (vertical)',
|
|
713
|
+
disabled: !alignEnabled,
|
|
714
|
+
onSelect: () => alignArtboards('center-y'),
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
id: 'align-bottom',
|
|
718
|
+
label: 'Align bottom',
|
|
719
|
+
disabled: !alignEnabled,
|
|
720
|
+
onSelect: () => alignArtboards('bottom'),
|
|
721
|
+
},
|
|
722
|
+
],
|
|
508
723
|
[
|
|
509
724
|
// T24 — distribute commands. Primary surface is the floating
|
|
510
725
|
// MultiArtboardToolbar above the group bbox; the menu entries are
|
|
@@ -553,15 +768,18 @@ function CanvasRouter({
|
|
|
553
768
|
hostRef,
|
|
554
769
|
children,
|
|
555
770
|
distributeArtboards,
|
|
771
|
+
alignArtboards,
|
|
556
772
|
}: {
|
|
557
773
|
hostRef: RefObject<HTMLDivElement | null>;
|
|
558
774
|
children: ReactNode;
|
|
559
775
|
distributeArtboards: (axis: 'x' | 'y') => void;
|
|
776
|
+
alignArtboards: (mode: 'left' | 'right' | 'center-x' | 'top' | 'bottom' | 'center-y') => void;
|
|
560
777
|
}) {
|
|
561
778
|
const { tool, setTool, clearSticky } = useToolMode();
|
|
562
779
|
const selSet = useSelectionSet();
|
|
563
780
|
const annotSel = useAnnotationSelection();
|
|
564
781
|
const ctxMenu = useContextMenu();
|
|
782
|
+
const undoStack = useUndoStack();
|
|
565
783
|
|
|
566
784
|
// Hover state drives the floating .dc-cv-halo--hover overlay. The overlay
|
|
567
785
|
// itself reads getBoundingClientRect on every rAF tick to follow pan/zoom.
|
|
@@ -663,6 +881,12 @@ function CanvasRouter({
|
|
|
663
881
|
ctxMenu.open(ctxTarget);
|
|
664
882
|
},
|
|
665
883
|
onTool: ({ tool: t }) => setTool(t),
|
|
884
|
+
onUndo: () => {
|
|
885
|
+
void undoStack.undo();
|
|
886
|
+
},
|
|
887
|
+
onRedo: () => {
|
|
888
|
+
void undoStack.redo();
|
|
889
|
+
},
|
|
666
890
|
onEscape: () => {
|
|
667
891
|
// T21 — abort any mid-stroke draw FIRST. The annotations layer
|
|
668
892
|
// listens for `maude:cancel-stroke` and drops the in-progress
|
|
@@ -781,11 +1005,18 @@ function CanvasRouter({
|
|
|
781
1005
|
<AnnotationsLayer />
|
|
782
1006
|
<ToolPalette />
|
|
783
1007
|
<ArtboardMarqueeOverlay />
|
|
1008
|
+
<ElementMarqueeOverlay />
|
|
784
1009
|
<HoverHalo el={hoverEl} />
|
|
785
1010
|
<SelectionHalos />
|
|
786
1011
|
<GroupBbox />
|
|
787
|
-
<
|
|
1012
|
+
<EqualSpacingHandles />
|
|
1013
|
+
<ContextualToolbar />
|
|
1014
|
+
<MultiArtboardToolbar
|
|
1015
|
+
distributeArtboards={distributeArtboards}
|
|
1016
|
+
alignArtboards={alignArtboards}
|
|
1017
|
+
/>
|
|
788
1018
|
<SnapGuideOverlay />
|
|
1019
|
+
<UndoHud />
|
|
789
1020
|
</>
|
|
790
1021
|
);
|
|
791
1022
|
}
|
|
@@ -849,6 +1080,12 @@ const MULTI_TOOLBAR_CSS = `
|
|
|
849
1080
|
align-items: center;
|
|
850
1081
|
justify-content: center;
|
|
851
1082
|
}
|
|
1083
|
+
.dc-multi-artboard-tb .dc-mab-divider {
|
|
1084
|
+
width: 1px;
|
|
1085
|
+
align-self: stretch;
|
|
1086
|
+
background: var(--u-border-subtle, rgba(0,0,0,0.10));
|
|
1087
|
+
margin: 0 4px;
|
|
1088
|
+
}
|
|
852
1089
|
@media (prefers-reduced-motion: reduce) {
|
|
853
1090
|
.dc-multi-artboard-tb button { transition: none; }
|
|
854
1091
|
}
|
|
@@ -865,8 +1102,10 @@ function ensureMultiToolbarStyles(): void {
|
|
|
865
1102
|
|
|
866
1103
|
function MultiArtboardToolbar({
|
|
867
1104
|
distributeArtboards,
|
|
1105
|
+
alignArtboards,
|
|
868
1106
|
}: {
|
|
869
1107
|
distributeArtboards: (axis: 'x' | 'y') => void;
|
|
1108
|
+
alignArtboards: (mode: 'left' | 'right' | 'center-x' | 'top' | 'bottom' | 'center-y') => void;
|
|
870
1109
|
}) {
|
|
871
1110
|
ensureMultiToolbarStyles();
|
|
872
1111
|
const { selected } = useSelectionSet();
|
|
@@ -875,7 +1114,8 @@ function MultiArtboardToolbar({
|
|
|
875
1114
|
|
|
876
1115
|
const artboardSelections = useMemo(() => selected.filter((s) => !!s.artboardId), [selected]);
|
|
877
1116
|
const artboardCount = artboardSelections.length;
|
|
878
|
-
const
|
|
1117
|
+
const distributeOk = artboardCount >= 3;
|
|
1118
|
+
const alignOk = artboardCount >= 2;
|
|
879
1119
|
|
|
880
1120
|
useEffect(() => {
|
|
881
1121
|
const div = ref.current;
|
|
@@ -943,11 +1183,105 @@ function MultiArtboardToolbar({
|
|
|
943
1183
|
aria-label="Multi-artboard actions"
|
|
944
1184
|
>
|
|
945
1185
|
<span className="dc-mab-count">{artboardCount} artboards</span>
|
|
1186
|
+
{/* G7 — align cluster. Icon-only to keep the toolbar narrow; full
|
|
1187
|
+
label lives in the tooltip + the right-click menu. Enabled at ≥ 2
|
|
1188
|
+
artboards (alignment is well-defined with 2). */}
|
|
1189
|
+
<button
|
|
1190
|
+
type="button"
|
|
1191
|
+
disabled={!alignOk}
|
|
1192
|
+
title={alignOk ? 'Align left' : 'Select at least 2 artboards to align'}
|
|
1193
|
+
aria-label="Align left"
|
|
1194
|
+
onClick={() => alignArtboards('left')}
|
|
1195
|
+
>
|
|
1196
|
+
<span className="dc-mab-icon" aria-hidden="true">
|
|
1197
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
1198
|
+
<line x1="0.5" y1="0.5" x2="0.5" y2="13.5" stroke="currentColor" />
|
|
1199
|
+
<rect x="1" y="2" width="7" height="3" fill="currentColor" />
|
|
1200
|
+
<rect x="1" y="9" width="11" height="3" fill="currentColor" />
|
|
1201
|
+
</svg>
|
|
1202
|
+
</span>
|
|
1203
|
+
</button>
|
|
1204
|
+
<button
|
|
1205
|
+
type="button"
|
|
1206
|
+
disabled={!alignOk}
|
|
1207
|
+
title={alignOk ? 'Align center (horizontal)' : 'Select at least 2 artboards to align'}
|
|
1208
|
+
aria-label="Align center horizontally"
|
|
1209
|
+
onClick={() => alignArtboards('center-x')}
|
|
1210
|
+
>
|
|
1211
|
+
<span className="dc-mab-icon" aria-hidden="true">
|
|
1212
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
1213
|
+
<line x1="7" y1="0.5" x2="7" y2="13.5" stroke="currentColor" />
|
|
1214
|
+
<rect x="3.5" y="2" width="7" height="3" fill="currentColor" />
|
|
1215
|
+
<rect x="1.5" y="9" width="11" height="3" fill="currentColor" />
|
|
1216
|
+
</svg>
|
|
1217
|
+
</span>
|
|
1218
|
+
</button>
|
|
1219
|
+
<button
|
|
1220
|
+
type="button"
|
|
1221
|
+
disabled={!alignOk}
|
|
1222
|
+
title={alignOk ? 'Align right' : 'Select at least 2 artboards to align'}
|
|
1223
|
+
aria-label="Align right"
|
|
1224
|
+
onClick={() => alignArtboards('right')}
|
|
1225
|
+
>
|
|
1226
|
+
<span className="dc-mab-icon" aria-hidden="true">
|
|
1227
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
1228
|
+
<line x1="13.5" y1="0.5" x2="13.5" y2="13.5" stroke="currentColor" />
|
|
1229
|
+
<rect x="6" y="2" width="7" height="3" fill="currentColor" />
|
|
1230
|
+
<rect x="2" y="9" width="11" height="3" fill="currentColor" />
|
|
1231
|
+
</svg>
|
|
1232
|
+
</span>
|
|
1233
|
+
</button>
|
|
1234
|
+
<button
|
|
1235
|
+
type="button"
|
|
1236
|
+
disabled={!alignOk}
|
|
1237
|
+
title={alignOk ? 'Align top' : 'Select at least 2 artboards to align'}
|
|
1238
|
+
aria-label="Align top"
|
|
1239
|
+
onClick={() => alignArtboards('top')}
|
|
1240
|
+
>
|
|
1241
|
+
<span className="dc-mab-icon" aria-hidden="true">
|
|
1242
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
1243
|
+
<line x1="0.5" y1="0.5" x2="13.5" y2="0.5" stroke="currentColor" />
|
|
1244
|
+
<rect x="2" y="1" width="3" height="7" fill="currentColor" />
|
|
1245
|
+
<rect x="9" y="1" width="3" height="11" fill="currentColor" />
|
|
1246
|
+
</svg>
|
|
1247
|
+
</span>
|
|
1248
|
+
</button>
|
|
1249
|
+
<button
|
|
1250
|
+
type="button"
|
|
1251
|
+
disabled={!alignOk}
|
|
1252
|
+
title={alignOk ? 'Align center (vertical)' : 'Select at least 2 artboards to align'}
|
|
1253
|
+
aria-label="Align center vertically"
|
|
1254
|
+
onClick={() => alignArtboards('center-y')}
|
|
1255
|
+
>
|
|
1256
|
+
<span className="dc-mab-icon" aria-hidden="true">
|
|
1257
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
1258
|
+
<line x1="0.5" y1="7" x2="13.5" y2="7" stroke="currentColor" />
|
|
1259
|
+
<rect x="2" y="3.5" width="3" height="7" fill="currentColor" />
|
|
1260
|
+
<rect x="9" y="1.5" width="3" height="11" fill="currentColor" />
|
|
1261
|
+
</svg>
|
|
1262
|
+
</span>
|
|
1263
|
+
</button>
|
|
1264
|
+
<button
|
|
1265
|
+
type="button"
|
|
1266
|
+
disabled={!alignOk}
|
|
1267
|
+
title={alignOk ? 'Align bottom' : 'Select at least 2 artboards to align'}
|
|
1268
|
+
aria-label="Align bottom"
|
|
1269
|
+
onClick={() => alignArtboards('bottom')}
|
|
1270
|
+
>
|
|
1271
|
+
<span className="dc-mab-icon" aria-hidden="true">
|
|
1272
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
1273
|
+
<line x1="0.5" y1="13.5" x2="13.5" y2="13.5" stroke="currentColor" />
|
|
1274
|
+
<rect x="2" y="6" width="3" height="7" fill="currentColor" />
|
|
1275
|
+
<rect x="9" y="2" width="3" height="11" fill="currentColor" />
|
|
1276
|
+
</svg>
|
|
1277
|
+
</span>
|
|
1278
|
+
</button>
|
|
1279
|
+
<span className="dc-mab-divider" aria-hidden="true" />
|
|
946
1280
|
<button
|
|
947
1281
|
type="button"
|
|
948
|
-
disabled={!
|
|
1282
|
+
disabled={!distributeOk}
|
|
949
1283
|
title={
|
|
950
|
-
|
|
1284
|
+
distributeOk
|
|
951
1285
|
? 'Distribute horizontally — equal gaps between artboards'
|
|
952
1286
|
: 'Select at least 3 artboards to distribute'
|
|
953
1287
|
}
|
|
@@ -961,13 +1295,12 @@ function MultiArtboardToolbar({
|
|
|
961
1295
|
<rect x="10.5" y="3" width="3" height="8" fill="currentColor" />
|
|
962
1296
|
</svg>
|
|
963
1297
|
</span>
|
|
964
|
-
Distribute H
|
|
965
1298
|
</button>
|
|
966
1299
|
<button
|
|
967
1300
|
type="button"
|
|
968
|
-
disabled={!
|
|
1301
|
+
disabled={!distributeOk}
|
|
969
1302
|
title={
|
|
970
|
-
|
|
1303
|
+
distributeOk
|
|
971
1304
|
? 'Distribute vertically — equal gaps between artboards'
|
|
972
1305
|
: 'Select at least 3 artboards to distribute'
|
|
973
1306
|
}
|
|
@@ -981,7 +1314,6 @@ function MultiArtboardToolbar({
|
|
|
981
1314
|
<rect x="3" y="10.5" width="8" height="3" fill="currentColor" />
|
|
982
1315
|
</svg>
|
|
983
1316
|
</span>
|
|
984
|
-
Distribute V
|
|
985
1317
|
</button>
|
|
986
1318
|
</div>
|
|
987
1319
|
);
|
|
@@ -65,6 +65,34 @@ function handle(evt) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (evt.type === 'fs:json' && evt.file) {
|
|
71
|
+
// Phase 20 (DDR-049) — external edits to canvas `.meta.json` or
|
|
72
|
+
// `.annotations.svg` invalidate the iframe's in-memory undo stack
|
|
73
|
+
// (stale before/after snapshots would otherwise restore wrong state).
|
|
74
|
+
// The iframe's UndoStackProvider self-echo-dampens our own PATCH-es
|
|
75
|
+
// via window.__maude_last_meta_self_write_at, so we forward
|
|
76
|
+
// indiscriminately here.
|
|
77
|
+
const file = evt.file;
|
|
78
|
+
const isMeta = file.endsWith('.meta.json');
|
|
79
|
+
if (!isMeta) return;
|
|
80
|
+
const iframes = document.querySelectorAll('iframe[data-canvas-path]');
|
|
81
|
+
for (const f of iframes) {
|
|
82
|
+
if (!f.dataset.canvasPath) continue;
|
|
83
|
+
// Match by the canvas filename root (foo.tsx ↔ foo.meta.json).
|
|
84
|
+
const root = f.dataset.canvasPath.replace(/\.(tsx|jsx|ts|js|html)$/i, '');
|
|
85
|
+
if (!file.includes(`${root}.meta.json`)) continue;
|
|
86
|
+
try {
|
|
87
|
+
f.contentWindow?.dispatchEvent(
|
|
88
|
+
new CustomEvent('maude:invalidate-undo', {
|
|
89
|
+
detail: { reason: 'Edit history reset (external change)' },
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
} catch {
|
|
93
|
+
/* cross-origin / detached — ignore */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
68
96
|
}
|
|
69
97
|
}
|
|
70
98
|
|