@1agh/maude 0.29.0 → 0.30.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 +1 -1
- package/apps/studio/ai-banner.tsx +0 -1
- package/apps/studio/annotations-align.ts +149 -0
- package/apps/studio/annotations-bindings.ts +197 -0
- package/apps/studio/annotations-context-toolbar.tsx +436 -186
- package/apps/studio/annotations-groups.ts +270 -0
- package/apps/studio/annotations-layer.tsx +1708 -1660
- package/apps/studio/annotations-model.ts +2077 -0
- package/apps/studio/annotations-snap.ts +125 -0
- package/apps/studio/api.ts +208 -116
- package/apps/studio/bin/_png-playwright.mjs +1 -1
- package/apps/studio/bin/annotate.mjs +732 -0
- package/apps/studio/bin/annotate.sh +17 -0
- package/apps/studio/bin/read-annotations.mjs +152 -17
- package/apps/studio/build.ts +1 -1
- package/apps/studio/canvas-arrowheads.ts +78 -9
- package/apps/studio/canvas-cursors.ts +2 -0
- package/apps/studio/canvas-edit.ts +257 -7
- package/apps/studio/canvas-icons.tsx +105 -0
- package/apps/studio/canvas-shell.tsx +305 -8
- package/apps/studio/client/app.jsx +2812 -238
- package/apps/studio/client/index.html +1 -1
- package/apps/studio/client/styles/3-shell-maude.css +713 -11
- package/apps/studio/client/styles/4-components.css +39 -0
- package/apps/studio/client/styles/5-maude-overrides.css +16 -2
- package/apps/studio/client/tour/overlay.jsx +105 -17
- package/apps/studio/client/tour/usage-tour.js +23 -10
- package/apps/studio/commands/annotation-strokes-command.ts +1 -1
- package/apps/studio/commands/move-artboards-command.ts +1 -1
- package/apps/studio/comments-overlay.tsx +7 -5
- package/apps/studio/context-menu.tsx +4 -3
- package/apps/studio/contextual-toolbar.tsx +14 -0
- package/apps/studio/cursors-overlay.tsx +20 -20
- package/apps/studio/dist/client.bundle.js +3322 -602
- package/apps/studio/dist/comment-mount.js +95 -9
- package/apps/studio/dist/styles.css +2490 -167
- package/apps/studio/dom-selection.ts +115 -0
- package/apps/studio/export-dialog.tsx +3 -3
- package/apps/studio/history.ts +20 -3
- package/apps/studio/http.ts +124 -5
- package/apps/studio/input-router.tsx +7 -0
- package/apps/studio/inspect.ts +37 -8
- package/apps/studio/participants-chrome.tsx +1 -7
- package/apps/studio/server.ts +5 -2
- package/apps/studio/sync/agent.ts +231 -57
- package/apps/studio/sync/codec.ts +45 -0
- package/apps/studio/sync/cold-start.ts +158 -0
- package/apps/studio/sync/connection-state.ts +58 -2
- package/apps/studio/sync/index.ts +563 -221
- package/apps/studio/sync/journal.ts +190 -0
- package/apps/studio/sync/migrate-seed.ts +202 -44
- package/apps/studio/sync/projection.ts +22 -3
- package/apps/studio/sync/status.ts +15 -4
- package/apps/studio/test/activity.test.ts +1 -7
- package/apps/studio/test/annotate-write.test.ts +184 -0
- package/apps/studio/test/annotations-align.test.ts +88 -0
- package/apps/studio/test/annotations-bindings.test.ts +124 -0
- package/apps/studio/test/annotations-groups.test.ts +231 -0
- package/apps/studio/test/annotations-snap.test.ts +79 -0
- package/apps/studio/test/canvas-edit.test.ts +181 -1
- package/apps/studio/test/canvas-origin-gate.test.ts +8 -0
- package/apps/studio/test/collab-bridge.test.ts +0 -2
- package/apps/studio/test/collab-room.test.ts +2 -7
- package/apps/studio/test/csp-canvas-shell.test.ts +5 -0
- package/apps/studio/test/csrf-write-guard.test.ts +52 -0
- package/apps/studio/test/figjam-v3-model.test.ts +342 -0
- package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +1 -0
- package/apps/studio/test/shared-doc-migrate.test.ts +224 -14
- package/apps/studio/test/sync-agent.test.ts +272 -2
- package/apps/studio/test/sync-codec.test.ts +65 -0
- package/apps/studio/test/sync-cold-start.test.ts +244 -0
- package/apps/studio/test/sync-connection-state.test.ts +68 -0
- package/apps/studio/test/sync-hardening.test.ts +0 -8
- package/apps/studio/test/sync-hubs-config.test.ts +0 -1
- package/apps/studio/test/sync-incident-replay.test.ts +211 -0
- package/apps/studio/test/sync-journal.test.ts +176 -0
- package/apps/studio/test/sync-runtime.test.ts +360 -5
- package/apps/studio/test/sync-status.test.ts +66 -0
- package/apps/studio/test/use-annotation-resize.test.ts +159 -0
- package/apps/studio/test/use-artboard-drag.test.ts +0 -1
- package/apps/studio/test/use-collab.test.ts +0 -0
- package/apps/studio/test/use-tool-mode.test.tsx +5 -2
- package/apps/studio/tool-palette.tsx +12 -2
- package/apps/studio/undo-hud.tsx +0 -1
- package/apps/studio/use-agent-presence.tsx +4 -2
- package/apps/studio/use-annotation-resize.tsx +484 -54
- package/apps/studio/use-annotation-selection.tsx +16 -4
- package/apps/studio/use-canvas-activity.tsx +2 -2
- package/apps/studio/use-collab.tsx +20 -15
- package/apps/studio/use-selection-set.tsx +9 -0
- package/apps/studio/use-tool-mode.tsx +5 -1
- package/apps/studio/whats-new.json +135 -10
- package/apps/studio/whats-new.schema.json +18 -2
- package/cli/commands/design-link.test.mjs +84 -0
- package/cli/commands/design.mjs +2 -1
- package/cli/commands/design.test.mjs +5 -1
- package/cli/lib/design-link.mjs +51 -1
- package/package.json +8 -8
- package/plugins/design/templates/_shell.html +28 -4
|
@@ -1353,6 +1353,45 @@
|
|
|
1353
1353
|
color: var(--u-fg-2);
|
|
1354
1354
|
margin-bottom: 14px;
|
|
1355
1355
|
}
|
|
1356
|
+
/* "Do this one thing" hint — shown when a step needs a canvas open or an element
|
|
1357
|
+
⌘-clicked (the cross-origin canvas means the tour can't do it for you). */
|
|
1358
|
+
.mdcc-tour__hint {
|
|
1359
|
+
display: flex;
|
|
1360
|
+
align-items: center;
|
|
1361
|
+
gap: 8px;
|
|
1362
|
+
margin: -4px 0 14px;
|
|
1363
|
+
padding: 8px 10px;
|
|
1364
|
+
border-radius: var(--radius-md);
|
|
1365
|
+
background: color-mix(in oklab, var(--u-accent) 12%, transparent);
|
|
1366
|
+
border: 1px solid color-mix(in oklab, var(--u-accent) 35%, transparent);
|
|
1367
|
+
color: var(--u-fg-1);
|
|
1368
|
+
font-size: 12px;
|
|
1369
|
+
line-height: 1.4;
|
|
1370
|
+
}
|
|
1371
|
+
.mdcc-tour__hint-dot {
|
|
1372
|
+
flex: none;
|
|
1373
|
+
width: 8px;
|
|
1374
|
+
height: 8px;
|
|
1375
|
+
border-radius: 50%;
|
|
1376
|
+
background: var(--u-accent);
|
|
1377
|
+
animation: mdcc-tour-pulse 1.4s ease-in-out infinite;
|
|
1378
|
+
}
|
|
1379
|
+
@keyframes mdcc-tour-pulse {
|
|
1380
|
+
0%,
|
|
1381
|
+
100% {
|
|
1382
|
+
opacity: 0.4;
|
|
1383
|
+
transform: scale(0.85);
|
|
1384
|
+
}
|
|
1385
|
+
50% {
|
|
1386
|
+
opacity: 1;
|
|
1387
|
+
transform: scale(1.15);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1391
|
+
.mdcc-tour__hint-dot {
|
|
1392
|
+
animation: none;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1356
1395
|
.mdcc-tour__actions {
|
|
1357
1396
|
display: flex;
|
|
1358
1397
|
align-items: center;
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
.maude .help-modal-hd {
|
|
26
26
|
border-bottom: 1px solid var(--border-subtle);
|
|
27
27
|
background: var(--bg-1);
|
|
28
|
+
/* title + SKU sat glued together (flex space-between, no gap) */
|
|
29
|
+
justify-content: flex-start;
|
|
30
|
+
gap: var(--space-4);
|
|
28
31
|
}
|
|
29
32
|
.maude .help-modal-hd .title { font-family: var(--font-display); font-weight: 600; letter-spacing: var(--tracking-tight); text-transform: none; color: var(--fg-0); }
|
|
30
33
|
.maude .help-modal-hd .sku { font-family: var(--font-mono); color: var(--fg-3); }
|
|
@@ -65,6 +68,8 @@
|
|
|
65
68
|
.maude .mdcc-tour-nudge {
|
|
66
69
|
background: var(--bg-2); border: 1px solid var(--border-default); border-left: 3px solid var(--accent);
|
|
67
70
|
border-radius: var(--radius-md); box-shadow: var(--shadow-lg); font-family: var(--font-body); color: var(--fg-1);
|
|
71
|
+
/* raised above the in-canvas zoom pill (bottom-left), same as .st-toast */
|
|
72
|
+
bottom: 84px;
|
|
68
73
|
}
|
|
69
74
|
.maude .mdcc-tour-nudge__cta { background: var(--accent); color: var(--accent-fg); border: 0; border-radius: var(--radius-sm); font-family: var(--font-body); font-weight: 500; }
|
|
70
75
|
.maude .mdcc-tour-nudge__cta:hover { background: var(--accent-hover); }
|
|
@@ -108,8 +113,17 @@
|
|
|
108
113
|
.maude .sv-h-num, .maude .sv-count { font-family: var(--font-mono); color: var(--fg-3); }
|
|
109
114
|
.maude .sv-tok-cell { border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); background: var(--bg-1); overflow: hidden; }
|
|
110
115
|
.maude .sv-tok-swatch { border-radius: 0; }
|
|
111
|
-
|
|
112
|
-
|
|
116
|
+
/* token meta — the legacy side-by-side row wrapped long names across 3 lines
|
|
117
|
+
* ("--presence-" / "offline"); stack name over value, each on one nowrap line. */
|
|
118
|
+
.maude .sv-tok-meta {
|
|
119
|
+
flex-direction: column;
|
|
120
|
+
align-items: flex-start;
|
|
121
|
+
justify-content: flex-start;
|
|
122
|
+
gap: 1px;
|
|
123
|
+
padding: var(--space-2) var(--space-3);
|
|
124
|
+
}
|
|
125
|
+
.maude .sv-tok-name { font-family: var(--font-mono); color: var(--fg-0); white-space: nowrap; max-width: 100%; }
|
|
126
|
+
.maude .sv-tok-value { font-family: var(--font-mono); color: var(--fg-2); max-width: 100%; }
|
|
113
127
|
.maude .sv-type-row { border-bottom: 1px solid var(--border-subtle); }
|
|
114
128
|
.maude .sv-type-tok { font-family: var(--font-mono); color: var(--fg-2); }
|
|
115
129
|
.maude .sv-preview-card { border: 1px solid var(--border-subtle); border-radius: var(--radius-md); background: var(--bg-1); overflow: hidden; box-shadow: var(--shadow-sm); }
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
// overlay.jsx — hand-rolled guided-tour engine (zero runtime dep, DDR-B).
|
|
2
2
|
//
|
|
3
|
-
// One <TourOverlay steps={[…]} open onClose onComplete
|
|
4
|
-
// per-feature spotlight tours (a What's New entry's `tour[]`) and
|
|
5
|
-
// usage tour.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// One <TourOverlay steps={[…]} open onClose onComplete bus hasSelection hasCanvas/>
|
|
4
|
+
// drives both the per-feature spotlight tours (a What's New entry's `tour[]`) and
|
|
5
|
+
// the evergreen usage tour.
|
|
6
|
+
//
|
|
7
|
+
// A step is { target, title, body, placement?, ...setup }. The engine does more
|
|
8
|
+
// than spotlight: before each step it puts the shell into the state the target
|
|
9
|
+
// needs, then waits for the element to actually render before measuring — so the
|
|
10
|
+
// tour *walks the user through the live feature* instead of floating a modal over
|
|
11
|
+
// a missing element. Setup directives (all optional):
|
|
12
|
+
// canvas: true → ensure a canvas is open (bus.setup opens the first)
|
|
13
|
+
// inspector: true → open the right-hand Inspector
|
|
14
|
+
// tab: 'inspect'|'layers'|'css' → switch the Inspector tab
|
|
15
|
+
// requireSelection: true → the target only exists once an element is ⌘-clicked
|
|
16
|
+
// in the canvas; the card shows a "⌘-click to continue"
|
|
17
|
+
// hint and the spotlight snaps onto the real row the
|
|
18
|
+
// moment a selection lands.
|
|
19
|
+
// A step whose target still can't be resolved shows centered (no spotlight) — the
|
|
20
|
+
// tour never dead-ends on a missing element.
|
|
8
21
|
//
|
|
9
22
|
// A11y: role="dialog" + aria-modal, focus moves to the primary button on each
|
|
10
23
|
// step and is trapped within the card, focus is restored on close, Esc skips,
|
|
@@ -26,7 +39,7 @@ function getRect(target) {
|
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
const CARD_W = 320;
|
|
29
|
-
const CARD_H_EST =
|
|
42
|
+
const CARD_H_EST = 184;
|
|
30
43
|
|
|
31
44
|
function computeCardStyle(rect) {
|
|
32
45
|
if (typeof window === 'undefined' || !rect) {
|
|
@@ -34,6 +47,13 @@ function computeCardStyle(rect) {
|
|
|
34
47
|
}
|
|
35
48
|
const vw = window.innerWidth;
|
|
36
49
|
const vh = window.innerHeight;
|
|
50
|
+
const placeLeft = rect.left > CARD_W + 36 && vw - rect.right < CARD_W + 36;
|
|
51
|
+
if (placeLeft) {
|
|
52
|
+
// Panel-on-the-right targets (the Inspector): park the card to its left so it
|
|
53
|
+
// never covers the very thing it's pointing at.
|
|
54
|
+
const top = Math.min(Math.max(12, rect.top), vh - CARD_H_EST - 12);
|
|
55
|
+
return { top, left: Math.max(12, rect.left - CARD_W - 16), width: CARD_W };
|
|
56
|
+
}
|
|
37
57
|
const roomBelow = vh - rect.bottom;
|
|
38
58
|
const top =
|
|
39
59
|
roomBelow > CARD_H_EST + 24 ? rect.bottom + 12 : Math.max(12, rect.top - CARD_H_EST - 12);
|
|
@@ -41,29 +61,84 @@ function computeCardStyle(rect) {
|
|
|
41
61
|
return { top, left, width: CARD_W };
|
|
42
62
|
}
|
|
43
63
|
|
|
44
|
-
export function TourOverlay({ steps, open, onClose, onComplete }) {
|
|
64
|
+
export function TourOverlay({ steps, open, onClose, onComplete, bus, hasSelection, hasCanvas }) {
|
|
45
65
|
const [i, setI] = useState(0);
|
|
46
66
|
const [rect, setRect] = useState(null);
|
|
47
67
|
const cardRef = useRef(null);
|
|
48
68
|
const prevFocus = useRef(null);
|
|
69
|
+
const pollRef = useRef(null);
|
|
70
|
+
// The bus is rebuilt by the parent on every render; ref it so its identity
|
|
71
|
+
// churn never re-triggers the per-step setup effect (which would thrash).
|
|
72
|
+
const busRef = useRef(bus);
|
|
73
|
+
busRef.current = bus;
|
|
74
|
+
|
|
75
|
+
const step = steps[i] || null;
|
|
49
76
|
|
|
50
77
|
const measure = useCallback(() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
78
|
+
if (!step) {
|
|
79
|
+
setRect(null);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const r = getRect(step.target);
|
|
83
|
+
setRect(r);
|
|
84
|
+
return r;
|
|
85
|
+
}, [step]);
|
|
54
86
|
|
|
55
87
|
// Reset to the first step whenever a tour (re)opens.
|
|
56
88
|
useEffect(() => {
|
|
57
89
|
if (open) setI(0);
|
|
58
90
|
}, [open]);
|
|
59
91
|
|
|
92
|
+
// Per-step: run the step's setup (open canvas / inspector / switch tab), then
|
|
93
|
+
// poll for the target to render before settling on a measurement. React state
|
|
94
|
+
// updates + the canvas-shell round-trip are async, so a single synchronous
|
|
95
|
+
// querySelector almost always misses on the first frame.
|
|
96
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: re-run only on step/selection change; bus + measure reached via ref/stable closures on purpose.
|
|
60
97
|
useLayoutEffect(() => {
|
|
61
|
-
if (open)
|
|
62
|
-
|
|
98
|
+
if (!open || !step) return undefined;
|
|
99
|
+
let cancelled = false;
|
|
100
|
+
setRect(null);
|
|
101
|
+
|
|
102
|
+
// Establish the UI state this step wants to highlight.
|
|
103
|
+
try {
|
|
104
|
+
busRef.current?.setup?.(step);
|
|
105
|
+
} catch {}
|
|
106
|
+
|
|
107
|
+
let tries = 0;
|
|
108
|
+
const MAX = 24; // ~3s at 130ms
|
|
109
|
+
const tick = () => {
|
|
110
|
+
if (cancelled) return;
|
|
111
|
+
const r = measure();
|
|
112
|
+
const settled = r || tries >= MAX;
|
|
113
|
+
if (settled) {
|
|
114
|
+
if (r) {
|
|
115
|
+
try {
|
|
116
|
+
document.querySelector(step.target)?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
117
|
+
} catch {}
|
|
118
|
+
// Re-measure after the scroll so the spotlight is glued post-layout.
|
|
119
|
+
requestAnimationFrame(() => {
|
|
120
|
+
if (!cancelled) measure();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
tries += 1;
|
|
126
|
+
pollRef.current = setTimeout(tick, 130);
|
|
127
|
+
};
|
|
128
|
+
tick();
|
|
129
|
+
|
|
130
|
+
return () => {
|
|
131
|
+
cancelled = true;
|
|
132
|
+
if (pollRef.current) clearTimeout(pollRef.current);
|
|
133
|
+
};
|
|
134
|
+
// Re-run when the step changes OR a selection arrives (a requireSelection
|
|
135
|
+
// target pops into existence the moment the user ⌘-clicks). `bus`/`measure`
|
|
136
|
+
// are reached through refs/stable closures to avoid identity-churn re-runs.
|
|
137
|
+
}, [open, i, hasSelection]);
|
|
63
138
|
|
|
64
139
|
// Keep the spotlight glued to its target through scroll/resize; restore focus.
|
|
65
140
|
useEffect(() => {
|
|
66
|
-
if (!open) return;
|
|
141
|
+
if (!open) return undefined;
|
|
67
142
|
prevFocus.current = document.activeElement;
|
|
68
143
|
const onMove = () => measure();
|
|
69
144
|
window.addEventListener('resize', onMove);
|
|
@@ -90,7 +165,7 @@ export function TourOverlay({ steps, open, onClose, onComplete }) {
|
|
|
90
165
|
|
|
91
166
|
// Keyboard: Esc skips, ←/→ navigate, Tab is trapped within the card buttons.
|
|
92
167
|
useEffect(() => {
|
|
93
|
-
if (!open) return;
|
|
168
|
+
if (!open) return undefined;
|
|
94
169
|
function onKey(e) {
|
|
95
170
|
if (e.key === 'Escape') {
|
|
96
171
|
e.preventDefault();
|
|
@@ -123,7 +198,7 @@ export function TourOverlay({ steps, open, onClose, onComplete }) {
|
|
|
123
198
|
|
|
124
199
|
// Move focus to the primary action on each step (accessible dialog behavior).
|
|
125
200
|
useEffect(() => {
|
|
126
|
-
if (!open) return;
|
|
201
|
+
if (!open) return undefined;
|
|
127
202
|
const t = setTimeout(() => {
|
|
128
203
|
const primary = cardRef.current?.querySelector('[data-tour-primary]');
|
|
129
204
|
try {
|
|
@@ -133,8 +208,7 @@ export function TourOverlay({ steps, open, onClose, onComplete }) {
|
|
|
133
208
|
return () => clearTimeout(t);
|
|
134
209
|
}, [open, i]);
|
|
135
210
|
|
|
136
|
-
if (!open || !steps.length) return null;
|
|
137
|
-
const step = steps[i];
|
|
211
|
+
if (!open || !steps.length || !step) return null;
|
|
138
212
|
const pad = 6;
|
|
139
213
|
const spot = rect
|
|
140
214
|
? {
|
|
@@ -145,6 +219,12 @@ export function TourOverlay({ steps, open, onClose, onComplete }) {
|
|
|
145
219
|
}
|
|
146
220
|
: null;
|
|
147
221
|
|
|
222
|
+
// A requireSelection step whose target hasn't materialized yet: tell the user
|
|
223
|
+
// the one action only they can take (the canvas iframe is cross-origin, so the
|
|
224
|
+
// tour can't ⌘-click for them — DDR-054).
|
|
225
|
+
const needSelHint = !!step.requireSelection && !hasSelection;
|
|
226
|
+
const needCanvasHint = (!!step.canvas || !!step.requireSelection) && hasCanvas === false;
|
|
227
|
+
|
|
148
228
|
return (
|
|
149
229
|
<div className="mdcc-tour" role="presentation">
|
|
150
230
|
{spot ? (
|
|
@@ -170,6 +250,14 @@ export function TourOverlay({ steps, open, onClose, onComplete }) {
|
|
|
170
250
|
<div className="mdcc-tour__body" id="mdcc-tour-body">
|
|
171
251
|
{step.body}
|
|
172
252
|
</div>
|
|
253
|
+
{(needCanvasHint || needSelHint) && (
|
|
254
|
+
<div className="mdcc-tour__hint" role="status" aria-live="polite">
|
|
255
|
+
<span className="mdcc-tour__hint-dot" aria-hidden="true" />
|
|
256
|
+
{needCanvasHint
|
|
257
|
+
? 'Open any canvas from the sidebar to follow along.'
|
|
258
|
+
: 'Hold ⌘ and click an element in the canvas — the panel fills in live.'}
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
173
261
|
<div className="mdcc-tour__actions">
|
|
174
262
|
<button type="button" className="mdcc-tour__skip" onClick={onClose}>
|
|
175
263
|
Skip
|
|
@@ -1,35 +1,48 @@
|
|
|
1
1
|
// usage-tour.js — the evergreen "how the Maude UI works" walkthrough.
|
|
2
2
|
//
|
|
3
|
-
// Targets stable
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// Targets stable [data-tour] anchors (redesign-proof — styling classes get
|
|
4
|
+
// renamed, these don't) on elements that exist at first load: the sidebar, the
|
|
5
|
+
// viewport, the menus, the Inspector, the What's New badge, and Help. The
|
|
6
|
+
// Inspector step carries `inspector: true` so the engine opens the panel before
|
|
7
|
+
// spotlighting it. Steps whose target is missing are shown centered by the
|
|
8
|
+
// overlay, so this stays resilient to chrome changes. Reached from Help →
|
|
9
|
+
// "Take the tour" and offered once on first run.
|
|
8
10
|
|
|
9
11
|
export const USAGE_TOUR = [
|
|
10
12
|
{
|
|
11
|
-
target: '
|
|
13
|
+
target: "[data-tour='sidebar']",
|
|
12
14
|
title: 'Your canvases live here',
|
|
13
15
|
body: 'Every mock in the project shows up in this tree. Click one to open it in a tab — or use “+ board” to spin up a blank brief-board, no command needed.',
|
|
16
|
+
placement: 'right',
|
|
14
17
|
},
|
|
15
18
|
{
|
|
16
|
-
target: '
|
|
19
|
+
target: "[data-tour='viewport']",
|
|
17
20
|
title: 'The canvas',
|
|
18
21
|
body: 'Open a canvas, then ⌘-hover to preview the element under your cursor and ⌘-click to select it. Right-click for Copy CSS / Fit / Reset.',
|
|
19
22
|
},
|
|
20
23
|
{
|
|
21
|
-
target: '
|
|
24
|
+
target: "[data-tour='menus']",
|
|
22
25
|
title: 'Menus & tools',
|
|
23
|
-
body: 'View toggles panels (tree, comments,
|
|
26
|
+
body: 'View toggles panels (tree, comments, Inspector); Selection and Tools act on the active canvas. Press ? for every shortcut.',
|
|
27
|
+
placement: 'bottom',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
target: "[data-tour='inspector']",
|
|
31
|
+
inspector: true,
|
|
32
|
+
title: 'The Inspector',
|
|
33
|
+
body: 'Opened from View → Inspector (or press I). Inspect · Layers · CSS — ⌘-click any element and edit its CSS against your design tokens, right here. No AI round-trip.',
|
|
34
|
+
placement: 'left',
|
|
24
35
|
},
|
|
25
36
|
{
|
|
26
37
|
target: "[data-tour='whatsnew']",
|
|
27
38
|
title: 'What’s new',
|
|
28
|
-
body: 'New features land here. Click the ✦ badge any time to catch up on what shipped.',
|
|
39
|
+
body: 'New features land here. Click the ✦ badge any time to catch up on what shipped — some entries come with a guided tour like this one.',
|
|
40
|
+
placement: 'bottom',
|
|
29
41
|
},
|
|
30
42
|
{
|
|
31
43
|
target: "[data-tour='help']",
|
|
32
44
|
title: 'Help is one key away',
|
|
33
45
|
body: 'Press ? for the full keyboard cheat-sheet — and you can restart this tour from the Help menu whenever you like.',
|
|
46
|
+
placement: 'bottom',
|
|
34
47
|
},
|
|
35
48
|
];
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import type { Stroke } from '../annotations-layer.tsx';
|
|
23
|
-
import type { CommandRecord,
|
|
23
|
+
import type { CommandRecord, EditCommand } from '../undo-stack.ts';
|
|
24
24
|
import { registerCommand } from '../undo-stack.ts';
|
|
25
25
|
|
|
26
26
|
/**
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* stale layout.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import type { CommandRecord,
|
|
20
|
+
import type { CommandRecord, EditCommand } from '../undo-stack.ts';
|
|
21
21
|
import { registerCommand } from '../undo-stack.ts';
|
|
22
22
|
|
|
23
23
|
export interface ArtboardLayoutEntry {
|
|
@@ -161,7 +161,7 @@ function screenRectFor(
|
|
|
161
161
|
} catch {
|
|
162
162
|
return null;
|
|
163
163
|
}
|
|
164
|
-
if (!el
|
|
164
|
+
if (!el?.isConnected) return null;
|
|
165
165
|
const r = el.getBoundingClientRect();
|
|
166
166
|
if (r.width === 0 && r.height === 0) return null;
|
|
167
167
|
return { x: r.left, y: r.top, w: r.width, h: r.height };
|
|
@@ -204,7 +204,7 @@ export function CommentsOverlay(): React.ReactNode {
|
|
|
204
204
|
const mirrorSelection = useCallback(
|
|
205
205
|
(comment: OverlayComment | undefined) => {
|
|
206
206
|
if (!selSet) return;
|
|
207
|
-
if (!comment
|
|
207
|
+
if (!comment?.selector) {
|
|
208
208
|
selSet.clear();
|
|
209
209
|
return;
|
|
210
210
|
}
|
|
@@ -298,7 +298,7 @@ export function CommentsOverlay(): React.ReactNode {
|
|
|
298
298
|
const detail = (
|
|
299
299
|
e as CustomEvent<{ selection?: ComposeSelection; clientX?: number; clientY?: number }>
|
|
300
300
|
).detail;
|
|
301
|
-
if (!detail
|
|
301
|
+
if (!detail?.selection) return;
|
|
302
302
|
setComposer({
|
|
303
303
|
selection: detail.selection,
|
|
304
304
|
clientX: typeof detail.clientX === 'number' ? detail.clientX : 0,
|
|
@@ -383,7 +383,9 @@ export function CommentsOverlay(): React.ReactNode {
|
|
|
383
383
|
const indexById = useMemo(() => {
|
|
384
384
|
const m = new Map<string, number>();
|
|
385
385
|
const all = comments.slice().sort((a, b) => a.created.localeCompare(b.created));
|
|
386
|
-
all.forEach((c, i) =>
|
|
386
|
+
all.forEach((c, i) => {
|
|
387
|
+
m.set(c.id, i + 1);
|
|
388
|
+
});
|
|
387
389
|
return m;
|
|
388
390
|
}, [comments]);
|
|
389
391
|
|
|
@@ -784,7 +786,7 @@ function CommentPin({
|
|
|
784
786
|
return () => {
|
|
785
787
|
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
|
|
786
788
|
};
|
|
787
|
-
}, [comment.selector, comment.bounds]);
|
|
789
|
+
}, [comment.selector, comment.bounds, comment.index]);
|
|
788
790
|
|
|
789
791
|
const author = comment.author?.trim() || 'unknown';
|
|
790
792
|
const label = `Comment ${sequence} by ${author}`;
|
|
@@ -237,7 +237,9 @@ const MENU_CSS = `
|
|
|
237
237
|
.dc-context-menu .dc-menu-flyout.is-flip { left: auto; right: calc(100% + 3px); }
|
|
238
238
|
`.trim();
|
|
239
239
|
|
|
240
|
-
|
|
240
|
+
// Exported (FigJam v3) — the annotation layer's right-click menu reuses the
|
|
241
|
+
// same .dc-context-menu visual language without duplicating the stylesheet.
|
|
242
|
+
export function ensureMenuStyles(): void {
|
|
241
243
|
if (typeof document === 'undefined') return;
|
|
242
244
|
if (document.getElementById('dc-context-menu-css')) return;
|
|
243
245
|
const s = document.createElement('style');
|
|
@@ -342,7 +344,7 @@ function ContextMenuView({
|
|
|
342
344
|
el.querySelectorAll<HTMLButtonElement>('button.dc-menu-item:not([disabled])')
|
|
343
345
|
);
|
|
344
346
|
if (items.length === 0) return;
|
|
345
|
-
const idx = items.
|
|
347
|
+
const idx = items.indexOf(document.activeElement);
|
|
346
348
|
const nextIdx =
|
|
347
349
|
e.key === 'ArrowDown'
|
|
348
350
|
? (idx + 1) % items.length
|
|
@@ -368,7 +370,6 @@ function ContextMenuView({
|
|
|
368
370
|
{sections.map((section, si) => {
|
|
369
371
|
const sectionKey = section.map((i) => i.id).join('|') || `s${si}`;
|
|
370
372
|
return (
|
|
371
|
-
// biome-ignore lint/a11y/useSemanticElements: ARIA group within role="menu"; no native equivalent.
|
|
372
373
|
<div key={sectionKey} role="group">
|
|
373
374
|
{si > 0 ? <div className="dc-menu-sep" aria-hidden="true" /> : null}
|
|
374
375
|
{section.map((item) => (
|
|
@@ -207,6 +207,20 @@ export function ContextualToolbar() {
|
|
|
207
207
|
return (
|
|
208
208
|
<div ref={ref} className="dc-elem-ctx-tb" role="toolbar" aria-label="Element actions">
|
|
209
209
|
<span className="dc-elem-ctx-count">{count === 1 ? '1 element' : `${count} elements`}</span>
|
|
210
|
+
<button
|
|
211
|
+
type="button"
|
|
212
|
+
title="Inspect — open the right panel (Inspect / Layers / CSS)"
|
|
213
|
+
onClick={() => {
|
|
214
|
+
// Phase 12 — open the shell's Inspector on the current selection.
|
|
215
|
+
try {
|
|
216
|
+
window.parent.postMessage({ dgn: 'open-inspector' }, '*');
|
|
217
|
+
} catch {
|
|
218
|
+
/* detached / cross-origin */
|
|
219
|
+
}
|
|
220
|
+
}}
|
|
221
|
+
>
|
|
222
|
+
Inspect
|
|
223
|
+
</button>
|
|
210
224
|
<button
|
|
211
225
|
type="button"
|
|
212
226
|
title="Copy CSS selector"
|
|
@@ -43,24 +43,28 @@ const CURSOR_CSS = `
|
|
|
43
43
|
}
|
|
44
44
|
.dc-cursor-arrow {
|
|
45
45
|
display: block;
|
|
46
|
-
filter: drop-shadow(0 1px
|
|
46
|
+
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.4));
|
|
47
47
|
}
|
|
48
|
+
/* DS colors-presence .cur-label recipe — hue pill, mono 10px, dark
|
|
49
|
+
* accent-fg text (never white-on-hue, which washed out on light hues).
|
|
50
|
+
* NOTE: keep this comment backtick-free — it lives inside the CURSOR_CSS
|
|
51
|
+
* template literal and a stray backtick closes it. */
|
|
48
52
|
.dc-cursor-label {
|
|
49
53
|
position: absolute;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
/* specimen geometry: the pill hangs below-right of the 15px triangle */
|
|
55
|
+
top: 14px;
|
|
56
|
+
left: 10px;
|
|
57
|
+
padding: 1px 8px;
|
|
58
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
54
59
|
font-weight: 500;
|
|
55
|
-
font-size:
|
|
56
|
-
line-height: 1.
|
|
57
|
-
color: var(--maude-hud-accent-fg,
|
|
58
|
-
border-radius:
|
|
60
|
+
font-size: 10px;
|
|
61
|
+
line-height: 1.4;
|
|
62
|
+
color: var(--maude-hud-accent-fg, oklch(0.180 0.030 268));
|
|
63
|
+
border-radius: 999px;
|
|
59
64
|
white-space: nowrap;
|
|
60
65
|
max-width: 180px;
|
|
61
66
|
overflow: hidden;
|
|
62
67
|
text-overflow: ellipsis;
|
|
63
|
-
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
64
68
|
}
|
|
65
69
|
.dc-peer-selection {
|
|
66
70
|
position: absolute;
|
|
@@ -117,20 +121,16 @@ const Cursor = memo(function Cursor({ peer, viewport }: CursorProps): JSX.Elemen
|
|
|
117
121
|
const screenY = peer.cursor.y * viewport.zoom + viewport.y;
|
|
118
122
|
return (
|
|
119
123
|
<div className="dc-cursor" style={{ transform: `translate(${screenX}px, ${screenY}px)` }}>
|
|
124
|
+
{/* DS colors-presence Pointer — one plain triangle glyph, tinted by its
|
|
125
|
+
owner (the specimen's exact 24-grid path, no tail/notch). */}
|
|
120
126
|
<svg
|
|
121
127
|
className="dc-cursor-arrow"
|
|
122
|
-
width="
|
|
123
|
-
height="
|
|
124
|
-
viewBox="0 0
|
|
128
|
+
width="15"
|
|
129
|
+
height="15"
|
|
130
|
+
viewBox="0 0 24 24"
|
|
125
131
|
aria-hidden="true"
|
|
126
132
|
>
|
|
127
|
-
<path
|
|
128
|
-
d="M 2 2 L 2 18 L 6 14 L 9 21 L 12 20 L 9 13 L 14 13 Z"
|
|
129
|
-
fill={peer.color}
|
|
130
|
-
stroke="#fff"
|
|
131
|
-
strokeWidth="1.2"
|
|
132
|
-
strokeLinejoin="round"
|
|
133
|
-
/>
|
|
133
|
+
<path d="M4 3 L20 11.5 L12.5 13 L10 21 Z" fill={peer.color} />
|
|
134
134
|
</svg>
|
|
135
135
|
<div className="dc-cursor-label" style={{ background: peer.color }}>
|
|
136
136
|
{peer.name}
|