@1agh/maude 0.19.1 → 0.21.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/README.md +7 -0
- package/cli/bin/maude.mjs +5 -1
- package/cli/commands/design-link.test.mjs +207 -0
- package/cli/commands/design.mjs +42 -12
- package/cli/commands/doctor.mjs +350 -0
- package/cli/commands/doctor.test.mjs +185 -0
- package/cli/commands/help.mjs +24 -0
- package/cli/commands/hub.mjs +231 -0
- package/cli/commands/hub.test.mjs +87 -0
- package/cli/lib/config-lint.mjs +141 -0
- package/cli/lib/config-lint.test.mjs +117 -0
- package/cli/lib/design-link.mjs +216 -0
- package/cli/lib/hubs-config.mjs +123 -0
- package/cli/lib/hubs-config.test.mjs +100 -0
- package/cli/lib/preflight.mjs +232 -0
- package/cli/lib/stack-detect.mjs +344 -0
- package/cli/lib/stack-detect.test.mjs +121 -0
- package/package.json +17 -9
- package/plugins/design/dependencies.json +147 -0
- package/plugins/design/dependencies.schema.json +107 -0
- package/plugins/design/dev-server/ai-banner.tsx +188 -0
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +115 -41
- package/plugins/design/dev-server/annotations-layer.tsx +256 -107
- package/plugins/design/dev-server/api.ts +17 -1
- package/plugins/design/dev-server/artboard-marquee.tsx +10 -6
- package/plugins/design/dev-server/bin/asset-sweep.sh +180 -0
- package/plugins/design/dev-server/bin/preflight.sh +32 -0
- package/plugins/design/dev-server/bin/runtime-health.sh +169 -0
- package/plugins/design/dev-server/bin/visual-sanity.sh +221 -0
- package/plugins/design/dev-server/canvas-lib.tsx +534 -32
- package/plugins/design/dev-server/canvas-shell.tsx +479 -29
- package/plugins/design/dev-server/client/app.jsx +72 -0
- package/plugins/design/dev-server/client/hmr.mjs +28 -0
- package/plugins/design/dev-server/collab/ai-activity.ts +127 -0
- package/plugins/design/dev-server/collab/git-lifecycle.ts +124 -0
- package/plugins/design/dev-server/collab/index.ts +47 -0
- package/plugins/design/dev-server/collab/persistence.ts +123 -0
- package/plugins/design/dev-server/collab/protocol.ts +108 -0
- package/plugins/design/dev-server/collab/registry.ts +110 -0
- package/plugins/design/dev-server/collab/room.ts +215 -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 +41 -4
- package/plugins/design/dev-server/contextual-toolbar.tsx +241 -0
- package/plugins/design/dev-server/cursors-overlay.tsx +296 -0
- package/plugins/design/dev-server/dist/client.bundle.js +75 -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/dist/runtime/y-protocols_awareness.js +587 -0
- package/plugins/design/dev-server/dist/runtime/y-protocols_sync.js +257 -0
- package/plugins/design/dev-server/dist/runtime/yjs.js +7107 -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/export-dialog.tsx +1 -1
- package/plugins/design/dev-server/handoff.ts +24 -0
- package/plugins/design/dev-server/hmr-broadcast.ts +15 -2
- package/plugins/design/dev-server/http.ts +91 -1
- 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/participants-chrome.tsx +261 -0
- package/plugins/design/dev-server/runtime-bundle.ts +15 -0
- package/plugins/design/dev-server/server.ts +78 -11
- package/plugins/design/dev-server/test/ai-activity.test.ts +113 -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/collab-annotations-bridge.test.ts +55 -0
- package/plugins/design/dev-server/test/collab-bridge.test.ts +81 -0
- package/plugins/design/dev-server/test/collab-loopback.test.ts +63 -0
- package/plugins/design/dev-server/test/collab-protocol.test.ts +146 -0
- package/plugins/design/dev-server/test/collab-room.test.ts +182 -0
- package/plugins/design/dev-server/test/collab-stress.test.ts +121 -0
- package/plugins/design/dev-server/test/equal-spacing-detector.test.ts +93 -0
- package/plugins/design/dev-server/test/git-lifecycle.test.ts +101 -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/participants-chrome.test.ts +30 -0
- package/plugins/design/dev-server/test/undo-stack.test.ts +211 -0
- package/plugins/design/dev-server/test/use-collab.test.ts +71 -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/tool-palette.tsx +7 -7
- 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-annotation-resize.tsx +1 -1
- package/plugins/design/dev-server/use-artboard-drag.tsx +6 -3
- package/plugins/design/dev-server/use-collab.tsx +478 -0
- 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/dev-server/ws.ts +123 -7
- package/plugins/design/templates/_shell.html +51 -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/flow/.claude-plugin/config.schema.json +12 -0
- package/plugins/flow/dependencies.json +143 -0
- package/plugins/flow/dependencies.schema.json +107 -0
- /package/plugins/design/templates/design-system-inspiration/core/preview/{motion.html → .archive/motion.html} +0 -0
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
useState,
|
|
37
37
|
} from 'react';
|
|
38
38
|
|
|
39
|
+
import { AiBanner } from './ai-banner.tsx';
|
|
39
40
|
import { AnnotationsLayer } from './annotations-layer.tsx';
|
|
40
41
|
import { ArtboardMarqueeOverlay } from './artboard-marquee.tsx';
|
|
41
42
|
import {
|
|
@@ -46,6 +47,7 @@ import {
|
|
|
46
47
|
useDragStateContext,
|
|
47
48
|
useViewportControllerContext,
|
|
48
49
|
} from './canvas-lib.tsx';
|
|
50
|
+
import { type AlignMode, alignLabel, equalSpacingLabel } from './commands/equal-spacing-command.ts';
|
|
49
51
|
import { CommentsOverlay } from './comments-overlay.tsx';
|
|
50
52
|
import {
|
|
51
53
|
ContextMenuProvider,
|
|
@@ -55,16 +57,27 @@ import {
|
|
|
55
57
|
type MenuItem,
|
|
56
58
|
useContextMenu,
|
|
57
59
|
} from './context-menu.tsx';
|
|
60
|
+
import { ContextualToolbar } from './contextual-toolbar.tsx';
|
|
61
|
+
import { CursorsOverlay } from './cursors-overlay.tsx';
|
|
62
|
+
import { EqualSpacingHandles } from './equal-spacing-handles.tsx';
|
|
58
63
|
import { ExportDialogProvider } from './export-dialog.tsx';
|
|
59
64
|
import { type HoverTarget, resolveHoverTarget, useInputRouter } from './input-router.tsx';
|
|
65
|
+
import { ElementMarqueeOverlay } from './marquee-overlay.tsx';
|
|
66
|
+
import { ParticipantsChrome } from './participants-chrome.tsx';
|
|
60
67
|
import { ToolPalette } from './tool-palette.tsx';
|
|
68
|
+
import { UndoHud } from './undo-hud.tsx';
|
|
61
69
|
import {
|
|
62
70
|
AnnotationSelectionProvider,
|
|
63
71
|
useAnnotationSelection,
|
|
72
|
+
useAnnotationSelectionOptional,
|
|
64
73
|
} from './use-annotation-selection.tsx';
|
|
65
74
|
import { AnnotationsVisibilityProvider } from './use-annotations-visibility.tsx';
|
|
75
|
+
import { useCollab } from './use-collab.tsx';
|
|
76
|
+
import { useCursorModifiers } from './use-cursor-modifiers.tsx';
|
|
77
|
+
import { useKeyboardDiscipline } from './use-keyboard-discipline.tsx';
|
|
66
78
|
import { type Selection, SelectionSetProvider, useSelectionSet } from './use-selection-set.tsx';
|
|
67
79
|
import { useToolMode } from './use-tool-mode.tsx';
|
|
80
|
+
import { useUndoStack } from './use-undo-stack.tsx';
|
|
68
81
|
|
|
69
82
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
70
83
|
// Styles — halos render as `position: fixed` siblings of the canvas. Reading
|
|
@@ -73,6 +86,28 @@ import { useToolMode } from './use-tool-mode.tsx';
|
|
|
73
86
|
// plane would otherwise scale a 2 px outline to 0.84 px at 42 % zoom (subpixel
|
|
74
87
|
// = invisible). No per-element class stamping is used.
|
|
75
88
|
|
|
89
|
+
// HUD-namespace token block. System-review 2026-05-27 (D-4) flagged that the
|
|
90
|
+
// dev-server chrome (toolbar + minimap + halos + marquee + AI banner) used
|
|
91
|
+
// `var(--accent, …)` which inherited the canvas DS palette — a violet StudyFi
|
|
92
|
+
// canvas turned the floating cursor toolbar violet. The HUD owns its own
|
|
93
|
+
// `--maude-hud-*` token family, set on `:root` of the canvas iframe document
|
|
94
|
+
// here. Canvas DSs do NOT define `--maude-hud-*`, so HUD CSS resolves against
|
|
95
|
+
// this block regardless of what the imported `:root { --accent: … }` looks like.
|
|
96
|
+
//
|
|
97
|
+
// Defaults match the existing inline fallback color (`#d63b1f`, Maude brand
|
|
98
|
+
// orange-rust) so no visual change to the default theme. Users who want to
|
|
99
|
+
// re-theme the HUD can set `--maude-hud-accent` etc. via a `<style>` block
|
|
100
|
+
// AFTER this one (CSS cascade — later wins).
|
|
101
|
+
const HUD_TOKENS_CSS = `
|
|
102
|
+
:root {
|
|
103
|
+
--maude-hud-accent: #d63b1f;
|
|
104
|
+
--maude-hud-accent-hover: #b8331b;
|
|
105
|
+
--maude-hud-accent-active: #962a16;
|
|
106
|
+
--maude-hud-accent-fg: #ffffff;
|
|
107
|
+
--maude-hud-accent-tint: color-mix(in oklab, #d63b1f 14%, transparent);
|
|
108
|
+
}
|
|
109
|
+
`;
|
|
110
|
+
|
|
76
111
|
// DDR-046 — Three-state halo language. Each state has its own border weight,
|
|
77
112
|
// color treatment, and geometric idiom so 8+ semantic states (hover / selected
|
|
78
113
|
// / member-of-multi / group / snap-sibling / snap-grid / marquee / annotation
|
|
@@ -90,20 +125,20 @@ const HALO_CSS = `
|
|
|
90
125
|
/* Hover — lighter 1.5px tinted line + white inner ring for contrast on dark
|
|
91
126
|
elements. NO ring, NO ticks. Synchronous paint (no debounce). */
|
|
92
127
|
.dc-cv-halo--hover {
|
|
93
|
-
border: 1.5px solid color-mix(in oklab, var(--accent, #0d99ff) 60%, transparent);
|
|
128
|
+
border: 1.5px solid color-mix(in oklab, var(--maude-hud-accent, #0d99ff) 60%, transparent);
|
|
94
129
|
box-shadow: inset 0 0 0 1px var(--bg-0, #ffffff);
|
|
95
130
|
}
|
|
96
131
|
/* Selected (single) — 2px solid + 18% ring halo + 4 filled corner ticks.
|
|
97
132
|
Ticks are <i class="tick tick-*"> children at inset:-3px, 8x8, accent fill. */
|
|
98
133
|
.dc-cv-halo--selected {
|
|
99
|
-
border: 2px solid var(--accent, #0d99ff);
|
|
100
|
-
box-shadow: 0 0 0 4px color-mix(in oklab, var(--accent, #0d99ff) 18%, transparent);
|
|
134
|
+
border: 2px solid var(--maude-hud-accent, #0d99ff);
|
|
135
|
+
box-shadow: 0 0 0 4px color-mix(in oklab, var(--maude-hud-accent, #0d99ff) 18%, transparent);
|
|
101
136
|
}
|
|
102
137
|
.dc-cv-halo--selected .tick {
|
|
103
138
|
position: absolute;
|
|
104
139
|
width: 8px;
|
|
105
140
|
height: 8px;
|
|
106
|
-
background: var(--accent, #0d99ff);
|
|
141
|
+
background: var(--maude-hud-accent, #0d99ff);
|
|
107
142
|
border-radius: 1px;
|
|
108
143
|
box-shadow: 0 0 0 1px var(--bg-0, #ffffff);
|
|
109
144
|
}
|
|
@@ -117,7 +152,7 @@ const HALO_CSS = `
|
|
|
117
152
|
as "draft / placeholder" and members would melt away inside artboards once
|
|
118
153
|
the artboard border itself is 22%-tinted (T15). */
|
|
119
154
|
.dc-cv-halo--selected-member {
|
|
120
|
-
border: 1.5px solid var(--accent, #0d99ff);
|
|
155
|
+
border: 1.5px solid var(--maude-hud-accent, #0d99ff);
|
|
121
156
|
}
|
|
122
157
|
/* Group bbox — 1 px DASHED full accent + four 6 × 6 square corner handles.
|
|
123
158
|
T16 / DDR-046 rev 2 — dashed is the canonical group-container affordance
|
|
@@ -130,14 +165,14 @@ const HALO_CSS = `
|
|
|
130
165
|
position: fixed;
|
|
131
166
|
pointer-events: none;
|
|
132
167
|
z-index: 5;
|
|
133
|
-
border: 1px dashed var(--accent, #0d99ff);
|
|
168
|
+
border: 1px dashed var(--maude-hud-accent, #0d99ff);
|
|
134
169
|
border-radius: 2px;
|
|
135
170
|
}
|
|
136
171
|
.dc-cv-group-bbox .tick {
|
|
137
172
|
position: absolute;
|
|
138
173
|
width: 6px;
|
|
139
174
|
height: 6px;
|
|
140
|
-
background: var(--accent, #0d99ff);
|
|
175
|
+
background: var(--maude-hud-accent, #0d99ff);
|
|
141
176
|
border-radius: 1px;
|
|
142
177
|
box-shadow: 0 0 0 1px var(--bg-0, #ffffff);
|
|
143
178
|
}
|
|
@@ -157,7 +192,7 @@ const HALO_CSS = `
|
|
|
157
192
|
(3 px ring + hard 6×6×0 offset) was readable but visually expensive once
|
|
158
193
|
the frame itself lost its brutalist treatment. A 2 px ring on a 22 %
|
|
159
194
|
tinted hairline reads unambiguous without claiming subject-ness. */
|
|
160
|
-
box-shadow: 0 0 0 2px var(--accent, #0d99ff);
|
|
195
|
+
box-shadow: 0 0 0 2px var(--maude-hud-accent, #0d99ff);
|
|
161
196
|
transition: box-shadow 120ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
162
197
|
}
|
|
163
198
|
/* Respect prefers-reduced-motion across all chrome transitions. */
|
|
@@ -215,10 +250,39 @@ const HALO_CSS = `
|
|
|
215
250
|
.dc-canvas[data-active-tool="eraser"] * {
|
|
216
251
|
cursor: var(--cursor-eraser), cell !important;
|
|
217
252
|
}
|
|
253
|
+
|
|
254
|
+
/* T31 — Level of detail. Below 0.35 zoom we hide pre-attentive chrome that
|
|
255
|
+
becomes visual noise (corner ticks, distance pills, active-artboard ring,
|
|
256
|
+
snap pills). Above 4.0 we coax the browser into crisper text rendering.
|
|
257
|
+
The "normal" band 0.35..4.0 carries the full chrome. */
|
|
258
|
+
.dc-canvas[data-cv-zoom-lod="low"] .dc-cv-halo .tick,
|
|
259
|
+
.dc-canvas[data-cv-zoom-lod="low"] .dc-cv-group-bbox .tick {
|
|
260
|
+
display: none;
|
|
261
|
+
}
|
|
262
|
+
.dc-canvas[data-cv-zoom-lod="low"] .dc-snap-pill,
|
|
263
|
+
.dc-canvas[data-cv-zoom-lod="low"] .dc-cv-eq-pill {
|
|
264
|
+
display: none;
|
|
265
|
+
}
|
|
266
|
+
.dc-canvas[data-cv-zoom-lod="low"] .dc-artboard[aria-current="true"] {
|
|
267
|
+
box-shadow: none;
|
|
268
|
+
}
|
|
269
|
+
.dc-canvas[data-cv-zoom-lod="crisp"] {
|
|
270
|
+
-webkit-font-smoothing: subpixel-antialiased;
|
|
271
|
+
font-smooth: always;
|
|
272
|
+
}
|
|
218
273
|
`.trim();
|
|
219
274
|
|
|
220
275
|
function ensureHaloStyles(): void {
|
|
221
276
|
if (typeof document === 'undefined') return;
|
|
277
|
+
// HUD tokens MUST be injected before HALO_CSS so the cascade resolves
|
|
278
|
+
// `var(--maude-hud-accent, …)` against the dev-server's brand defaults
|
|
279
|
+
// even when the canvas DS's tokens.css later sets `:root { --accent: … }`.
|
|
280
|
+
if (!document.getElementById('dc-cv-hud-tokens-css')) {
|
|
281
|
+
const t = document.createElement('style');
|
|
282
|
+
t.id = 'dc-cv-hud-tokens-css';
|
|
283
|
+
t.textContent = HUD_TOKENS_CSS;
|
|
284
|
+
document.head.appendChild(t);
|
|
285
|
+
}
|
|
222
286
|
if (document.getElementById('dc-cv-halo-css')) return;
|
|
223
287
|
const s = document.createElement('style');
|
|
224
288
|
s.id = 'dc-cv-halo-css';
|
|
@@ -279,9 +343,142 @@ function CanvasCore({
|
|
|
279
343
|
};
|
|
280
344
|
}, [hostRef, tool]);
|
|
281
345
|
|
|
346
|
+
// T28 — modifier-aware cursor (Alt → copy on cd-id, Shift → crosshair on
|
|
347
|
+
// body padding). CSS-driven once data-mod-* is reflected on the host.
|
|
348
|
+
useCursorModifiers(hostRef);
|
|
349
|
+
// T29 — arrow nudge (artboards) + Cmd+A select-all (active artboard).
|
|
350
|
+
// Cmd+D duplicate deferred; no live duplicate channel for either artboards
|
|
351
|
+
// or stamped elements yet.
|
|
352
|
+
useKeyboardDiscipline();
|
|
353
|
+
|
|
282
354
|
const artboardsCtx = useArtboardsContext();
|
|
283
355
|
const dragBus = useDragStateContext();
|
|
284
356
|
|
|
357
|
+
// T33 — programmatic-zoom easing via double-click on empty world only.
|
|
358
|
+
// Per post-Wave-3 user feedback, dblclick-on-artboard auto-zoom was
|
|
359
|
+
// surprising (interfered with native dblclick text-select inside chrome
|
|
360
|
+
// and felt magnetic). We keep the dblclick-empty → `fit()` path because
|
|
361
|
+
// it's a discoverable "back to overview" gesture; artboard zoom is still
|
|
362
|
+
// reachable via Cmd+1 and the zoom HUD.
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
if (!controller) return;
|
|
365
|
+
const host = hostRef.current;
|
|
366
|
+
if (!host) return;
|
|
367
|
+
const onDbl = (e: MouseEvent) => {
|
|
368
|
+
const t = e.target as Element | null;
|
|
369
|
+
if (!t || !t.closest) return;
|
|
370
|
+
// Floating chrome / overlays / drawn user content / any artboard
|
|
371
|
+
// surface → leave alone. Only dblclick that lands on the canvas
|
|
372
|
+
// background outside every artboard triggers `fit()`.
|
|
373
|
+
if (
|
|
374
|
+
t.closest(
|
|
375
|
+
'.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'
|
|
376
|
+
)
|
|
377
|
+
) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (t.closest('[data-dc-screen]')) return; // any part of an artboard
|
|
381
|
+
e.preventDefault();
|
|
382
|
+
controller.fit();
|
|
383
|
+
};
|
|
384
|
+
host.addEventListener('dblclick', onDbl);
|
|
385
|
+
return () => host.removeEventListener('dblclick', onDbl);
|
|
386
|
+
}, [controller, hostRef]);
|
|
387
|
+
|
|
388
|
+
// T31 — level-of-detail attribute on `.dc-canvas`. CSS rules hide
|
|
389
|
+
// pre-attentive chrome (ticks, distance pills, accent ring) below 0.35
|
|
390
|
+
// zoom and sharpen text above 4.0. tldraw's textShadowLod = 0.35 is the
|
|
391
|
+
// canonical threshold. Reads the published viewport (settle-cadence) so
|
|
392
|
+
// the LOD doesn't flicker between bands mid-zoom — chrome should settle
|
|
393
|
+
// once per gesture, not on every frame.
|
|
394
|
+
const publishedZoom = artboardsCtx?.viewport?.zoom ?? 1;
|
|
395
|
+
useEffect(() => {
|
|
396
|
+
const host = hostRef.current;
|
|
397
|
+
if (!host) return;
|
|
398
|
+
const lod = publishedZoom < 0.35 ? 'low' : publishedZoom > 4 ? 'crisp' : 'normal';
|
|
399
|
+
host.setAttribute('data-cv-zoom-lod', lod);
|
|
400
|
+
return () => host.removeAttribute('data-cv-zoom-lod');
|
|
401
|
+
}, [hostRef, publishedZoom]);
|
|
402
|
+
|
|
403
|
+
// Phase 8 — publish local cursor (world coords) + viewport to Awareness
|
|
404
|
+
// so foreign peers can render our cursor on their CursorsOverlay. The
|
|
405
|
+
// collab.publishAwareness call is already throttled to ~30 Hz internally
|
|
406
|
+
// (use-collab.tsx) — we just need to compute screen → world per move.
|
|
407
|
+
const collab = useCollab();
|
|
408
|
+
useEffect(() => {
|
|
409
|
+
const host = hostRef.current;
|
|
410
|
+
if (!host || !collab || !controller) return;
|
|
411
|
+
const onMove = (e: MouseEvent) => {
|
|
412
|
+
const v = controller.viewport;
|
|
413
|
+
// screen → world: world = (screen - viewport.{x,y}) / zoom.
|
|
414
|
+
const worldX = (e.clientX - v.x) / Math.max(v.zoom, 0.0001);
|
|
415
|
+
const worldY = (e.clientY - v.y) / Math.max(v.zoom, 0.0001);
|
|
416
|
+
collab.publishAwareness({ cursor: { x: worldX, y: worldY } });
|
|
417
|
+
};
|
|
418
|
+
const onLeave = () => {
|
|
419
|
+
collab.publishAwareness({ cursor: null });
|
|
420
|
+
};
|
|
421
|
+
host.addEventListener('mousemove', onMove);
|
|
422
|
+
host.addEventListener('mouseleave', onLeave);
|
|
423
|
+
return () => {
|
|
424
|
+
host.removeEventListener('mousemove', onMove);
|
|
425
|
+
host.removeEventListener('mouseleave', onLeave);
|
|
426
|
+
};
|
|
427
|
+
}, [hostRef, collab, controller]);
|
|
428
|
+
|
|
429
|
+
// Phase 8 — publish viewport when it settles. The CursorsOverlay only
|
|
430
|
+
// needs the LOCAL viewport (to transform foreign world coords back to
|
|
431
|
+
// screen), but exposing ours over Awareness sets up Task 6's follow-mode.
|
|
432
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: deliberate settle-cadence — re-publish only when x/y/zoom change, not on every viewport field.
|
|
433
|
+
useEffect(() => {
|
|
434
|
+
if (!collab || !controller) return;
|
|
435
|
+
collab.publishAwareness({ viewport: controller.viewport });
|
|
436
|
+
}, [
|
|
437
|
+
collab,
|
|
438
|
+
controller,
|
|
439
|
+
controller?.viewport.x,
|
|
440
|
+
controller?.viewport.y,
|
|
441
|
+
controller?.viewport.zoom,
|
|
442
|
+
]);
|
|
443
|
+
|
|
444
|
+
// Phase 8 — publish local selection so foreign PeerSelection halos can
|
|
445
|
+
// render around what *I* selected. Convert the first entry's selector
|
|
446
|
+
// (CSS path the peer resolves in their own DOM) + current bounds. The
|
|
447
|
+
// bounds are screen-px from publish time — peers re-resolve cssPath
|
|
448
|
+
// when possible and fall back to bounds otherwise.
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
if (!collab) return;
|
|
451
|
+
const first = selSet.selected[0];
|
|
452
|
+
if (!first || !first.selector) {
|
|
453
|
+
collab.publishAwareness({ selection: null });
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const b = first.bounds;
|
|
457
|
+
const bounds =
|
|
458
|
+
b && typeof b === 'object'
|
|
459
|
+
? {
|
|
460
|
+
x: Number(b.x) || 0,
|
|
461
|
+
y: Number(b.y) || 0,
|
|
462
|
+
w: Number(b.w) || 0,
|
|
463
|
+
h: Number(b.h) || 0,
|
|
464
|
+
}
|
|
465
|
+
: { x: 0, y: 0, w: 0, h: 0 };
|
|
466
|
+
collab.publishAwareness({ selection: { cssPath: first.selector, bounds } });
|
|
467
|
+
}, [collab, selSet.selected]);
|
|
468
|
+
|
|
469
|
+
// Phase 8 — publish annotation selection (Phase 5 strokes). Separate from
|
|
470
|
+
// selSet because annotations have their own selection registry. Peers
|
|
471
|
+
// render halos by querying `[data-id="<id>"]` so the same halo follows
|
|
472
|
+
// resize / move (SVG re-emits with the same data-id). `Optional` flavor
|
|
473
|
+
// because CanvasCore is mounted INSIDE the AnnotationSelectionProvider
|
|
474
|
+
// tree but TypeScript / a defensive boot path can't always prove it.
|
|
475
|
+
const annotSelForPublish = useAnnotationSelectionOptional();
|
|
476
|
+
const annotSelectedIds = annotSelForPublish?.selectedIds;
|
|
477
|
+
useEffect(() => {
|
|
478
|
+
if (!collab) return;
|
|
479
|
+
collab.publishAwareness({ annotationSelection: annotSelectedIds ?? [] });
|
|
480
|
+
}, [collab, annotSelectedIds]);
|
|
481
|
+
|
|
285
482
|
/**
|
|
286
483
|
* T24 — distribute the currently-selected artboards evenly on the given
|
|
287
484
|
* axis. Requires ≥ 3 selected artboards. Sort by leading edge, hold the
|
|
@@ -323,11 +520,98 @@ function CanvasCore({
|
|
|
323
520
|
}
|
|
324
521
|
}
|
|
325
522
|
if (moved.length === 0) return;
|
|
326
|
-
dragBus.commitPositions(moved);
|
|
523
|
+
dragBus.commitPositions(moved, { label: equalSpacingLabel(sorted.length) });
|
|
327
524
|
},
|
|
328
525
|
[artboardsCtx, dragBus, selSet.selected]
|
|
329
526
|
);
|
|
330
527
|
|
|
528
|
+
/**
|
|
529
|
+
* G7 — align selected artboards to a common edge / midline. Requires ≥ 2
|
|
530
|
+
* selected artboards. Six modes:
|
|
531
|
+
* - 'left' → all artboards share the minimum x of the set
|
|
532
|
+
* - 'right' → all artboards share the maximum (x + w) edge
|
|
533
|
+
* - 'center-x' → all artboards share the midpoint of the set's x extent
|
|
534
|
+
* - 'top' → all artboards share the minimum y
|
|
535
|
+
* - 'bottom' → all artboards share the maximum (y + h) edge
|
|
536
|
+
* - 'center-y' → all artboards share the midpoint of the set's y extent
|
|
537
|
+
*
|
|
538
|
+
* Holds the reference edge constant; only the perpendicular axis stays as
|
|
539
|
+
* each artboard already was (alignment doesn't relocate on the orthogonal
|
|
540
|
+
* axis). This matches Figma / Sketch / FigJam align semantics.
|
|
541
|
+
*/
|
|
542
|
+
const alignArtboards = useCallback(
|
|
543
|
+
(mode: AlignMode) => {
|
|
544
|
+
if (!artboardsCtx || !dragBus) return;
|
|
545
|
+
const ids = new Set(
|
|
546
|
+
selSet.selected.filter((s) => !!s.artboardId).map((s) => s.artboardId as string)
|
|
547
|
+
);
|
|
548
|
+
if (ids.size < 2) return;
|
|
549
|
+
const targets: ArtboardRect[] = artboardsCtx.artboards.filter((r) => ids.has(r.id));
|
|
550
|
+
if (targets.length < 2) return;
|
|
551
|
+
|
|
552
|
+
// Union bbox for center modes.
|
|
553
|
+
let xMin = Number.POSITIVE_INFINITY;
|
|
554
|
+
let yMin = Number.POSITIVE_INFINITY;
|
|
555
|
+
let xMax = Number.NEGATIVE_INFINITY;
|
|
556
|
+
let yMax = Number.NEGATIVE_INFINITY;
|
|
557
|
+
for (const r of targets) {
|
|
558
|
+
if (r.x < xMin) xMin = r.x;
|
|
559
|
+
if (r.y < yMin) yMin = r.y;
|
|
560
|
+
if (r.x + r.w > xMax) xMax = r.x + r.w;
|
|
561
|
+
if (r.y + r.h > yMax) yMax = r.y + r.h;
|
|
562
|
+
}
|
|
563
|
+
const cx = (xMin + xMax) / 2;
|
|
564
|
+
const cy = (yMin + yMax) / 2;
|
|
565
|
+
|
|
566
|
+
const moved: { id: string; x: number; y: number }[] = [];
|
|
567
|
+
for (const r of targets) {
|
|
568
|
+
let nx = r.x;
|
|
569
|
+
let ny = r.y;
|
|
570
|
+
switch (mode) {
|
|
571
|
+
case 'left':
|
|
572
|
+
nx = xMin;
|
|
573
|
+
break;
|
|
574
|
+
case 'right':
|
|
575
|
+
nx = xMax - r.w;
|
|
576
|
+
break;
|
|
577
|
+
case 'center-x':
|
|
578
|
+
nx = cx - r.w / 2;
|
|
579
|
+
break;
|
|
580
|
+
case 'top':
|
|
581
|
+
ny = yMin;
|
|
582
|
+
break;
|
|
583
|
+
case 'bottom':
|
|
584
|
+
ny = yMax - r.h;
|
|
585
|
+
break;
|
|
586
|
+
case 'center-y':
|
|
587
|
+
ny = cy - r.h / 2;
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
if (Math.round(nx) === r.x && Math.round(ny) === r.y) continue;
|
|
591
|
+
moved.push({ id: r.id, x: Math.round(nx), y: Math.round(ny) });
|
|
592
|
+
}
|
|
593
|
+
if (moved.length === 0) return;
|
|
594
|
+
dragBus.commitPositions(moved, { label: alignLabel(mode, targets.length) });
|
|
595
|
+
},
|
|
596
|
+
[artboardsCtx, dragBus, selSet.selected]
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* G2v2 — "Fit just this artboard" context-menu entry previously bridged
|
|
601
|
+
* via a synthetic click on the label button (which used to call
|
|
602
|
+
* controller.jumpTo). Now that the label is purely a11y, the menu entry
|
|
603
|
+
* needs a direct path: look the rect up from the live artboards list and
|
|
604
|
+
* call jumpTo straight on the controller.
|
|
605
|
+
*/
|
|
606
|
+
const focusArtboard = useCallback(
|
|
607
|
+
(artboardId: string) => {
|
|
608
|
+
if (!controller || !artboardsCtx) return;
|
|
609
|
+
const rect = artboardsCtx.artboards.find((r) => r.id === artboardId);
|
|
610
|
+
if (rect) controller.jumpTo(rect);
|
|
611
|
+
},
|
|
612
|
+
[controller, artboardsCtx]
|
|
613
|
+
);
|
|
614
|
+
|
|
331
615
|
const registry = useMemo<ContextRegistry>(
|
|
332
616
|
() =>
|
|
333
617
|
buildRegistry({
|
|
@@ -335,8 +619,10 @@ function CanvasCore({
|
|
|
335
619
|
clearSelection: selSet.clear,
|
|
336
620
|
selSet,
|
|
337
621
|
distributeArtboards,
|
|
622
|
+
alignArtboards,
|
|
623
|
+
focusArtboard,
|
|
338
624
|
}),
|
|
339
|
-
[controller, selSet, distributeArtboards]
|
|
625
|
+
[controller, selSet, distributeArtboards, alignArtboards, focusArtboard]
|
|
340
626
|
);
|
|
341
627
|
|
|
342
628
|
// Distribute is reached via the MultiArtboardToolbar (floating chrome
|
|
@@ -346,7 +632,11 @@ function CanvasCore({
|
|
|
346
632
|
return (
|
|
347
633
|
<ExportDialogProvider>
|
|
348
634
|
<ContextMenuProvider registry={registry}>
|
|
349
|
-
<CanvasRouter
|
|
635
|
+
<CanvasRouter
|
|
636
|
+
hostRef={hostRef}
|
|
637
|
+
distributeArtboards={distributeArtboards}
|
|
638
|
+
alignArtboards={alignArtboards}
|
|
639
|
+
>
|
|
350
640
|
{children}
|
|
351
641
|
</CanvasRouter>
|
|
352
642
|
</ContextMenuProvider>
|
|
@@ -362,14 +652,19 @@ function buildRegistry(deps: {
|
|
|
362
652
|
clearSelection: () => void;
|
|
363
653
|
selSet: { selected: Selection[] };
|
|
364
654
|
distributeArtboards: (axis: 'x' | 'y') => void;
|
|
655
|
+
alignArtboards: (mode: 'left' | 'right' | 'center-x' | 'top' | 'bottom' | 'center-y') => void;
|
|
656
|
+
focusArtboard: (artboardId: string) => void;
|
|
365
657
|
}): ContextRegistry {
|
|
366
|
-
const { controller, clearSelection, selSet, distributeArtboards } =
|
|
658
|
+
const { controller, clearSelection, selSet, distributeArtboards, alignArtboards, focusArtboard } =
|
|
659
|
+
deps;
|
|
367
660
|
|
|
368
661
|
// T24 — distribute commands are only enabled when ≥ 3 artboards are
|
|
369
662
|
// selected. Below that, the menu items render as `disabled` so the user
|
|
370
663
|
// sees the affordance but understands the precondition.
|
|
664
|
+
// G7 — align commands are enabled at ≥ 2 (alignment is well-defined with 2).
|
|
371
665
|
const selectedArtboardCount = selSet.selected.filter((s) => !!s.artboardId).length;
|
|
372
666
|
const distributeEnabled = selectedArtboardCount >= 3;
|
|
667
|
+
const alignEnabled = selectedArtboardCount >= 2;
|
|
373
668
|
|
|
374
669
|
const copy = (text: string): void => {
|
|
375
670
|
if (typeof navigator === 'undefined' || !navigator.clipboard) return;
|
|
@@ -492,19 +787,54 @@ function buildRegistry(deps: {
|
|
|
492
787
|
id: 'fit-one',
|
|
493
788
|
label: 'Fit just this artboard',
|
|
494
789
|
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();
|
|
790
|
+
if (!target.artboardId) return;
|
|
791
|
+
focusArtboard(target.artboardId);
|
|
503
792
|
},
|
|
504
793
|
},
|
|
505
794
|
fitItem,
|
|
506
795
|
resetItem,
|
|
507
796
|
],
|
|
797
|
+
[
|
|
798
|
+
// G7 — align commands. Six modes; gated on ≥ 2 selected artboards
|
|
799
|
+
// (alignment is well-defined with 2). Primary surface is the
|
|
800
|
+
// MultiArtboardToolbar; menu entries are discoverability backup.
|
|
801
|
+
{
|
|
802
|
+
id: 'align-left',
|
|
803
|
+
label: 'Align left',
|
|
804
|
+
disabled: !alignEnabled,
|
|
805
|
+
onSelect: () => alignArtboards('left'),
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
id: 'align-center-x',
|
|
809
|
+
label: 'Align center (horizontal)',
|
|
810
|
+
disabled: !alignEnabled,
|
|
811
|
+
onSelect: () => alignArtboards('center-x'),
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
id: 'align-right',
|
|
815
|
+
label: 'Align right',
|
|
816
|
+
disabled: !alignEnabled,
|
|
817
|
+
onSelect: () => alignArtboards('right'),
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
id: 'align-top',
|
|
821
|
+
label: 'Align top',
|
|
822
|
+
disabled: !alignEnabled,
|
|
823
|
+
onSelect: () => alignArtboards('top'),
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
id: 'align-center-y',
|
|
827
|
+
label: 'Align center (vertical)',
|
|
828
|
+
disabled: !alignEnabled,
|
|
829
|
+
onSelect: () => alignArtboards('center-y'),
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
id: 'align-bottom',
|
|
833
|
+
label: 'Align bottom',
|
|
834
|
+
disabled: !alignEnabled,
|
|
835
|
+
onSelect: () => alignArtboards('bottom'),
|
|
836
|
+
},
|
|
837
|
+
],
|
|
508
838
|
[
|
|
509
839
|
// T24 — distribute commands. Primary surface is the floating
|
|
510
840
|
// MultiArtboardToolbar above the group bbox; the menu entries are
|
|
@@ -553,15 +883,18 @@ function CanvasRouter({
|
|
|
553
883
|
hostRef,
|
|
554
884
|
children,
|
|
555
885
|
distributeArtboards,
|
|
886
|
+
alignArtboards,
|
|
556
887
|
}: {
|
|
557
888
|
hostRef: RefObject<HTMLDivElement | null>;
|
|
558
889
|
children: ReactNode;
|
|
559
890
|
distributeArtboards: (axis: 'x' | 'y') => void;
|
|
891
|
+
alignArtboards: (mode: 'left' | 'right' | 'center-x' | 'top' | 'bottom' | 'center-y') => void;
|
|
560
892
|
}) {
|
|
561
893
|
const { tool, setTool, clearSticky } = useToolMode();
|
|
562
894
|
const selSet = useSelectionSet();
|
|
563
895
|
const annotSel = useAnnotationSelection();
|
|
564
896
|
const ctxMenu = useContextMenu();
|
|
897
|
+
const undoStack = useUndoStack();
|
|
565
898
|
|
|
566
899
|
// Hover state drives the floating .dc-cv-halo--hover overlay. The overlay
|
|
567
900
|
// itself reads getBoundingClientRect on every rAF tick to follow pan/zoom.
|
|
@@ -663,6 +996,12 @@ function CanvasRouter({
|
|
|
663
996
|
ctxMenu.open(ctxTarget);
|
|
664
997
|
},
|
|
665
998
|
onTool: ({ tool: t }) => setTool(t),
|
|
999
|
+
onUndo: () => {
|
|
1000
|
+
void undoStack.undo();
|
|
1001
|
+
},
|
|
1002
|
+
onRedo: () => {
|
|
1003
|
+
void undoStack.redo();
|
|
1004
|
+
},
|
|
666
1005
|
onEscape: () => {
|
|
667
1006
|
// T21 — abort any mid-stroke draw FIRST. The annotations layer
|
|
668
1007
|
// listens for `maude:cancel-stroke` and drops the in-progress
|
|
@@ -781,11 +1120,21 @@ function CanvasRouter({
|
|
|
781
1120
|
<AnnotationsLayer />
|
|
782
1121
|
<ToolPalette />
|
|
783
1122
|
<ArtboardMarqueeOverlay />
|
|
1123
|
+
<ElementMarqueeOverlay />
|
|
784
1124
|
<HoverHalo el={hoverEl} />
|
|
785
1125
|
<SelectionHalos />
|
|
786
1126
|
<GroupBbox />
|
|
787
|
-
<
|
|
1127
|
+
<EqualSpacingHandles />
|
|
1128
|
+
<ContextualToolbar />
|
|
1129
|
+
<MultiArtboardToolbar
|
|
1130
|
+
distributeArtboards={distributeArtboards}
|
|
1131
|
+
alignArtboards={alignArtboards}
|
|
1132
|
+
/>
|
|
788
1133
|
<SnapGuideOverlay />
|
|
1134
|
+
<UndoHud />
|
|
1135
|
+
<CursorsOverlay />
|
|
1136
|
+
<AiBanner />
|
|
1137
|
+
<ParticipantsChrome />
|
|
789
1138
|
</>
|
|
790
1139
|
);
|
|
791
1140
|
}
|
|
@@ -831,7 +1180,7 @@ const MULTI_TOOLBAR_CSS = `
|
|
|
831
1180
|
transition: background-color 80ms linear;
|
|
832
1181
|
}
|
|
833
1182
|
.dc-multi-artboard-tb button:hover:not(:disabled) {
|
|
834
|
-
background: color-mix(in oklab, var(--accent, #d63b1f) 8%, transparent);
|
|
1183
|
+
background: color-mix(in oklab, var(--maude-hud-accent, #d63b1f) 8%, transparent);
|
|
835
1184
|
}
|
|
836
1185
|
.dc-multi-artboard-tb button:disabled {
|
|
837
1186
|
cursor: default;
|
|
@@ -849,6 +1198,12 @@ const MULTI_TOOLBAR_CSS = `
|
|
|
849
1198
|
align-items: center;
|
|
850
1199
|
justify-content: center;
|
|
851
1200
|
}
|
|
1201
|
+
.dc-multi-artboard-tb .dc-mab-divider {
|
|
1202
|
+
width: 1px;
|
|
1203
|
+
align-self: stretch;
|
|
1204
|
+
background: var(--u-border-subtle, rgba(0,0,0,0.10));
|
|
1205
|
+
margin: 0 4px;
|
|
1206
|
+
}
|
|
852
1207
|
@media (prefers-reduced-motion: reduce) {
|
|
853
1208
|
.dc-multi-artboard-tb button { transition: none; }
|
|
854
1209
|
}
|
|
@@ -865,8 +1220,10 @@ function ensureMultiToolbarStyles(): void {
|
|
|
865
1220
|
|
|
866
1221
|
function MultiArtboardToolbar({
|
|
867
1222
|
distributeArtboards,
|
|
1223
|
+
alignArtboards,
|
|
868
1224
|
}: {
|
|
869
1225
|
distributeArtboards: (axis: 'x' | 'y') => void;
|
|
1226
|
+
alignArtboards: (mode: 'left' | 'right' | 'center-x' | 'top' | 'bottom' | 'center-y') => void;
|
|
870
1227
|
}) {
|
|
871
1228
|
ensureMultiToolbarStyles();
|
|
872
1229
|
const { selected } = useSelectionSet();
|
|
@@ -875,7 +1232,8 @@ function MultiArtboardToolbar({
|
|
|
875
1232
|
|
|
876
1233
|
const artboardSelections = useMemo(() => selected.filter((s) => !!s.artboardId), [selected]);
|
|
877
1234
|
const artboardCount = artboardSelections.length;
|
|
878
|
-
const
|
|
1235
|
+
const distributeOk = artboardCount >= 3;
|
|
1236
|
+
const alignOk = artboardCount >= 2;
|
|
879
1237
|
|
|
880
1238
|
useEffect(() => {
|
|
881
1239
|
const div = ref.current;
|
|
@@ -943,11 +1301,105 @@ function MultiArtboardToolbar({
|
|
|
943
1301
|
aria-label="Multi-artboard actions"
|
|
944
1302
|
>
|
|
945
1303
|
<span className="dc-mab-count">{artboardCount} artboards</span>
|
|
1304
|
+
{/* G7 — align cluster. Icon-only to keep the toolbar narrow; full
|
|
1305
|
+
label lives in the tooltip + the right-click menu. Enabled at ≥ 2
|
|
1306
|
+
artboards (alignment is well-defined with 2). */}
|
|
1307
|
+
<button
|
|
1308
|
+
type="button"
|
|
1309
|
+
disabled={!alignOk}
|
|
1310
|
+
title={alignOk ? 'Align left' : 'Select at least 2 artboards to align'}
|
|
1311
|
+
aria-label="Align left"
|
|
1312
|
+
onClick={() => alignArtboards('left')}
|
|
1313
|
+
>
|
|
1314
|
+
<span className="dc-mab-icon" aria-hidden="true">
|
|
1315
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
1316
|
+
<line x1="0.5" y1="0.5" x2="0.5" y2="13.5" stroke="currentColor" />
|
|
1317
|
+
<rect x="1" y="2" width="7" height="3" fill="currentColor" />
|
|
1318
|
+
<rect x="1" y="9" width="11" height="3" fill="currentColor" />
|
|
1319
|
+
</svg>
|
|
1320
|
+
</span>
|
|
1321
|
+
</button>
|
|
1322
|
+
<button
|
|
1323
|
+
type="button"
|
|
1324
|
+
disabled={!alignOk}
|
|
1325
|
+
title={alignOk ? 'Align center (horizontal)' : 'Select at least 2 artboards to align'}
|
|
1326
|
+
aria-label="Align center horizontally"
|
|
1327
|
+
onClick={() => alignArtboards('center-x')}
|
|
1328
|
+
>
|
|
1329
|
+
<span className="dc-mab-icon" aria-hidden="true">
|
|
1330
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
1331
|
+
<line x1="7" y1="0.5" x2="7" y2="13.5" stroke="currentColor" />
|
|
1332
|
+
<rect x="3.5" y="2" width="7" height="3" fill="currentColor" />
|
|
1333
|
+
<rect x="1.5" y="9" width="11" height="3" fill="currentColor" />
|
|
1334
|
+
</svg>
|
|
1335
|
+
</span>
|
|
1336
|
+
</button>
|
|
1337
|
+
<button
|
|
1338
|
+
type="button"
|
|
1339
|
+
disabled={!alignOk}
|
|
1340
|
+
title={alignOk ? 'Align right' : 'Select at least 2 artboards to align'}
|
|
1341
|
+
aria-label="Align right"
|
|
1342
|
+
onClick={() => alignArtboards('right')}
|
|
1343
|
+
>
|
|
1344
|
+
<span className="dc-mab-icon" aria-hidden="true">
|
|
1345
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
1346
|
+
<line x1="13.5" y1="0.5" x2="13.5" y2="13.5" stroke="currentColor" />
|
|
1347
|
+
<rect x="6" y="2" width="7" height="3" fill="currentColor" />
|
|
1348
|
+
<rect x="2" y="9" width="11" height="3" fill="currentColor" />
|
|
1349
|
+
</svg>
|
|
1350
|
+
</span>
|
|
1351
|
+
</button>
|
|
1352
|
+
<button
|
|
1353
|
+
type="button"
|
|
1354
|
+
disabled={!alignOk}
|
|
1355
|
+
title={alignOk ? 'Align top' : 'Select at least 2 artboards to align'}
|
|
1356
|
+
aria-label="Align top"
|
|
1357
|
+
onClick={() => alignArtboards('top')}
|
|
1358
|
+
>
|
|
1359
|
+
<span className="dc-mab-icon" aria-hidden="true">
|
|
1360
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
1361
|
+
<line x1="0.5" y1="0.5" x2="13.5" y2="0.5" stroke="currentColor" />
|
|
1362
|
+
<rect x="2" y="1" width="3" height="7" fill="currentColor" />
|
|
1363
|
+
<rect x="9" y="1" width="3" height="11" fill="currentColor" />
|
|
1364
|
+
</svg>
|
|
1365
|
+
</span>
|
|
1366
|
+
</button>
|
|
1367
|
+
<button
|
|
1368
|
+
type="button"
|
|
1369
|
+
disabled={!alignOk}
|
|
1370
|
+
title={alignOk ? 'Align center (vertical)' : 'Select at least 2 artboards to align'}
|
|
1371
|
+
aria-label="Align center vertically"
|
|
1372
|
+
onClick={() => alignArtboards('center-y')}
|
|
1373
|
+
>
|
|
1374
|
+
<span className="dc-mab-icon" aria-hidden="true">
|
|
1375
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
1376
|
+
<line x1="0.5" y1="7" x2="13.5" y2="7" stroke="currentColor" />
|
|
1377
|
+
<rect x="2" y="3.5" width="3" height="7" fill="currentColor" />
|
|
1378
|
+
<rect x="9" y="1.5" width="3" height="11" fill="currentColor" />
|
|
1379
|
+
</svg>
|
|
1380
|
+
</span>
|
|
1381
|
+
</button>
|
|
1382
|
+
<button
|
|
1383
|
+
type="button"
|
|
1384
|
+
disabled={!alignOk}
|
|
1385
|
+
title={alignOk ? 'Align bottom' : 'Select at least 2 artboards to align'}
|
|
1386
|
+
aria-label="Align bottom"
|
|
1387
|
+
onClick={() => alignArtboards('bottom')}
|
|
1388
|
+
>
|
|
1389
|
+
<span className="dc-mab-icon" aria-hidden="true">
|
|
1390
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
1391
|
+
<line x1="0.5" y1="13.5" x2="13.5" y2="13.5" stroke="currentColor" />
|
|
1392
|
+
<rect x="2" y="6" width="3" height="7" fill="currentColor" />
|
|
1393
|
+
<rect x="9" y="2" width="3" height="11" fill="currentColor" />
|
|
1394
|
+
</svg>
|
|
1395
|
+
</span>
|
|
1396
|
+
</button>
|
|
1397
|
+
<span className="dc-mab-divider" aria-hidden="true" />
|
|
946
1398
|
<button
|
|
947
1399
|
type="button"
|
|
948
|
-
disabled={!
|
|
1400
|
+
disabled={!distributeOk}
|
|
949
1401
|
title={
|
|
950
|
-
|
|
1402
|
+
distributeOk
|
|
951
1403
|
? 'Distribute horizontally — equal gaps between artboards'
|
|
952
1404
|
: 'Select at least 3 artboards to distribute'
|
|
953
1405
|
}
|
|
@@ -961,13 +1413,12 @@ function MultiArtboardToolbar({
|
|
|
961
1413
|
<rect x="10.5" y="3" width="3" height="8" fill="currentColor" />
|
|
962
1414
|
</svg>
|
|
963
1415
|
</span>
|
|
964
|
-
Distribute H
|
|
965
1416
|
</button>
|
|
966
1417
|
<button
|
|
967
1418
|
type="button"
|
|
968
|
-
disabled={!
|
|
1419
|
+
disabled={!distributeOk}
|
|
969
1420
|
title={
|
|
970
|
-
|
|
1421
|
+
distributeOk
|
|
971
1422
|
? 'Distribute vertically — equal gaps between artboards'
|
|
972
1423
|
: 'Select at least 3 artboards to distribute'
|
|
973
1424
|
}
|
|
@@ -981,7 +1432,6 @@ function MultiArtboardToolbar({
|
|
|
981
1432
|
<rect x="3" y="10.5" width="8" height="3" fill="currentColor" />
|
|
982
1433
|
</svg>
|
|
983
1434
|
</span>
|
|
984
|
-
Distribute V
|
|
985
1435
|
</button>
|
|
986
1436
|
</div>
|
|
987
1437
|
);
|