@1agh/maude 0.29.0 → 0.31.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 +6 -6
- package/apps/studio/acp/bridge.ts +285 -0
- package/apps/studio/acp/env.ts +48 -0
- package/apps/studio/acp/index.ts +132 -0
- package/apps/studio/acp/probe.ts +112 -0
- package/apps/studio/acp/transcript.ts +149 -0
- 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 +1714 -1661
- package/apps/studio/annotations-model.ts +2077 -0
- package/apps/studio/annotations-snap.ts +125 -0
- package/apps/studio/api.ts +433 -182
- 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/chat-open.sh +44 -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-lib.tsx +112 -19
- package/apps/studio/canvas-list-watch.ts +177 -0
- package/apps/studio/canvas-shell.tsx +326 -9
- package/apps/studio/client/app.jsx +3579 -250
- package/apps/studio/client/canvas-url.js +5 -0
- package/apps/studio/client/github.js +99 -0
- package/apps/studio/client/index.html +1 -1
- package/apps/studio/client/panels/ChatPanel.jsx +770 -0
- package/apps/studio/client/panels/CollabModelInfographic.jsx +71 -0
- package/apps/studio/client/panels/CreateProject.jsx +334 -0
- package/apps/studio/client/panels/DiffView.jsx +590 -0
- package/apps/studio/client/panels/GitPanel.jsx +767 -0
- package/apps/studio/client/panels/IdentityBar.jsx +294 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +563 -0
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +349 -0
- package/apps/studio/client/panels/acp-runtime.js +286 -0
- package/apps/studio/client/panels/chat-markdown.jsx +138 -0
- package/apps/studio/client/panels/git-grouping.js +86 -0
- package/apps/studio/client/styles/0-reset.css +4 -0
- package/apps/studio/client/styles/3-shell-maude.css +1326 -14
- package/apps/studio/client/styles/4-components.css +39 -0
- package/apps/studio/client/styles/5-maude-overrides.css +41 -2
- package/apps/studio/client/styles/6-acp-chat.css +771 -0
- package/apps/studio/client/styles/_index.css +2 -0
- package/apps/studio/client/tour/collab-tour.js +61 -0
- package/apps/studio/client/tour/overlay.jsx +116 -18
- package/apps/studio/client/tour/usage-tour.js +23 -10
- package/apps/studio/client/whats-new.jsx +25 -10
- package/apps/studio/collab/registry.ts +13 -0
- package/apps/studio/collab/room.ts +36 -0
- 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 +37 -21
- package/apps/studio/dist/client.bundle.js +31795 -1829
- package/apps/studio/dist/comment-mount.js +97 -9
- package/apps/studio/dist/styles.css +7938 -1599
- package/apps/studio/dom-selection.ts +115 -0
- package/apps/studio/export-dialog.tsx +3 -3
- package/apps/studio/git/endpoints.ts +338 -0
- package/apps/studio/git/service.ts +1334 -0
- package/apps/studio/git/watch.ts +97 -0
- package/apps/studio/github/endpoints.ts +358 -0
- package/apps/studio/github/service.ts +231 -0
- package/apps/studio/github/token.ts +53 -0
- package/apps/studio/history.ts +20 -3
- package/apps/studio/hmr-broadcast.ts +9 -2
- package/apps/studio/http.ts +508 -6
- package/apps/studio/input-router.tsx +7 -0
- package/apps/studio/inspect.ts +37 -8
- package/apps/studio/participants-chrome.tsx +70 -16
- package/apps/studio/paths.ts +12 -0
- package/apps/studio/scaffold-design.ts +57 -0
- package/apps/studio/server.ts +70 -4
- package/apps/studio/sync/agent.ts +311 -57
- package/apps/studio/sync/codec.ts +69 -0
- package/apps/studio/sync/cold-start.ts +198 -0
- package/apps/studio/sync/connection-state.ts +58 -2
- package/apps/studio/sync/hub-link.ts +137 -0
- 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/acp-bridge.test.ts +127 -0
- package/apps/studio/test/acp-env.test.ts +65 -0
- package/apps/studio/test/acp-origin-gate.test.ts +78 -0
- package/apps/studio/test/acp-transcript.test.ts +112 -0
- 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-create-api.test.ts +72 -0
- package/apps/studio/test/canvas-edit.test.ts +181 -1
- package/apps/studio/test/canvas-list-watch.test.ts +322 -0
- package/apps/studio/test/canvas-meta-api.test.ts +161 -27
- package/apps/studio/test/canvas-origin-gate.test.ts +43 -0
- package/apps/studio/test/chat-markdown.test.tsx +58 -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/collab-session-survival.test.tsx +176 -0
- package/apps/studio/test/csp-canvas-shell.test.ts +5 -0
- package/apps/studio/test/csrf-write-guard.test.ts +78 -0
- package/apps/studio/test/editing-presence.test.ts +103 -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/fixtures/mock-acp-agent.mjs +45 -0
- package/apps/studio/test/git-api.test.ts +0 -0
- package/apps/studio/test/git-branches.test.ts +106 -0
- package/apps/studio/test/git-grouping.test.ts +106 -0
- package/apps/studio/test/git-watch.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +465 -0
- package/apps/studio/test/hub-link.test.ts +69 -0
- package/apps/studio/test/participants-chrome.test.ts +36 -1
- 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-seed-dedup.test.ts +187 -0
- package/apps/studio/test/sync-cold-start.test.ts +304 -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/tour-overlay.test.tsx +18 -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 +30 -11
- 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-chrome-visibility.tsx +66 -0
- package/apps/studio/use-collab.tsx +434 -202
- package/apps/studio/use-selection-set.tsx +9 -0
- package/apps/studio/use-tool-mode.tsx +5 -1
- package/apps/studio/whats-new.json +208 -10
- package/apps/studio/whats-new.schema.json +18 -2
- package/apps/studio/ws.ts +44 -1
- package/cli/commands/design-link.test.mjs +84 -0
- package/cli/commands/design.mjs +3 -1
- package/cli/commands/design.test.mjs +5 -1
- package/cli/lib/design-link.mjs +51 -1
- package/cli/lib/gitignore-block.mjs +16 -3
- package/cli/lib/gitignore-block.test.mjs +13 -1
- package/package.json +11 -9
- package/plugins/design/dependencies.json +17 -0
- package/plugins/design/templates/_shell.html +58 -12
|
@@ -30,4 +30,6 @@
|
|
|
30
30
|
@import url('./4-components-maude.css');
|
|
31
31
|
/* Plan B — maude redesign of remaining legacy shell surfaces (HelpModal, tour, what's-new, SystemView, CommentBar). */
|
|
32
32
|
@import url('./5-maude-overrides.css');
|
|
33
|
+
/* Phase 31 (DDR-123) — ACP chat sidepanel (chat-* inside the Assistant st-rpanel). */
|
|
34
|
+
@import url('./6-acp-chat.css');
|
|
33
35
|
@import url('./5-utilities.css');
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// collab-tour.js — the version-control "rychlý kurz" (Phase 29, epic E4).
|
|
2
|
+
//
|
|
3
|
+
// Teaches the two-layer collab model on the EXISTING tour engine (overlay.jsx) —
|
|
4
|
+
// never a fork. Per the teaching-model DDR (this phase REVERSES the "hide the
|
|
5
|
+
// duality" recommendation in collab-model-design.md § Part 2): we surface a visible
|
|
6
|
+
// three-verb action-cycle and teach the live-vs-async split honestly, because the
|
|
7
|
+
// cycle has to match real buttons in the app and pure-hiding is leaky.
|
|
8
|
+
//
|
|
9
|
+
// Step 1 is a centered, target-less step whose graphic is the two-layer infographic
|
|
10
|
+
// (the engine already centers target-less steps; the `render` field is the only
|
|
11
|
+
// addition). Steps 2-4 spotlight the real action controls via stable [data-tour]
|
|
12
|
+
// anchors (save-local / publish / pull) — `changes: true` opens the Changes panel
|
|
13
|
+
// first so the buttons exist. Step 5 points at the live presence layer; step 6 is
|
|
14
|
+
// the one honest hard thing, centered.
|
|
15
|
+
//
|
|
16
|
+
// Vocabulary is the canonical set ONLY (teaching-model table) — no raw git terms:
|
|
17
|
+
// Save changes locally · Publish for everyone · Pull changes · Draft · Shared version.
|
|
18
|
+
|
|
19
|
+
import CollabModelInfographic from '../panels/CollabModelInfographic.jsx';
|
|
20
|
+
|
|
21
|
+
export const COLLAB_TOUR = [
|
|
22
|
+
{
|
|
23
|
+
// Centered, no target — the engine renders target-less steps centered, and the
|
|
24
|
+
// `render` field draws the infographic above the copy.
|
|
25
|
+
render: CollabModelInfographic,
|
|
26
|
+
title: 'Working together, in one picture',
|
|
27
|
+
body: 'There are two layers. Up top, being together just happens. Below, your work moves through three simple steps. Let’s walk them.',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
target: "[data-tour='save-local']",
|
|
31
|
+
changes: true,
|
|
32
|
+
title: 'Save changes locally',
|
|
33
|
+
body: 'When something looks right, save it. That keeps a version on your computer you can always come back to — like a checkpoint, just for you.',
|
|
34
|
+
placement: 'left',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
target: "[data-tour='publish']",
|
|
38
|
+
changes: true,
|
|
39
|
+
title: 'Publish for everyone',
|
|
40
|
+
body: 'Ready to share? Publish sends your saved work to the whole team. Think of it as putting your version on the shared shelf.',
|
|
41
|
+
placement: 'left',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
target: "[data-tour='pull']",
|
|
45
|
+
changes: true,
|
|
46
|
+
title: 'Pull changes',
|
|
47
|
+
body: 'When teammates publish, Pull brings their work onto your computer so you’re both looking at the same thing.',
|
|
48
|
+
placement: 'left',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
target: "[data-tour='status']",
|
|
52
|
+
title: 'Being together is automatic',
|
|
53
|
+
body: 'Cursors, who’s here, and comments need no buttons at all. When you’re both in a canvas, you see each other live — that’s the top layer, always on.',
|
|
54
|
+
placement: 'bottom',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
// Centered — the one honest hard thing.
|
|
58
|
+
title: 'The one thing worth knowing',
|
|
59
|
+
body: 'When you’re here together, publishing already covers your teammate — they’ve seen it live, so Publish is just dropping a bookmark. Only when you work apart can two versions drift. If that happens, Maude shows you both and lets you pick — keep mine, keep theirs, or keep both. Never a confusing merge.',
|
|
60
|
+
},
|
|
61
|
+
];
|
|
@@ -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,17 @@ 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
|
+
|
|
228
|
+
// Optional per-step graphic (Phase 29 / E4): a component drawn above the copy —
|
|
229
|
+
// the collab tour uses it for the centered two-layer infographic step. Widens the
|
|
230
|
+
// card so the diagram has room.
|
|
231
|
+
const Graphic = step.render || null;
|
|
232
|
+
|
|
148
233
|
return (
|
|
149
234
|
<div className="mdcc-tour" role="presentation">
|
|
150
235
|
{spot ? (
|
|
@@ -153,7 +238,7 @@ export function TourOverlay({ steps, open, onClose, onComplete }) {
|
|
|
153
238
|
<div className="mdcc-tour__scrim" aria-hidden="true" />
|
|
154
239
|
)}
|
|
155
240
|
<div
|
|
156
|
-
className=
|
|
241
|
+
className={'mdcc-tour__card' + (Graphic ? ' mdcc-tour__card--graphic' : '')}
|
|
157
242
|
ref={cardRef}
|
|
158
243
|
role="dialog"
|
|
159
244
|
aria-modal="true"
|
|
@@ -164,12 +249,25 @@ export function TourOverlay({ steps, open, onClose, onComplete }) {
|
|
|
164
249
|
<div className="mdcc-tour__step">
|
|
165
250
|
{i + 1} / {steps.length}
|
|
166
251
|
</div>
|
|
252
|
+
{Graphic && (
|
|
253
|
+
<div className="mdcc-tour__graphic">
|
|
254
|
+
<Graphic />
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
167
257
|
<div className="mdcc-tour__title" id="mdcc-tour-title">
|
|
168
258
|
{step.title}
|
|
169
259
|
</div>
|
|
170
260
|
<div className="mdcc-tour__body" id="mdcc-tour-body">
|
|
171
261
|
{step.body}
|
|
172
262
|
</div>
|
|
263
|
+
{(needCanvasHint || needSelHint) && (
|
|
264
|
+
<div className="mdcc-tour__hint" role="status" aria-live="polite">
|
|
265
|
+
<span className="mdcc-tour__hint-dot" aria-hidden="true" />
|
|
266
|
+
{needCanvasHint
|
|
267
|
+
? 'Open any canvas from the sidebar to follow along.'
|
|
268
|
+
: 'Hold ⌘ and click an element in the canvas — the panel fills in live.'}
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
173
271
|
<div className="mdcc-tour__actions">
|
|
174
272
|
<button type="button" className="mdcc-tour__skip" onClick={onClose}>
|
|
175
273
|
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
|
];
|
|
@@ -48,20 +48,35 @@ export function useWhatsNew(currentVersion) {
|
|
|
48
48
|
const [toastDismissed, setToastDismissed] = useState(() => readToastDismissed());
|
|
49
49
|
const [panelOpen, setPanelOpen] = useState(false);
|
|
50
50
|
|
|
51
|
+
// Fetch the feed on mount AND whenever the window regains focus (Phase 32 /
|
|
52
|
+
// Task 5). The native shell can sit backgrounded for a long time; an auto-update
|
|
53
|
+
// (or a deploy of the web studio) bumps the feed while we're not looking, so we
|
|
54
|
+
// re-evaluate on focus to surface the ✦ badge without a full reload.
|
|
51
55
|
useEffect(() => {
|
|
52
56
|
let cancelled = false;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
const refresh = () => {
|
|
58
|
+
fetch('/_api/whats-new')
|
|
59
|
+
.then((r) => r.json())
|
|
60
|
+
.then((j) => {
|
|
61
|
+
if (cancelled) return;
|
|
62
|
+
setEntries(Array.isArray(j?.entries) ? j.entries : []);
|
|
63
|
+
if (typeof j?.version === 'string') setFeedVersion(j.version);
|
|
64
|
+
})
|
|
65
|
+
.catch(() => {
|
|
66
|
+
/* offline / restart — no banner */
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
refresh();
|
|
70
|
+
const onFocus = () => refresh();
|
|
71
|
+
const onVisible = () => {
|
|
72
|
+
if (document.visibilityState === 'visible') refresh();
|
|
73
|
+
};
|
|
74
|
+
window.addEventListener('focus', onFocus);
|
|
75
|
+
document.addEventListener('visibilitychange', onVisible);
|
|
63
76
|
return () => {
|
|
64
77
|
cancelled = true;
|
|
78
|
+
window.removeEventListener('focus', onFocus);
|
|
79
|
+
document.removeEventListener('visibilitychange', onVisible);
|
|
65
80
|
};
|
|
66
81
|
}, []);
|
|
67
82
|
|
|
@@ -58,6 +58,14 @@ export interface Registry {
|
|
|
58
58
|
* updated stroke set without waiting for a cold-open re-seed.
|
|
59
59
|
*/
|
|
60
60
|
syncRoomFromAnnotations(slug: string, svg: string): void;
|
|
61
|
+
/**
|
|
62
|
+
* Phase 30 — project agent editing-presence onto a slug's room awareness so
|
|
63
|
+
* it crosses the hub (the loopback `ai-activity` bus event does not). `null`
|
|
64
|
+
* clears it. No-op when no room is live for the slug — a peer that joins
|
|
65
|
+
* later will see the agent on the next ai-activity heartbeat. Soft heads-up;
|
|
66
|
+
* never a lock.
|
|
67
|
+
*/
|
|
68
|
+
setAgentEditing(slug: string, state: { name: string; since: number } | null): void;
|
|
61
69
|
/**
|
|
62
70
|
* Phase 9 Task 5 — attach the hub-side Awareness (from a sync provider) for
|
|
63
71
|
* a slug so the Room's Awareness (browser peers) is bridged bidirectionally
|
|
@@ -192,6 +200,10 @@ export function createRegistry(callbacks: RoomCallbacks): Registry {
|
|
|
192
200
|
}, 'inspector-write');
|
|
193
201
|
}
|
|
194
202
|
|
|
203
|
+
function setAgentEditing(slug: string, state: { name: string; since: number } | null): void {
|
|
204
|
+
rooms.get(slug)?.setAgentEditing(state);
|
|
205
|
+
}
|
|
206
|
+
|
|
195
207
|
async function flushAll(): Promise<void> {
|
|
196
208
|
await Promise.all(Array.from(rooms.values(), (r) => r.flush()));
|
|
197
209
|
}
|
|
@@ -227,6 +239,7 @@ export function createRegistry(callbacks: RoomCallbacks): Registry {
|
|
|
227
239
|
getDoc,
|
|
228
240
|
syncRoomFromComments,
|
|
229
241
|
syncRoomFromAnnotations,
|
|
242
|
+
setAgentEditing,
|
|
230
243
|
attachHubAwareness,
|
|
231
244
|
pin,
|
|
232
245
|
unpin,
|
|
@@ -57,6 +57,15 @@ export interface Room {
|
|
|
57
57
|
connect(conn: RoomConn): Promise<void>;
|
|
58
58
|
disconnect(conn: RoomConn): void;
|
|
59
59
|
receive(conn: RoomConn, payload: Uint8Array): void;
|
|
60
|
+
/**
|
|
61
|
+
* Phase 30 — project an agent's "editing" activity onto this room's awareness
|
|
62
|
+
* using the room's own (otherwise-unused) local awareness slot, so the soft
|
|
63
|
+
* editing-presence crosses the hub to remote peers (the loopback `ai-activity`
|
|
64
|
+
* bus event does not). `null` clears it. Idempotent — a re-set of the same
|
|
65
|
+
* `{name, since}` is a no-op (ai-activity heartbeats re-emit every 10 s).
|
|
66
|
+
* NOT a lock; never blocks a peer.
|
|
67
|
+
*/
|
|
68
|
+
setAgentEditing(state: { name: string; since: number } | null): void;
|
|
60
69
|
/** Force the debounced flush to fire now. DDR-051 §3 (branch-switch). */
|
|
61
70
|
flush(): Promise<void>;
|
|
62
71
|
/** Tear down — clears timers + removes awareness. */
|
|
@@ -192,6 +201,32 @@ export function createRoom(slug: string, callbacks: RoomCallbacks): Room {
|
|
|
192
201
|
if (reply) conn.send(reply);
|
|
193
202
|
}
|
|
194
203
|
|
|
204
|
+
// Phase 30 — agent editing-presence projected onto the room's own awareness
|
|
205
|
+
// slot. The key short-circuits redundant re-sets (ai-activity heartbeats
|
|
206
|
+
// re-emit the same entry every 10 s). The awareness `update` handler above
|
|
207
|
+
// broadcasts the change to local conns AND relays it to the hub via
|
|
208
|
+
// bridgeAwareness, so a remote peer's overlay attributes the edit.
|
|
209
|
+
let agentEditingKey: string | null = null;
|
|
210
|
+
function setAgentEditing(state: { name: string; since: number } | null): void {
|
|
211
|
+
if (destroyed) return;
|
|
212
|
+
const key = state ? `${state.name}${state.since}` : null;
|
|
213
|
+
if (key === agentEditingKey) return;
|
|
214
|
+
agentEditingKey = key;
|
|
215
|
+
if (state === null) {
|
|
216
|
+
awareness.setLocalState(null);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
awareness.setLocalState({
|
|
220
|
+
name: state.name,
|
|
221
|
+
color: '', // re-derived client-side from name; wire value is discarded
|
|
222
|
+
cursor: null,
|
|
223
|
+
selection: null,
|
|
224
|
+
annotationSelection: [],
|
|
225
|
+
viewport: { x: 0, y: 0, zoom: 1 },
|
|
226
|
+
editing: { since: state.since },
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
195
230
|
async function destroy(): Promise<void> {
|
|
196
231
|
destroyed = true;
|
|
197
232
|
if (flushTimer) {
|
|
@@ -211,6 +246,7 @@ export function createRoom(slug: string, callbacks: RoomCallbacks): Room {
|
|
|
211
246
|
connect,
|
|
212
247
|
disconnect,
|
|
213
248
|
receive,
|
|
249
|
+
setAgentEditing,
|
|
214
250
|
flush,
|
|
215
251
|
destroy,
|
|
216
252
|
size: () => conns.size,
|
|
@@ -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"
|