@1agh/maude 0.22.2 → 0.24.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 +34 -1
- package/cli/bin/maude.mjs +3 -0
- package/cli/commands/cache.mjs +181 -0
- package/cli/commands/cache.test.mjs +166 -0
- package/cli/commands/design-link.test.mjs +84 -2
- package/cli/commands/design.mjs +95 -4
- package/cli/commands/design.test.mjs +56 -0
- package/cli/commands/help.mjs +34 -0
- package/cli/commands/hub.mjs +255 -30
- package/cli/commands/hub.test.mjs +135 -10
- package/cli/commands/init.mjs +3 -0
- package/cli/commands/preflight.mjs +11 -0
- package/cli/commands/preflight.test.mjs +46 -0
- package/cli/commands/scenario-report.mjs +45 -0
- package/cli/lib/cache.mjs +407 -0
- package/cli/lib/cache.test.mjs +303 -0
- package/cli/lib/design-link.mjs +222 -11
- package/cli/lib/flow-design-integration.test.mjs +165 -0
- package/cli/lib/gitignore-block.mjs +90 -0
- package/cli/lib/gitignore-block.test.mjs +123 -0
- package/cli/lib/hubs-config.mjs +42 -4
- package/cli/lib/plugin-cli-reachability.test.mjs +56 -0
- package/cli/lib/preflight.mjs +41 -10
- package/package.json +8 -8
- package/plugins/design/dev-server/ai-banner.tsx +2 -2
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +349 -112
- package/plugins/design/dev-server/annotations-layer.tsx +906 -137
- package/plugins/design/dev-server/api.ts +109 -4
- package/plugins/design/dev-server/bin/preflight.sh +21 -10
- package/plugins/design/dev-server/bin/prep.sh +211 -0
- package/plugins/design/dev-server/bin/scenario-report.mjs +209 -0
- package/plugins/design/dev-server/bin/screenshot.sh +18 -1
- package/plugins/design/dev-server/bin/smoke.sh +94 -11
- package/plugins/design/dev-server/build.ts +69 -3
- package/plugins/design/dev-server/canvas-comment-mount.tsx +384 -0
- package/plugins/design/dev-server/canvas-cursors.ts +125 -0
- package/plugins/design/dev-server/canvas-icons.tsx +130 -0
- package/plugins/design/dev-server/canvas-lib.tsx +47 -27
- package/plugins/design/dev-server/canvas-meta.schema.json +20 -0
- package/plugins/design/dev-server/canvas-shell.tsx +358 -245
- package/plugins/design/dev-server/client/app.jsx +191 -21
- package/plugins/design/dev-server/client/styles/3-shell.css +10 -0
- package/plugins/design/dev-server/collab/awareness-bridge.ts +77 -0
- package/plugins/design/dev-server/collab/index.ts +87 -9
- package/plugins/design/dev-server/collab/persistence.ts +34 -3
- package/plugins/design/dev-server/collab/registry.ts +131 -2
- package/plugins/design/dev-server/collab/room.ts +21 -8
- package/plugins/design/dev-server/config.schema.json +20 -0
- package/plugins/design/dev-server/context-menu.tsx +167 -23
- package/plugins/design/dev-server/context.ts +31 -0
- package/plugins/design/dev-server/contextual-toolbar.tsx +7 -7
- package/plugins/design/dev-server/dist/client.bundle.js +144 -22
- package/plugins/design/dev-server/dist/comment-mount.js +1868 -0
- package/plugins/design/dev-server/dist/styles.css +16 -0
- package/plugins/design/dev-server/dom-selection.ts +156 -0
- package/plugins/design/dev-server/equal-spacing-handles.tsx +1 -1
- package/plugins/design/dev-server/export-dialog.tsx +19 -17
- package/plugins/design/dev-server/fs-watch.ts +1 -0
- package/plugins/design/dev-server/hmr-broadcast.ts +51 -20
- package/plugins/design/dev-server/http.ts +260 -20
- package/plugins/design/dev-server/input-router.tsx +125 -64
- package/plugins/design/dev-server/participants-chrome.tsx +10 -10
- package/plugins/design/dev-server/server.ts +141 -1
- package/plugins/design/dev-server/sync/agent.ts +418 -0
- package/plugins/design/dev-server/sync/atomic-write.ts +103 -0
- package/plugins/design/dev-server/sync/codec.ts +324 -0
- package/plugins/design/dev-server/sync/connection-state.ts +203 -0
- package/plugins/design/dev-server/sync/echo-guard.ts +108 -0
- package/plugins/design/dev-server/sync/fs-mirror.ts +160 -0
- package/plugins/design/dev-server/sync/hubs-config.ts +87 -0
- package/plugins/design/dev-server/sync/index.ts +918 -0
- package/plugins/design/dev-server/sync/materialize.ts +62 -0
- package/plugins/design/dev-server/sync/migrate-seed.ts +163 -0
- package/plugins/design/dev-server/sync/origins.ts +57 -0
- package/plugins/design/dev-server/sync/projection.ts +368 -0
- package/plugins/design/dev-server/sync/status.ts +115 -0
- package/plugins/design/dev-server/sync/untrusted.ts +153 -0
- package/plugins/design/dev-server/test/_helpers.ts +6 -2
- package/plugins/design/dev-server/test/annotations-layer.test.ts +231 -0
- package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +276 -0
- package/plugins/design/dev-server/test/canvas-cursors.test.ts +73 -0
- package/plugins/design/dev-server/test/canvas-origin-gate.test.ts +76 -0
- package/plugins/design/dev-server/test/collab-awareness-bridge.test.ts +223 -0
- package/plugins/design/dev-server/test/collab-reseed-guard.test.ts +78 -0
- package/plugins/design/dev-server/test/collab-room.test.ts +21 -10
- package/plugins/design/dev-server/test/comment-mount.test.ts +87 -0
- package/plugins/design/dev-server/test/csp-canvas-shell.test.ts +46 -0
- package/plugins/design/dev-server/test/fixtures/phase-20-annotations.svg +1 -0
- package/plugins/design/dev-server/test/hmr-classify.test.ts +70 -0
- package/plugins/design/dev-server/test/input-router.test.ts +21 -0
- package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +100 -0
- package/plugins/design/dev-server/test/shared-doc-convergence.test.ts +265 -0
- package/plugins/design/dev-server/test/shared-doc-foundation.test.ts +63 -0
- package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +160 -0
- package/plugins/design/dev-server/test/shared-doc-projection.test.ts +238 -0
- package/plugins/design/dev-server/test/sync-agent.test.ts +278 -0
- package/plugins/design/dev-server/test/sync-atomic-write.test.ts +63 -0
- package/plugins/design/dev-server/test/sync-codec.test.ts +165 -0
- package/plugins/design/dev-server/test/sync-connection-state.test.ts +146 -0
- package/plugins/design/dev-server/test/sync-echo-guard.test.ts +96 -0
- package/plugins/design/dev-server/test/sync-fs-mirror.test.ts +182 -0
- package/plugins/design/dev-server/test/sync-hardening.test.ts +520 -0
- package/plugins/design/dev-server/test/sync-hubs-config.test.ts +130 -0
- package/plugins/design/dev-server/test/sync-meta-codec.test.ts +123 -0
- package/plugins/design/dev-server/test/sync-runtime.test.ts +812 -0
- package/plugins/design/dev-server/test/sync-status.test.ts +80 -0
- package/plugins/design/dev-server/test/sync-untrusted.test.ts +104 -0
- package/plugins/design/dev-server/test/use-collab.test.ts +0 -0
- package/plugins/design/dev-server/test/use-tool-mode.test.tsx +38 -10
- package/plugins/design/dev-server/tool-palette.tsx +18 -16
- package/plugins/design/dev-server/undo-hud.tsx +4 -4
- package/plugins/design/dev-server/undo-stack.ts +20 -4
- package/plugins/design/dev-server/use-annotation-resize.tsx +16 -5
- package/plugins/design/dev-server/use-collab.tsx +157 -13
- package/plugins/design/dev-server/use-selection-set.tsx +12 -0
- package/plugins/design/dev-server/use-tool-mode.tsx +27 -12
- package/plugins/design/dev-server/ws.ts +40 -1
- package/plugins/design/templates/_shell.html +63 -5
- package/plugins/design/templates/canvas.tsx.template +13 -0
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +15 -7
- package/plugins/flow/.claude-plugin/config.schema.json +5 -0
- package/plugins/flow/templates/ai-skeleton/README.md +1 -1
- package/plugins/flow/templates/ai-skeleton/gitignore +10 -0
- package/plugins/flow/templates/ai-skeleton/scenarios/README.md +3 -1
- package/plugins/flow/templates/ai-skeleton/workflows.config.json +2 -1
|
@@ -189,6 +189,18 @@ export function SelectionSetProvider({
|
|
|
189
189
|
return <SelectionSetContext.Provider value={value}>{children}</SelectionSetContext.Provider>;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Mount a `SelectionSetProvider` only when none exists above us. The shell-
|
|
194
|
+
* owned comment mount layer provides one so both the lite comment router and
|
|
195
|
+
* `CanvasShell` share a single selection set. Hook called unconditionally;
|
|
196
|
+
* only the returned tree branches (hook rules).
|
|
197
|
+
*/
|
|
198
|
+
export function MaybeSelectionSetProvider({ children }: { children: ReactNode }) {
|
|
199
|
+
const outer = useContext(SelectionSetContext);
|
|
200
|
+
if (outer) return <>{children}</>;
|
|
201
|
+
return <SelectionSetProvider>{children}</SelectionSetProvider>;
|
|
202
|
+
}
|
|
203
|
+
|
|
192
204
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
193
205
|
// Hooks
|
|
194
206
|
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
useState,
|
|
22
22
|
} from 'react';
|
|
23
23
|
|
|
24
|
+
import { TOOL_CURSORS } from './canvas-cursors.ts';
|
|
24
25
|
import type { Tool } from './input-router.tsx';
|
|
25
26
|
|
|
26
27
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -35,19 +36,21 @@ export interface ToolDescriptor {
|
|
|
35
36
|
cursor: string;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
// Phase 21 — every tool ships a custom 32×32 SVG cursor (canvas-cursors.ts)
|
|
40
|
+
// with a white outline halo so the glyph reads on any background. The native
|
|
41
|
+
// crosshair/text/cell were thin + tiny ("pen almost invisible"); these mirror
|
|
42
|
+
// the tool-palette icons. `move` keeps the system arrow on purpose.
|
|
38
43
|
export const DEFAULT_TOOLS: readonly ToolDescriptor[] = Object.freeze([
|
|
39
|
-
{ id: 'move', label: 'Move', shortcut: 'V', cursor:
|
|
40
|
-
{ id: 'hand', label: 'Hand', shortcut: 'H', cursor:
|
|
41
|
-
{ id: 'comment', label: 'Comment', shortcut: 'C', cursor:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
{ id: '
|
|
47
|
-
{ id: '
|
|
48
|
-
{ id: '
|
|
49
|
-
{ id: 'arrow', label: 'Arrow', shortcut: 'A', cursor: 'crosshair' },
|
|
50
|
-
{ id: 'eraser', label: 'Eraser', shortcut: 'E', cursor: 'cell' },
|
|
44
|
+
{ id: 'move', label: 'Move', shortcut: 'V', cursor: TOOL_CURSORS.move },
|
|
45
|
+
{ id: 'hand', label: 'Hand', shortcut: 'H', cursor: TOOL_CURSORS.hand },
|
|
46
|
+
{ id: 'comment', label: 'Comment', shortcut: 'C', cursor: TOOL_CURSORS.comment },
|
|
47
|
+
{ id: 'pen', label: 'Pen', shortcut: 'B', cursor: TOOL_CURSORS.pen },
|
|
48
|
+
{ id: 'rect', label: 'Rect', shortcut: 'R', cursor: TOOL_CURSORS.rect },
|
|
49
|
+
{ id: 'ellipse', label: 'Ellipse', shortcut: 'O', cursor: TOOL_CURSORS.ellipse },
|
|
50
|
+
{ id: 'sticky', label: 'Sticky', shortcut: 'N', cursor: TOOL_CURSORS.sticky },
|
|
51
|
+
{ id: 'arrow', label: 'Arrow', shortcut: 'A', cursor: TOOL_CURSORS.arrow },
|
|
52
|
+
{ id: 'text', label: 'Text', shortcut: 'T', cursor: TOOL_CURSORS.text },
|
|
53
|
+
{ id: 'eraser', label: 'Eraser', shortcut: 'E', cursor: TOOL_CURSORS.eraser },
|
|
51
54
|
]);
|
|
52
55
|
|
|
53
56
|
interface ToolContextValue {
|
|
@@ -122,6 +125,18 @@ export function ToolProvider({
|
|
|
122
125
|
return <ToolContext.Provider value={value}>{children}</ToolContext.Provider>;
|
|
123
126
|
}
|
|
124
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Mount a `ToolProvider` only when none exists above us. When the shell-owned
|
|
130
|
+
* comment mount layer (canvas-comment-mount.tsx) already provides one,
|
|
131
|
+
* `DesignCanvas` consumes that instance instead of double-mounting. The hook
|
|
132
|
+
* is called unconditionally; only the returned tree branches (hook rules).
|
|
133
|
+
*/
|
|
134
|
+
export function MaybeToolProvider({ children }: { children: ReactNode }) {
|
|
135
|
+
const outer = useContext(ToolContext);
|
|
136
|
+
if (outer) return <>{children}</>;
|
|
137
|
+
return <ToolProvider>{children}</ToolProvider>;
|
|
138
|
+
}
|
|
139
|
+
|
|
125
140
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
126
141
|
// Hook
|
|
127
142
|
|
|
@@ -25,6 +25,16 @@ export type WsData =
|
|
|
25
25
|
remote: string;
|
|
26
26
|
kind: 'collab';
|
|
27
27
|
slug: string;
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
// T2 (9.1-A) — HMR-only socket for the segregated canvas origin. Receives
|
|
31
|
+
// ONLY `canvas-hmr` broadcasts; never the privileged inspector feed
|
|
32
|
+
// (comments / ai-activity / git-lifecycle / sync:status / selection) and
|
|
33
|
+
// ignores all inbound messages. Hub-pushed canvas code on the canvas
|
|
34
|
+
// origin can open this, but it leaks nothing and mutates nothing.
|
|
35
|
+
id: string;
|
|
36
|
+
remote: string;
|
|
37
|
+
kind: 'canvas-hmr';
|
|
28
38
|
};
|
|
29
39
|
|
|
30
40
|
/**
|
|
@@ -77,6 +87,8 @@ export function createWs(ctx: Context, api: Api, inspect: Inspect, collab: Colla
|
|
|
77
87
|
}
|
|
78
88
|
}
|
|
79
89
|
|
|
90
|
+
// Privileged inspector feed — comments, selection, ai-activity, git-lifecycle,
|
|
91
|
+
// sync:status, fs:*. ONLY the same-origin inspector clients (the shell) get it.
|
|
80
92
|
function broadcast(payload: unknown) {
|
|
81
93
|
const msg = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
82
94
|
for (const ws of clients) {
|
|
@@ -85,6 +97,17 @@ export function createWs(ctx: Context, api: Api, inspect: Inspect, collab: Colla
|
|
|
85
97
|
}
|
|
86
98
|
}
|
|
87
99
|
|
|
100
|
+
// HMR feed — `canvas-hmr` reload signals. Safe to deliver to the segregated
|
|
101
|
+
// canvas origin, so both inspector (shell) AND canvas-hmr (canvas iframe)
|
|
102
|
+
// sockets receive it. T2 (9.1-A).
|
|
103
|
+
function broadcastHmr(payload: unknown) {
|
|
104
|
+
const msg = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
105
|
+
for (const ws of clients) {
|
|
106
|
+
if (ws.data.kind !== 'inspector' && ws.data.kind !== 'canvas-hmr') continue;
|
|
107
|
+
send(ws, msg);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
88
111
|
// Wire bus -> WS broadcasts. inspect.ts emits 'selected' / 'active' after every
|
|
89
112
|
// state write; fs-watch.ts emits 'fs:*' on every save.
|
|
90
113
|
ctx.bus.on('selected', (sel) => broadcast({ type: 'selected', selected: sel }));
|
|
@@ -106,9 +129,17 @@ export function createWs(ctx: Context, api: Api, inspect: Inspect, collab: Colla
|
|
|
106
129
|
// canvas iframes both subscribe.
|
|
107
130
|
ctx.bus.on('git-lifecycle', (payload: unknown) => broadcast({ type: 'git-lifecycle', payload }));
|
|
108
131
|
|
|
132
|
+
// Phase 9 Task 8 — hub-down offline mode. The linked-mode sync runtime emits
|
|
133
|
+
// 'sync:status' on every connection-state change (online / connecting /
|
|
134
|
+
// offline / offline-long, queued-op count, conflict notices). Browser tabs
|
|
135
|
+
// render the offline/synced/escalation banner from this. Solo mode never
|
|
136
|
+
// emits, so this is a no-op for unlinked projects.
|
|
137
|
+
ctx.bus.on('sync:status', (payload: unknown) => broadcast({ type: 'sync:status', payload }));
|
|
138
|
+
|
|
109
139
|
// HMR broadcaster — turns fs:any change events into `canvas-hmr` messages.
|
|
110
140
|
// The iframe-side client (in _shell.html) decides reload strategy from `mode`.
|
|
111
|
-
|
|
141
|
+
// Uses broadcastHmr so the segregated canvas origin's HMR-only sockets get it.
|
|
142
|
+
createHmrBroadcaster(ctx, (msg) => broadcastHmr(msg));
|
|
112
143
|
|
|
113
144
|
// Bind a connection to its room. Stored per-socket so close() can find the
|
|
114
145
|
// right room to disconnect from. Multiplexed via ws.data.id.
|
|
@@ -138,6 +169,11 @@ export function createWs(ctx: Context, api: Api, inspect: Inspect, collab: Colla
|
|
|
138
169
|
await room.connect(conn);
|
|
139
170
|
return;
|
|
140
171
|
}
|
|
172
|
+
if (ws.data.kind === 'canvas-hmr') {
|
|
173
|
+
// HMR-only: join the broadcast set but get NO inspector snapshot.
|
|
174
|
+
clients.add(ws);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
141
177
|
clients.add(ws);
|
|
142
178
|
send(ws, { type: 'snapshot', state: inspect.state });
|
|
143
179
|
},
|
|
@@ -159,6 +195,9 @@ export function createWs(ctx: Context, api: Api, inspect: Inspect, collab: Colla
|
|
|
159
195
|
clients.delete(ws);
|
|
160
196
|
},
|
|
161
197
|
async message(ws, raw) {
|
|
198
|
+
// HMR-only canvas-origin socket: never accepts inbound messages (the
|
|
199
|
+
// canvas iframe only listens for canvas-hmr; it never sends).
|
|
200
|
+
if (ws.data.kind === 'canvas-hmr') return;
|
|
162
201
|
if (ws.data.kind === 'collab') {
|
|
163
202
|
const binding = collabConns.get(ws.data.id);
|
|
164
203
|
if (!binding) return;
|
|
@@ -28,6 +28,26 @@
|
|
|
28
28
|
<meta charset="utf-8" />
|
|
29
29
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
30
30
|
<title>Canvas</title>
|
|
31
|
+
<script>
|
|
32
|
+
// T2 (9.1 / DDR-060 F1 re-audit) — best-effort WebRTC denial in the canvas
|
|
33
|
+
// iframe. CSP `webrtc 'block'` is specified but UNIMPLEMENTED in shipping
|
|
34
|
+
// Chrome/Firefox as of 2026 (Chromium #40188662, Firefox bug 1783489), so
|
|
35
|
+
// it is currently a no-op, and `connect-src 'self'` does NOT cover WebRTC
|
|
36
|
+
// (it is not Fetch). Hub-pushed canvas JS could otherwise exfil via
|
|
37
|
+
// ICE/STUN. Locking these to undefined raises the bar against the naive
|
|
38
|
+
// case before any canvas module loads. NOT bulletproof — a nested
|
|
39
|
+
// same-origin (about:blank) frame can re-acquire them — so WebRTC exfil
|
|
40
|
+
// stays a DOCUMENTED residual of opt-in linked mode (see the linked-mode
|
|
41
|
+
// README banner). `webrtc 'block'` stays in the CSP for when browsers
|
|
42
|
+
// enforce it. The canvas runtime itself uses zero WebRTC.
|
|
43
|
+
for (var __k of ['RTCPeerConnection', 'webkitRTCPeerConnection', 'RTCDataChannel']) {
|
|
44
|
+
try {
|
|
45
|
+
Object.defineProperty(window, __k, { value: undefined, configurable: false, writable: false });
|
|
46
|
+
} catch (_) {
|
|
47
|
+
/* already locked / not present */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
31
51
|
<style>
|
|
32
52
|
:root {
|
|
33
53
|
color-scheme: light dark;
|
|
@@ -122,6 +142,10 @@
|
|
|
122
142
|
const styleEl = document.getElementById('canvas-hide-chrome');
|
|
123
143
|
if (styleEl) styleEl.media = 'all';
|
|
124
144
|
}
|
|
145
|
+
// Shell-owned comment layer toggle. Gallery thumbnails pass `?comments=0`
|
|
146
|
+
// so the comment chrome is suppressed in previews; opened tabs omit it
|
|
147
|
+
// (comments on). hide-chrome exports also suppress comments.
|
|
148
|
+
const commentsEnabled = params.get('comments') !== '0' && params.get('hide-chrome') !== '1';
|
|
125
149
|
|
|
126
150
|
function showError(msg) {
|
|
127
151
|
const el = document.getElementById('canvas-mount-error');
|
|
@@ -146,13 +170,28 @@
|
|
|
146
170
|
const v = msg.version || Date.now();
|
|
147
171
|
// Match by exact filename when we have one; otherwise refresh all.
|
|
148
172
|
const targetFile = (msg.file || '').split('/').pop();
|
|
173
|
+
let matched = false;
|
|
149
174
|
for (const link of document.querySelectorAll('link[rel="stylesheet"]')) {
|
|
150
175
|
const href = link.getAttribute('href') || '';
|
|
151
176
|
if (!targetFile || href.includes(targetFile)) {
|
|
152
177
|
const base = href.split('?')[0];
|
|
153
178
|
link.setAttribute('href', base + '?v=' + v);
|
|
179
|
+
matched = true;
|
|
154
180
|
}
|
|
155
181
|
}
|
|
182
|
+
// A per-canvas `<slug>.css` imported by the TSX is INLINED into the
|
|
183
|
+
// canvas module as a <style> by Bun.build — there is no <link> to
|
|
184
|
+
// swap, so the loop above matches nothing and the edit is silently
|
|
185
|
+
// dropped until a manual hard-reload (RCA hmr-inlined-css-dropped).
|
|
186
|
+
// When nothing matched AND the changed CSS lives in THIS canvas's
|
|
187
|
+
// directory, reload so the rebuilt module re-inlines the fresh CSS.
|
|
188
|
+
// Scoped to same-dir so an unrelated specimen's CSS write doesn't
|
|
189
|
+
// reload every open tab.
|
|
190
|
+
if (!matched && msg.file && canvasRel) {
|
|
191
|
+
const cssDir = msg.file.replace(/[^/]+$/, '');
|
|
192
|
+
const canvasDir = canvasRel.replace(/[^/]+$/, '');
|
|
193
|
+
if (cssDir === canvasDir) location.reload();
|
|
194
|
+
}
|
|
156
195
|
} else if (msg.mode === 'module' || msg.mode === 'hard') {
|
|
157
196
|
// Only reload when the change touches *this* canvas (or `_lib`).
|
|
158
197
|
if (msg.mode === 'hard' || !msg.file || (canvasRel && msg.file === canvasRel)) {
|
|
@@ -234,10 +273,13 @@
|
|
|
234
273
|
window.__canvas_meta_file__ = designRel + '/' + canvasRel;
|
|
235
274
|
});
|
|
236
275
|
try {
|
|
237
|
-
|
|
238
|
-
|
|
276
|
+
// mountCanvas (dist/comment-mount.js) owns the React root + the lite
|
|
277
|
+
// comment provider tree. It externalizes react / react-dom/client to
|
|
278
|
+
// the importmap so the canvas module and the comment layer share one
|
|
279
|
+
// React singleton (inlining a second React would break hooks).
|
|
280
|
+
const [{ mountCanvas }, mod] = await Promise.all([
|
|
281
|
+
import('/_client/comment-mount.js'),
|
|
239
282
|
import(canvasUrl),
|
|
240
|
-
import('react'),
|
|
241
283
|
metaFetch,
|
|
242
284
|
]);
|
|
243
285
|
const Canvas = mod.default;
|
|
@@ -246,8 +288,24 @@
|
|
|
246
288
|
'Canvas module at ' + canvasUrl + ' has no default export (or it is not a component).'
|
|
247
289
|
);
|
|
248
290
|
}
|
|
249
|
-
const
|
|
250
|
-
|
|
291
|
+
const rootEl = document.getElementById('canvas-root');
|
|
292
|
+
mountCanvas(Canvas, {
|
|
293
|
+
rootEl,
|
|
294
|
+
file: designRel + '/' + canvasRel,
|
|
295
|
+
commentsEnabled,
|
|
296
|
+
});
|
|
297
|
+
// T2 (9.1-A) diagnostic — tell the parent the canvas mounted + how many
|
|
298
|
+
// root children it rendered. Lets the shell (and tooling) confirm a
|
|
299
|
+
// cross-origin canvas actually rendered, which a parent-page screenshot
|
|
300
|
+
// of an OOPIF cannot show. Harmless heartbeat; ignored if unhandled.
|
|
301
|
+
setTimeout(() => {
|
|
302
|
+
try {
|
|
303
|
+
window.parent.postMessage(
|
|
304
|
+
{ dgn: 'canvas-mounted', file: designRel + '/' + canvasRel, children: rootEl ? rootEl.children.length : 0 },
|
|
305
|
+
'*'
|
|
306
|
+
);
|
|
307
|
+
} catch (e) { /* parent gone */ }
|
|
308
|
+
}, 800);
|
|
251
309
|
} catch (err) {
|
|
252
310
|
showError((err && err.stack) || String(err));
|
|
253
311
|
}
|
|
@@ -29,6 +29,19 @@
|
|
|
29
29
|
* server (canvas-pipeline.ts pass 1). Don't author them by hand — they'd be
|
|
30
30
|
* stripped + replaced. `/design:handoff` strips them again before the registry
|
|
31
31
|
* sidecar leaves dev (production has no business with dev-time scaffolding).
|
|
32
|
+
*
|
|
33
|
+
* Theme model (two ISOLATED layers — system-review D9):
|
|
34
|
+
* • Canvas-shell CHROME (workspace plane, floating toolbar, minimap, zoom
|
|
35
|
+
* HUD, halos) auto-follows the Maude dev-server theme via `data-maude-theme`
|
|
36
|
+
* on the iframe <html> — the dev-server sets it; never theme the chrome
|
|
37
|
+
* from canvas code.
|
|
38
|
+
* • ARTBOARDS keep THEIR design system's theme. The `data-theme` on the
|
|
39
|
+
* `{{ROOT_CLASS}}` wrapper pins this artboard to a specific DS theme block;
|
|
40
|
+
* leave it at the DS default ({{THEME_DEFAULT}}). Do NOT hardcode a theme
|
|
41
|
+
* the DS doesn't intend — a reviewer flips an individual artboard at will
|
|
42
|
+
* via right-click → Theme ▸ Light / Dark / Follow chrome (enabled when the
|
|
43
|
+
* DS ships both light + dark token blocks). Only pin a non-default theme
|
|
44
|
+
* when the canvas is intentionally single-theme.
|
|
32
45
|
*/
|
|
33
46
|
|
|
34
47
|
import { useState } from "react";
|
|
@@ -14,6 +14,7 @@ The three MANDATORY safety blocks (ANIMATION SAFETY, RELATIVE-URL SAFETY, PLACEH
|
|
|
14
14
|
|
|
15
15
|
### ANIMATION SAFETY (mandatory — applies to motion specimen + any canvas with @keyframes / transitions)
|
|
16
16
|
|
|
17
|
+
- **Tooling contract first (DDR-049).** The authoritative rule for WHICH tool to use is the "Animation tooling contract" in `skills/design-system/SKILL.md`: the **default for both specimens and canvases is `<MotionDemo role>` / the canvas-lib vocabulary from `@maude/canvas-lib`** (wraps `motion/react`), NOT hand-rolled `@keyframes`. Pure-CSS `.motion-*` role classes are a justified-only zero-JS escape hatch. The CSS rules below apply to that escape hatch and to the bounded-geometry constraints the vocabulary already satisfies — they do NOT license reaching for `@keyframes` by default.
|
|
17
18
|
- **Bounded geometry.** Every tile that hosts a rotating / scaling animation MUST have `overflow: hidden`. Otherwise the bounding box extends ~√2× at 45°/135° rotations and overflows adjacent rows. **The motion specimen tiles all set `overflow: hidden` explicitly** — codify this with a CSS comment so future agents reading the file see the rationale.
|
|
18
19
|
- **Sparkle / pulse / twinkle = small only.** Keyframes that scale from `0 → 1 → 0` (or similar "appear-and-disappear" patterns) are for elements ≤56px. **Never apply to full-width tiles.** Demo sparkle on a 32×32 chip inside a tile, never on the tile itself. The studyfi imprint retro D-3 caught a sparkle-on-full-tile that exploded the whole row.
|
|
19
20
|
- **Loop motion = `infinite alternate`.** Specimens meant for continuous display use `animation: <kf> <dur> infinite alternate <easing>`. Single-shot animations finish in 150-200ms and leave the demo invisible on the second look — "looks dead until you hover" is the regression mode.
|
|
@@ -24,9 +25,9 @@ The three MANDATORY safety blocks (ANIMATION SAFETY, RELATIVE-URL SAFETY, PLACEH
|
|
|
24
25
|
### RELATIVE-URL SAFETY (mandatory — applies to any specimen referencing assets, logos, glyphs)
|
|
25
26
|
|
|
26
27
|
- The dev-server serves canvases via `/_canvas-shell.html?canvas=<rel-path>`. **Relative URLs (`../foo.svg`, `./assets/logo.svg`) resolve against the SHELL's URL, not the canvas file's location.** Result: 404 / broken-image icon. Studyfi imprint retro D-4 was caught by the user mid-flow because of exactly this.
|
|
27
|
-
- **Always inline SVGs** in JSX. Use `useId()` for any `<filter id>` / `<linearGradient id>` so multiple instances of the same component don't collide on a single page. OR
|
|
28
|
-
- **
|
|
29
|
-
- **Forbidden:** `<img src="../foo.svg">`, `<image href="./logo.svg">`, `url('../bg.png')` inside inline `<style>` blocks. Any of these surviving in a specimen is a graphic-design-critic blocker.
|
|
28
|
+
- **Always inline SVGs** in JSX. Use `useId()` for any `<filter id>` / `<linearGradient id>` so multiple instances of the same component don't collide on a single page. This is the **strongly-preferred** default. OR
|
|
29
|
+
- **Use an absolute path that mirrors the real on-disk location under the repo root** — the ONE correct form is `/<designRoot>/system/<ds>/assets/<file>`, e.g. `/.design/system/<ds>/assets/logo.svg` (with the default `designRoot` of `.design`). The dev-server's fall-through serves any file by its real path under the repo root (`http.ts` `safePathUnderRoot`); there is **no `/assets/<ds>/` alias** — that earlier wording was wrong and 404s. Substitute the project's actual `designRoot` (read `config.json` `designRoot`) if it isn't `.design`.
|
|
30
|
+
- **Forbidden:** `<img src="../foo.svg">`, `<image href="./logo.svg">`, `url('../bg.png')` inside inline `<style>` blocks, AND the bogus `/assets/<ds>/…` alias. Any of these surviving in a specimen is a graphic-design-critic blocker.
|
|
30
31
|
|
|
31
32
|
### PLACEHOLDER POLICY (mandatory — applies to logo / mark / wordmark / mascot / glyph / illustration claims)
|
|
32
33
|
|
|
@@ -133,7 +134,8 @@ SAFETY BLOCKS — apply to every file in your slice:
|
|
|
133
134
|
+ compositor-only + reduced-motion + no-bouncy-springs.
|
|
134
135
|
- **RELATIVE-URL SAFETY** (see SUB-AGENT-PROMPTS.md). Mandatory if your slice
|
|
135
136
|
references ANY asset (logo, glyph, illustration, background image). Inline
|
|
136
|
-
SVG with `useId()`, OR absolute
|
|
137
|
+
SVG with `useId()`, OR absolute `/<designRoot>/system/<ds>/assets/...` path
|
|
138
|
+
(e.g. `/.design/system/<ds>/assets/...`). NEVER relative, NEVER `/assets/<ds>/`.
|
|
137
139
|
- **PLACEHOLDER POLICY** (see SUB-AGENT-PROMPTS.md). Mandatory if your slice
|
|
138
140
|
scaffolds the `logo` / `empty-state` specimen or any brand-asset surface.
|
|
139
141
|
Read the roster `assets:` block FIRST. If `source:` is populated, copy the
|
|
@@ -211,13 +213,17 @@ The base template above applies to every slice. Each slice gets a small addendum
|
|
|
211
213
|
|
|
212
214
|
**Apply RELATIVE-URL SAFETY in full.** The `iconography` specimen tends to reference glyph SVGs; the `focus` specimen sometimes references focus-ring fixtures. Inline ALL SVGs.
|
|
213
215
|
|
|
216
|
+
**Restraint-default type ladder (D-8).** The `type-scale` specimen demonstrates the scale the tokens already encode — do NOT reintroduce drama the tokens deliberately held back. The default ceiling is **type-scale ratio ≤ 1.2, optical-size ≤ 72, display weight ≤ semibold, tracking ≥ -0.02em**. If `colors_and_type.css` already carries a more dramatic ladder (a high-confidence research recommendation explicitly asked for maximalism), mirror what the tokens declare — never push *beyond* the tokens on your own initiative. Restraint is the default; the user opts UP via `/design:edit`, not the sub-agent.
|
|
217
|
+
|
|
218
|
+
**Research type-fidelity (D-7).** Read the research payload's type recommendation (in the discovery payload / DS README). **Mirror its PRIMARY display-face ROLE exactly.** A "grotesque" direction yields a grotesque display face even when an open-source serif is more convenient to wire up — do NOT let font availability flip the role. Distinguish the **display role** from the **body-accent role**: a recommendation like `display-grotesque-editorial-serif` means a grotesque sans for the DISPLAY face *with* an editorial serif for BODY accents — it does NOT mean the serif is the display face. If the named face is unavailable, substitute within the SAME classification (grotesque → grotesque, not grotesque → serif) and note the substitution so it lands in the bypass-log. (Studyfi read "serif" off `display-grotesque-editorial-serif` and picked Fraunces as the display face — inverting the roles.)
|
|
219
|
+
|
|
214
220
|
### Slice: `brand + voice` (empty-state, logo)
|
|
215
221
|
|
|
216
222
|
**Apply PLACEHOLDER POLICY in full.** This slice is the highest-risk for placeholder-bleed. The `logo` specimen MUST start by reading the roster's `assets:` block:
|
|
217
223
|
- If `assets.logo.source` is a real path, the specimen `<img>`s / inlines the real asset.
|
|
218
224
|
- If `assets.logo.source` is empty, fall back to a `-placeholder` SVG with the rationale in the JSX comment block. Set `source: placeholder` on roster update.
|
|
219
225
|
|
|
220
|
-
**Apply RELATIVE-URL SAFETY in full.** Logo specimens are 100% asset-bound. Inline SVG is the default; absolute
|
|
226
|
+
**Apply RELATIVE-URL SAFETY in full.** Logo specimens are 100% asset-bound. Inline SVG is the default; absolute `/<designRoot>/system/<ds>/assets/logo.svg` (e.g. `/.design/system/<ds>/assets/logo.svg`) is the alternative — never the bogus `/assets/<ds>/` alias.
|
|
221
227
|
|
|
222
228
|
### Slice: `core components` (components-buttons, components-cards, components-inputs)
|
|
223
229
|
|
|
@@ -229,6 +235,8 @@ No slice-specific addenda — the base template covers them.
|
|
|
229
235
|
|
|
230
236
|
### Slice: `ui_kits-*-showcase` (main agent OR signature sub-agent)
|
|
231
237
|
|
|
238
|
+
**Showcase-from-real-app (D-6) — read the real layout BEFORE composing, for existing products.** If the prompt injects real layout paths (the orchestrator passes the app's `AppLayout` + primary-nav component paths when the DS is for a shipped product), you MUST **read those files first and mirror that UX, restyling only** — apply the DS tokens + signature treatment to the *real* screen anatomy. Do NOT invent a plausible-but-fictional product UX when a real one was handed to you; inventing it is the studyfi D-6 regression that forced a ~5500-LOC rebuild. If NO real layout paths are injected (greenfield DS), compose a project-specific mock from `domain_nouns` per the creativity rubric — that path is unchanged.
|
|
239
|
+
|
|
232
240
|
**Apply ANIMATION SAFETY.** Showcase is the highest-density "DS in use" artifact; presence cursors, route transitions, and panel slides all show up here. Every animation must be bounded + reduced-motion-safe + token-derived.
|
|
233
241
|
|
|
234
242
|
**Apply RELATIVE-URL SAFETY.** Showcase often references domain product imagery. Inline SVG mocks, never `<img src="../assets/...">`.
|
|
@@ -241,5 +249,5 @@ No slice-specific addenda — the base template covers them.
|
|
|
241
249
|
- Discovery probe templates (sibling): `plugins/design/skills/design-system/_pastier-probe-templates.md`
|
|
242
250
|
- Motion library decision: `.ai/decisions/DDR-049-motion-one-as-canonical-motion-library.md`
|
|
243
251
|
- Imprint-bootstrap retro (source of D-1 … D-5): `.ai/logs/system-reviews/imprint-bootstrap-review-2026-05-26.md`
|
|
244
|
-
- Asset sweep helper: `
|
|
245
|
-
- Visual sanity helper: `
|
|
252
|
+
- Asset sweep helper: `maude design asset-sweep` (on-PATH `maude` dispatches to the bundled helper — DDR-062)
|
|
253
|
+
- Visual sanity helper: `maude design visual-sanity` (on-PATH `maude` dispatches to the bundled helper — DDR-062)
|
|
@@ -42,6 +42,11 @@
|
|
|
42
42
|
"type": "string",
|
|
43
43
|
"description": "Living codebase snapshot. Default: `.ai/context/codebase-map.md`.",
|
|
44
44
|
"default": ".ai/context/codebase-map.md"
|
|
45
|
+
},
|
|
46
|
+
"designRoot": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "Root of the design plugin's canvas workspace, relative to repo root. Where flow looks for `.meta.json` sidecars + canvases when bridging to design (`/flow:plan` canvas detection, `/flow:done` handoff sweep, `codebase-intelligence` design-artifacts section). Mirrors the design plugin's own default. Flow reads this directory read-only — it never writes into the design root except the `/flow:done` handoff sweep's meta status update. Default: `.design`.",
|
|
49
|
+
"default": ".design"
|
|
45
50
|
}
|
|
46
51
|
}
|
|
47
52
|
},
|
|
@@ -27,7 +27,7 @@ These live at the root of `.ai/` (not in subfolders) and are referenced by `work
|
|
|
27
27
|
|
|
28
28
|
- `<name>-prd.md` — product requirements / brief
|
|
29
29
|
- `<name>-design-system.md` — design system reference (look & feel, tokens, density)
|
|
30
|
-
- `workflows.config.json` — config consumed by all flow commands and skills
|
|
30
|
+
- `workflows.config.json` — config consumed by all flow commands and skills. The optional top-level `quality` map (lint / format / typecheck / tests / build → shell command) is **not** scaffolded here — run `maude doctor --fix` once your `package.json` scripts exist and it detects + adds them (additive, never overwrites your edits).
|
|
31
31
|
|
|
32
32
|
## Conventions
|
|
33
33
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Sidecar cache — Phase C / DDR-061 (cli/lib/cache.mjs).
|
|
2
|
+
# Commit the shareable, content-addressed layers (research/domain + codebase-
|
|
3
|
+
# intelligence are SHA-keyed and reusable across collaborators); keep the
|
|
4
|
+
# brief/run/HEAD-specific layers (research/project, design-context, scenario,
|
|
5
|
+
# security) and the local telemetry (_stats.json, README.md) out of git.
|
|
6
|
+
cache/*
|
|
7
|
+
!cache/codebase-intelligence/
|
|
8
|
+
!cache/research/
|
|
9
|
+
cache/research/*
|
|
10
|
+
!cache/research/domain/
|
|
@@ -9,6 +9,7 @@ scenarios/
|
|
|
9
9
|
├── _lib/ — shared helpers (selectors, fixtures, env)
|
|
10
10
|
└── <scenario-slug>/
|
|
11
11
|
├── spec.md — what the flow does, success criteria, platform matrix
|
|
12
|
+
├── covers.json — OPTIONAL: { web/native/shared git pathspecs } the scenario exercises
|
|
12
13
|
└── <YYYY-MM-DD-HHMM>/
|
|
13
14
|
├── report.md — TL;DR + per-platform pivot table + counter deltas
|
|
14
15
|
├── web-desktop/ — screenshots, logs
|
|
@@ -21,6 +22,7 @@ scenarios/
|
|
|
21
22
|
|
|
22
23
|
- **Scenario slug:** kebab-case, action-oriented (`coach-creates-recurring-practice`, `signup-via-invite`).
|
|
23
24
|
- **Run timestamp:** local time of the run start. Latest run = lexicographically last folder.
|
|
24
|
-
- **Report format:** owned by the `scenario` skill —
|
|
25
|
+
- **Report format:** owned by the `scenario` skill — `maude scenario-report <run-dir>` generates the mechanical tables; the LLM authors only the prose sections.
|
|
26
|
+
- **`covers.json`** (optional): declares the source globs the scenario exercises, split into `web` / `native` / `shared` git pathspecs. Enables the route-aware skip (re-run only when covered files changed) and the web-only scope skip (no sim boot for web-only diffs). Example: `{"web":["app/(video)/**"],"native":["expo-app/**"],"shared":["packages/api-client/**"]}`. Absent → the scenario always runs the full platform matrix.
|
|
25
27
|
|
|
26
28
|
Run with `/flow:scenario <slug>` (registered scenarios) or `/flow:scenario <ad-hoc description>` (one-off).
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
"paths": {
|
|
7
7
|
"prd": ".ai/PROJECT_NAME-prd.md",
|
|
8
8
|
"designSystem": ".ai/PROJECT_NAME-design-system.md",
|
|
9
|
-
"codebaseMap": ".ai/context/codebase-map.md"
|
|
9
|
+
"codebaseMap": ".ai/context/codebase-map.md",
|
|
10
|
+
"designRoot": ".design"
|
|
10
11
|
},
|
|
11
12
|
"platforms": ["web-desktop"],
|
|
12
13
|
"stack": {
|