@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
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// the route table without rewriting this module. The `fetch` export is the
|
|
5
5
|
// top-level fall-through for paths Bun's `routes` field doesn't cover.
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { existsSync, readFileSync, watch } from 'node:fs';
|
|
9
|
+
import { dirname, join, posix, resolve, sep } from 'node:path';
|
|
9
10
|
|
|
10
11
|
import type { Api } from './api.ts';
|
|
11
12
|
import { buildCanvasModule } from './canvas-build.ts';
|
|
@@ -52,6 +53,67 @@ function ext(p: string): string {
|
|
|
52
53
|
return i === -1 ? '' : p.slice(i).toLowerCase();
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
/**
|
|
57
|
+
* T2 (9.1-A) — build the strict CSP for the canvas-content shell. Every inline
|
|
58
|
+
* `<script>` (importmap, module bootstrap, inspector) is allowlisted by sha256
|
|
59
|
+
* hash so we never resort to `'unsafe-inline'`. `connect-src 'self'` is locked
|
|
60
|
+
* to the document's own origin so hub-pushed JSX can't beacon out / hit IMDS /
|
|
61
|
+
* LAN — and `'self'` covers same-origin `ws:`/`wss:` (CSP3), so the HMR + collab
|
|
62
|
+
* sockets (which connect to the canvas origin the iframe loads from) still work
|
|
63
|
+
* while `ws://attacker` / `wss://attacker` exfil is refused. `style-src
|
|
64
|
+
* 'unsafe-inline'` is intentional —
|
|
65
|
+
* specimens use `style={{…}}` attributes + injected `<style>`; style injection
|
|
66
|
+
* is not the F1 RCE vector (script + connect are). No `'unsafe-eval'` — the
|
|
67
|
+
* POC verifies the runtime (motion/pixi/Bun.build output) doesn't need it.
|
|
68
|
+
*
|
|
69
|
+
* `webrtc 'block'` (A6, DDR-060 F1 re-audit) — `connect-src` governs only
|
|
70
|
+
* fetch/XHR/WebSocket/sendBeacon; WebRTC does NOT flow through Fetch, so an
|
|
71
|
+
* `RTCPeerConnection` with an attacker STUN/TURN hostname smuggles bytes out via
|
|
72
|
+
* ICE DNS/STUN even under `connect-src 'self'`. ⚠️ This directive is specified
|
|
73
|
+
* (CSP3) but UNIMPLEMENTED in shipping Chrome/Firefox as of 2026 (Chromium
|
|
74
|
+
* #40188662, Firefox bug 1783489) — currently a NO-OP. The enforceable control
|
|
75
|
+
* today is the RTC-constructor lockout in `templates/_shell.html`; this directive
|
|
76
|
+
* is kept for when browsers honor it. WebRTC + self-navigation exfil remain
|
|
77
|
+
* DOCUMENTED residuals of opt-in linked mode (the reachable data is collab
|
|
78
|
+
* metadata, not repo files — traversal is closed). Do NOT treat this as a closed
|
|
79
|
+
* exfil lane. The canvas runtime itself uses zero WebRTC.
|
|
80
|
+
*
|
|
81
|
+
* `frame-ancestors` (A6) — restricts who may embed the canvas document. The
|
|
82
|
+
* legit embedder is the main dev-server origin, so we allowlist exactly that
|
|
83
|
+
* (`mainOrigin`) plus `'self'`; an arbitrary external page can no longer reframe
|
|
84
|
+
* the canvas. When `mainOrigin` is unknown (tests / pre-boot) the directive is
|
|
85
|
+
* OMITTED rather than set to `'self'` — `'self'` alone would forbid the legit
|
|
86
|
+
* cross-origin embed and blank the canvas.
|
|
87
|
+
*/
|
|
88
|
+
export function cspForCanvasShell(html: string, mainOrigin?: string): string {
|
|
89
|
+
const hashes: string[] = [];
|
|
90
|
+
// Match inline <script> blocks only (no src=). `[^>]*` excludes any with src.
|
|
91
|
+
const re = /<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/gi;
|
|
92
|
+
let m: RegExpExecArray | null;
|
|
93
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex-exec loop.
|
|
94
|
+
while ((m = re.exec(html)) !== null) {
|
|
95
|
+
const body = m[1] ?? '';
|
|
96
|
+
const digest = createHash('sha256').update(body, 'utf8').digest('base64');
|
|
97
|
+
hashes.push(`'sha256-${digest}'`);
|
|
98
|
+
}
|
|
99
|
+
const scriptSrc = ["'self'", ...hashes].join(' ');
|
|
100
|
+
const directives = [
|
|
101
|
+
"default-src 'none'",
|
|
102
|
+
`script-src ${scriptSrc}`,
|
|
103
|
+
"connect-src 'self'",
|
|
104
|
+
"img-src 'self' data: blob:",
|
|
105
|
+
"style-src 'self' 'unsafe-inline'",
|
|
106
|
+
"font-src 'self' data:",
|
|
107
|
+
"frame-src 'self'",
|
|
108
|
+
"base-uri 'none'",
|
|
109
|
+
"object-src 'none'",
|
|
110
|
+
"form-action 'none'",
|
|
111
|
+
"webrtc 'block'",
|
|
112
|
+
];
|
|
113
|
+
if (mainOrigin) directives.push(`frame-ancestors 'self' ${mainOrigin}`);
|
|
114
|
+
return directives.join('; ');
|
|
115
|
+
}
|
|
116
|
+
|
|
55
117
|
function safePathUnderRoot(reqUrl: string, repoRoot: string): string | null {
|
|
56
118
|
let pathname: string;
|
|
57
119
|
try {
|
|
@@ -71,16 +133,60 @@ const DIST_DIR = join(HERE, 'dist');
|
|
|
71
133
|
const CLIENT_DIR = join(HERE, 'client');
|
|
72
134
|
const TEMPLATES_DIR = join(HERE, '..', 'templates');
|
|
73
135
|
|
|
74
|
-
// In-memory transpile cache. Key = absolute canvas path
|
|
75
|
-
//
|
|
76
|
-
//
|
|
136
|
+
// In-memory transpile cache. Key = absolute canvas path. Repeat GETs against an
|
|
137
|
+
// unchanged source skip the parse + ID-injection + Bun.Transpiler + Bun.build
|
|
138
|
+
// entirely.
|
|
77
139
|
interface CanvasCacheEntry {
|
|
78
|
-
|
|
140
|
+
/** Freshness signature over the .tsx AND every directly-imported sibling/
|
|
141
|
+
* relative `.css` it inlines (Bun.build extracts `import "./x.css"` into a
|
|
142
|
+
* `<style>` tag — there is no `<link>`). Rebuild when ANY of them changes.
|
|
143
|
+
* A prior version keyed only on the .tsx mtime, so editing a sibling
|
|
144
|
+
* `<canvas>.css` was a cache HIT: the HMR reload (mode:'module') re-served the
|
|
145
|
+
* stale inlined CSS and the edit only showed once the .tsx itself changed.
|
|
146
|
+
* (DDR-064 collab dogfooding finding, 2026-05-29.) */
|
|
147
|
+
sig: string;
|
|
79
148
|
etag: string;
|
|
80
149
|
js: string;
|
|
150
|
+
/** Absolute paths of the relative `.css` imports inlined into this build —
|
|
151
|
+
* re-statted each request to recompute `sig` without re-reading the source. */
|
|
152
|
+
cssDeps: string[];
|
|
81
153
|
}
|
|
82
154
|
const canvasCache = new Map<string, CanvasCacheEntry>();
|
|
83
155
|
|
|
156
|
+
/** Relative `.css` import specifiers in a canvas source → absolute paths (the
|
|
157
|
+
* files Bun.build inlines). Bare / virtual specifiers (`@maude/…`, npm) are
|
|
158
|
+
* skipped — only on-disk relative CSS affects the inlined output. Resolved
|
|
159
|
+
* paths are clamped to `designRoot`: a canvas source is attacker-influenced
|
|
160
|
+
* under linked mode (DDR-054), and these paths are `stat`-ed for mtime, so a
|
|
161
|
+
* `../../../etc/…` specifier must not let the freshness probe reach outside the
|
|
162
|
+
* design tree. Legit DS imports (`../../system/<ds>/…`) stay inside designRoot. */
|
|
163
|
+
function cssDepsFromSource(source: string, canvasAbsPath: string, designRoot: string): string[] {
|
|
164
|
+
const dir = dirname(canvasAbsPath);
|
|
165
|
+
const root = resolve(designRoot);
|
|
166
|
+
const deps: string[] = [];
|
|
167
|
+
const re = /import\s+["']([^"']+\.css)["']/g;
|
|
168
|
+
let m: RegExpExecArray | null = re.exec(source);
|
|
169
|
+
while (m !== null) {
|
|
170
|
+
if (m[1].startsWith('.')) {
|
|
171
|
+
const abs = resolve(dir, m[1]);
|
|
172
|
+
if (abs === root || abs.startsWith(root + sep)) deps.push(abs);
|
|
173
|
+
}
|
|
174
|
+
m = re.exec(source);
|
|
175
|
+
}
|
|
176
|
+
return deps;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** mtime signature over the .tsx + its inlined CSS deps. A missing/unreadable
|
|
180
|
+
* file contributes 0 — a delete is itself a change, so the signature differs. */
|
|
181
|
+
function canvasFreshnessSig(tsxAbsPath: string, cssDeps: string[]): string {
|
|
182
|
+
const parts: string[] = [];
|
|
183
|
+
for (const p of [tsxAbsPath, ...cssDeps]) {
|
|
184
|
+
const mt = Bun.file(p).lastModified;
|
|
185
|
+
parts.push(`${p}@${Number.isFinite(mt) ? mt : 0}`);
|
|
186
|
+
}
|
|
187
|
+
return parts.join('|');
|
|
188
|
+
}
|
|
189
|
+
|
|
84
190
|
async function serveCanvasTsx(
|
|
85
191
|
absPath: string,
|
|
86
192
|
req: Request,
|
|
@@ -90,12 +196,16 @@ async function serveCanvasTsx(
|
|
|
90
196
|
const file = Bun.file(absPath);
|
|
91
197
|
if (!(await file.exists())) return new Response('Not found', { status: 404 });
|
|
92
198
|
|
|
93
|
-
//
|
|
94
|
-
|
|
199
|
+
// Freshness = the .tsx mtime AND every inlined sibling `.css` mtime. Keying on
|
|
200
|
+
// the .tsx alone meant a direct edit to `<canvas>.css` was a cache HIT, so the
|
|
201
|
+
// HMR reload (mode:'module') re-served the stale inlined CSS — the edit only
|
|
202
|
+
// surfaced once the .tsx itself changed (DDR-064 dogfooding finding).
|
|
95
203
|
let cached = canvasCache.get(absPath);
|
|
204
|
+
const sig = canvasFreshnessSig(absPath, cached?.cssDeps ?? []);
|
|
96
205
|
|
|
97
|
-
if (!cached || cached.
|
|
206
|
+
if (!cached || cached.sig !== sig) {
|
|
98
207
|
const source = await file.text();
|
|
208
|
+
const cssDeps = cssDepsFromSource(source, absPath, ctx.paths.designRoot);
|
|
99
209
|
let result: Awaited<ReturnType<typeof buildCanvasModule>>;
|
|
100
210
|
try {
|
|
101
211
|
result = await buildCanvasModule(absPath, source, {
|
|
@@ -114,7 +224,14 @@ async function serveCanvasTsx(
|
|
|
114
224
|
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
115
225
|
});
|
|
116
226
|
}
|
|
117
|
-
|
|
227
|
+
// Recompute the signature against the freshly-parsed deps — this very edit
|
|
228
|
+
// may have added or removed a `.css` import.
|
|
229
|
+
cached = {
|
|
230
|
+
sig: canvasFreshnessSig(absPath, cssDeps),
|
|
231
|
+
etag: result.etag,
|
|
232
|
+
js: result.js,
|
|
233
|
+
cssDeps,
|
|
234
|
+
};
|
|
118
235
|
canvasCache.set(absPath, cached);
|
|
119
236
|
// Persist the locator map. Awaited so the inspector / Phase-12 layers
|
|
120
237
|
// panel sees a consistent (cdId -> source) view by the time the canvas
|
|
@@ -156,6 +273,22 @@ async function serveFile(absPath: string, headers: Record<string, string> = {}):
|
|
|
156
273
|
export interface Http {
|
|
157
274
|
routes: Record<string, (req: Request) => Response | Promise<Response>>;
|
|
158
275
|
fetch(req: Request): Promise<Response>;
|
|
276
|
+
/**
|
|
277
|
+
* T2 (9.1-A) — build the canvas mount-harness response. `applyCsp` adds the
|
|
278
|
+
* strict CSP (always on for the segregated canvas origin; the legacy main
|
|
279
|
+
* origin keeps it env-gated for the POC). Shared so both listeners produce
|
|
280
|
+
* byte-identical HTML.
|
|
281
|
+
*/
|
|
282
|
+
serveCanvasShell(applyCsp: boolean): Promise<Response>;
|
|
283
|
+
/**
|
|
284
|
+
* T2 (9.1-A) — allowlist gate for the segregated canvas origin. Returns true
|
|
285
|
+
* only for the routes the canvas runtime legitimately needs (shell, runtime
|
|
286
|
+
* bundles, comment-mount, transpiled .tsx + CSS/assets under designRoot,
|
|
287
|
+
* git-user, canvas-meta, health). Everything else is 403'd at the door so
|
|
288
|
+
* hub-pushed JSX can't reach /_api/export, /_config, /_sync-status, comments,
|
|
289
|
+
* the app shell, or arbitrary repo files. WS upgrades are gated in server.ts.
|
|
290
|
+
*/
|
|
291
|
+
isCanvasSafeRoute(pathname: string): boolean;
|
|
159
292
|
}
|
|
160
293
|
|
|
161
294
|
export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActivity): Http {
|
|
@@ -241,7 +374,24 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
241
374
|
|
|
242
375
|
'/_active': () => Response.json(inspect.state),
|
|
243
376
|
|
|
244
|
-
|
|
377
|
+
// Phase 9 Task 8 — offline-mode banner poll fallback. The linked-mode sync
|
|
378
|
+
// runtime writes `_sync.json`; browser tabs also get live pushes over the
|
|
379
|
+
// WS ('sync:status'). Returns `{ linked: false }` in solo mode.
|
|
380
|
+
'/_sync-status': () => {
|
|
381
|
+
const file = join(ctx.paths.designRoot, '_sync.json');
|
|
382
|
+
if (!existsSync(file)) {
|
|
383
|
+
return Response.json({ linked: false }, { headers: { 'Cache-Control': 'no-store' } });
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
return Response.json(JSON.parse(readFileSync(file, 'utf8')), {
|
|
387
|
+
headers: { 'Cache-Control': 'no-store' },
|
|
388
|
+
});
|
|
389
|
+
} catch {
|
|
390
|
+
return Response.json({ linked: false }, { headers: { 'Cache-Control': 'no-store' } });
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
'/_config': () => Response.json({ ...ctx.cfg, canvasOrigin: ctx.canvasOrigin }),
|
|
245
395
|
|
|
246
396
|
'/_index-data': async () =>
|
|
247
397
|
Response.json(await api.buildIndexData(), { headers: { 'Cache-Control': 'no-store' } }),
|
|
@@ -585,14 +735,10 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
585
735
|
// Query parameter ?canvas=<path-relative-to-designRoot> tells the shell
|
|
586
736
|
// which canvas to import + mount. See plugins/design/templates/_shell.html.
|
|
587
737
|
if (pathname === '/_canvas-shell.html' || pathname === '/_canvas-shell') {
|
|
588
|
-
|
|
589
|
-
//
|
|
590
|
-
//
|
|
591
|
-
|
|
592
|
-
const injected = inspect.injectInspector(shellHtml);
|
|
593
|
-
return new Response(injected, {
|
|
594
|
-
headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' },
|
|
595
|
-
});
|
|
738
|
+
// The segregated canvas origin (server.ts) calls serveCanvasShell(true)
|
|
739
|
+
// directly with CSP always on; on the legacy main origin the CSP stays
|
|
740
|
+
// env-gated (MAUDE_CSP_POC) for the POC / backwards-compat.
|
|
741
|
+
return serveCanvasShell(process.env.MAUDE_CSP_POC === '1');
|
|
596
742
|
}
|
|
597
743
|
|
|
598
744
|
// Fall-through: serve user repo files (designRoot + everything under repoRoot).
|
|
@@ -622,5 +768,99 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect, ai: AiActiv
|
|
|
622
768
|
}
|
|
623
769
|
}
|
|
624
770
|
|
|
625
|
-
|
|
771
|
+
async function serveCanvasShell(applyCsp: boolean): Promise<Response> {
|
|
772
|
+
const shellHtml = await Bun.file(join(TEMPLATES_DIR, '_shell.html')).text();
|
|
773
|
+
// Inject inspector overlay — Cmd+Click selection + add-comment flow.
|
|
774
|
+
const injected = inspect.injectInspector(shellHtml);
|
|
775
|
+
const headers: Record<string, string> = {
|
|
776
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
777
|
+
'Cache-Control': 'no-store',
|
|
778
|
+
};
|
|
779
|
+
if (applyCsp) headers['Content-Security-Policy'] = cspForCanvasShell(injected, ctx.mainOrigin);
|
|
780
|
+
return new Response(injected, { headers });
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Canvas assets the segregated origin may serve out of designRoot. Excludes
|
|
784
|
+
// `.json` so no `*.meta.json` / `config.json` / `_comments/*.json` leaks via
|
|
785
|
+
// the static lane (canvas-meta goes through the gated /_api route instead).
|
|
786
|
+
const CANVAS_ASSET_EXTS = new Set([
|
|
787
|
+
'.tsx',
|
|
788
|
+
'.css',
|
|
789
|
+
'.svg',
|
|
790
|
+
'.png',
|
|
791
|
+
'.jpg',
|
|
792
|
+
'.jpeg',
|
|
793
|
+
'.gif',
|
|
794
|
+
'.webp',
|
|
795
|
+
'.ico',
|
|
796
|
+
'.woff',
|
|
797
|
+
'.woff2',
|
|
798
|
+
'.ttf',
|
|
799
|
+
'.otf',
|
|
800
|
+
]);
|
|
801
|
+
|
|
802
|
+
// Exact API paths the canvas iframe needs (collab + display data). See
|
|
803
|
+
// isCanvasSafeRoute for the trust rationale. Mutations are limited to inert
|
|
804
|
+
// collab data (annotations SVG, comment replies via the dynamic route).
|
|
805
|
+
const CANVAS_SAFE_API = new Set([
|
|
806
|
+
'/_api/git-user', // presence display name
|
|
807
|
+
'/_api/canvas-meta', // layout/viewport sidecar (GET + PATCH)
|
|
808
|
+
'/_api/annotations', // annotation SVG (GET + PUT) — drives the collab bridge
|
|
809
|
+
'/_api/git-committers', // @mention autocomplete
|
|
810
|
+
'/_api/ai', // AI-activity banner
|
|
811
|
+
'/_comments', // per-file comment list (renders pins)
|
|
812
|
+
]);
|
|
813
|
+
|
|
814
|
+
function isCanvasSafeRoute(pathname: string): boolean {
|
|
815
|
+
// A1/A2 (DDR-060 F1 re-audit, phase-9.1-t2-f1-cross-origin-reaudit.md) —
|
|
816
|
+
// DECODE + NORMALIZE before gating. `URL.pathname` preserves `%2f` (it does
|
|
817
|
+
// NOT decode it to `/`), so a raw allowlist check on the encoded path is
|
|
818
|
+
// fooled: `/.design/..%2fsite%2fx.css` reads as ONE opaque segment under the
|
|
819
|
+
// designRoot with an asset ext (the `_`-segment + ext checks see no literal
|
|
820
|
+
// slash to split on), yet `safePathUnderRoot` later DECODES the same `%2f`,
|
|
821
|
+
// turns `..%2f` into a real `../`, and climbs out of the designRoot — re-
|
|
822
|
+
// confined only to repoRoot. That decode mismatch let a hub-pushed canvas
|
|
823
|
+
// read any repo `.tsx`/`.css`/`.svg`/font + `_history` snapshots. Decoding
|
|
824
|
+
// here makes the gate agree with the resolver: `..%2f` → `../`, normalize
|
|
825
|
+
// collapses it, and the path no longer matches designPrefix → 403. A
|
|
826
|
+
// malformed escape (`%ZZ`) throws → reject. This gate runs ONLY on the
|
|
827
|
+
// segregated canvas origin (server.ts), so the main origin is untouched.
|
|
828
|
+
let safe: string;
|
|
829
|
+
try {
|
|
830
|
+
safe = posix.normalize(decodeURIComponent(pathname));
|
|
831
|
+
} catch {
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
if (safe === '/_canvas-shell.html' || safe === '/_canvas-shell') return true;
|
|
835
|
+
if (safe === '/_health') return true;
|
|
836
|
+
if (safe === '/_client/comment-mount.js') return true;
|
|
837
|
+
// Canvas-chrome stylesheets (composer / thread / pin / cursor CSS). Inert
|
|
838
|
+
// static assets from the dev-server distribution — no secrets, no code
|
|
839
|
+
// exec, no repo content. Without this the cross-origin canvas 403s e.g.
|
|
840
|
+
// `/_client/comments-overlay.css`, so the in-iframe comment composer renders
|
|
841
|
+
// unstyled and, missing `position: fixed`, collapses to the top-left (0,0).
|
|
842
|
+
// Allowed by pattern (not per-file) so future chrome CSS can't silently
|
|
843
|
+
// regress the same way.
|
|
844
|
+
if (safe.startsWith('/_client/') && ext(safe) === '.css') return true;
|
|
845
|
+
if (safe.startsWith('/_canvas-runtime/')) return true;
|
|
846
|
+
// Collab + display-data endpoints the canvas runtime legitimately calls from
|
|
847
|
+
// inside the iframe. All are reads or inert collab writes (annotations SVG,
|
|
848
|
+
// comment replies) — the "safe to sync" set per DDR-054. None expose code
|
|
849
|
+
// execution, secrets, export, /_config, /_sync-status, or files outside
|
|
850
|
+
// designRoot/annotations; the canvas origin's CSP `connect-src 'self'` still
|
|
851
|
+
// confines the iframe so hub-pushed JSX can't reach IMDS/LAN/main-origin.
|
|
852
|
+
if (CANVAS_SAFE_API.has(safe)) return true;
|
|
853
|
+
// POST /_api/comments/<id>/reply — dynamic path (fetch-handled).
|
|
854
|
+
if (/^\/_api\/comments\/[A-Za-z0-9_]+\/reply$/.test(safe)) return true;
|
|
855
|
+
const designPrefix = `/${ctx.paths.designRel.replace(/^\/+|\/+$/g, '')}/`;
|
|
856
|
+
if (safe.startsWith(designPrefix)) {
|
|
857
|
+
const rest = safe.slice(designPrefix.length);
|
|
858
|
+
// Reject runtime/state dirs+files (_comments, _sync.json, _history, …).
|
|
859
|
+
if (rest.split('/').some((seg) => seg.startsWith('_'))) return false;
|
|
860
|
+
return CANVAS_ASSET_EXTS.has(ext(safe));
|
|
861
|
+
}
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return { routes, fetch, serveCanvasShell, isCanvasSafeRoute };
|
|
626
866
|
}
|
|
@@ -63,9 +63,27 @@ export function crossedDragThreshold(
|
|
|
63
63
|
* returns `no-op` for the corresponding pointer events so the SVG overlay
|
|
64
64
|
* can grab them natively.
|
|
65
65
|
*/
|
|
66
|
-
export type Tool =
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
export type Tool =
|
|
67
|
+
| 'move'
|
|
68
|
+
| 'hand'
|
|
69
|
+
| 'comment'
|
|
70
|
+
| 'pen'
|
|
71
|
+
| 'rect'
|
|
72
|
+
| 'ellipse'
|
|
73
|
+
| 'arrow'
|
|
74
|
+
| 'sticky'
|
|
75
|
+
| 'text'
|
|
76
|
+
| 'eraser';
|
|
77
|
+
|
|
78
|
+
const ANNOTATION_TOOLS = new Set<Tool>([
|
|
79
|
+
'pen',
|
|
80
|
+
'rect',
|
|
81
|
+
'ellipse',
|
|
82
|
+
'arrow',
|
|
83
|
+
'sticky',
|
|
84
|
+
'text',
|
|
85
|
+
'eraser',
|
|
86
|
+
]);
|
|
69
87
|
|
|
70
88
|
export function isAnnotationTool(t: Tool): boolean {
|
|
71
89
|
return ANNOTATION_TOOLS.has(t);
|
|
@@ -146,6 +164,11 @@ export function classify(input: ClassifyInput): RouterAction {
|
|
|
146
164
|
if (k === 'r') return { kind: 'tool', tool: 'rect' };
|
|
147
165
|
if (k === 'o') return { kind: 'tool', tool: 'ellipse' };
|
|
148
166
|
if (k === 'a') return { kind: 'tool', tool: 'arrow' };
|
|
167
|
+
// Phase 21 — N = sticky Note ('S' is taken by the shell Design-system view
|
|
168
|
+
// + Shift-marquee); T = standalone Text. Both are bare letters the shell
|
|
169
|
+
// yields when focus is inside the canvas iframe (app.jsx onKey bail).
|
|
170
|
+
if (k === 'n') return { kind: 'tool', tool: 'sticky' };
|
|
171
|
+
if (k === 't') return { kind: 'tool', tool: 'text' };
|
|
149
172
|
if (k === 'e') return { kind: 'tool', tool: 'eraser' };
|
|
150
173
|
if (input.key === 'Escape') return { kind: 'escape' };
|
|
151
174
|
return { kind: 'no-op' };
|
|
@@ -268,6 +291,16 @@ export interface UseInputRouterOptions {
|
|
|
268
291
|
callbacks: RouterCallbacks;
|
|
269
292
|
/** When false, listeners are not attached. Defaults to true. */
|
|
270
293
|
enabled?: boolean;
|
|
294
|
+
/**
|
|
295
|
+
* Allowlist of action kinds this router is permitted to CLAIM (preventDefault
|
|
296
|
+
* + stopImmediatePropagation + dispatch). Any classified action outside the
|
|
297
|
+
* set is downgraded to `no-op` so it propagates untouched to other listeners.
|
|
298
|
+
* Omit to claim everything (the default — used by the full DesignCanvas
|
|
299
|
+
* router). The shell-owned comment mount layer passes a narrow set so it can
|
|
300
|
+
* coexist as an ANCESTOR capture-listener over a UI canvas's own router
|
|
301
|
+
* without swallowing select / context-menu / undo gestures it doesn't own.
|
|
302
|
+
*/
|
|
303
|
+
claimableActions?: ReadonlySet<RouterAction['kind']>;
|
|
271
304
|
}
|
|
272
305
|
|
|
273
306
|
export function isEditableTarget(t: EventTarget | null): boolean {
|
|
@@ -295,13 +328,21 @@ export function isOverlayTarget(t: EventTarget | null): boolean {
|
|
|
295
328
|
}
|
|
296
329
|
|
|
297
330
|
export function useInputRouter(opts: UseInputRouterOptions): void {
|
|
298
|
-
const { hostRef, getActiveTool, isSpaceHeld, callbacks, enabled = true } = opts;
|
|
331
|
+
const { hostRef, getActiveTool, isSpaceHeld, callbacks, enabled = true, claimableActions } = opts;
|
|
299
332
|
|
|
300
333
|
useEffect(() => {
|
|
301
334
|
if (!enabled) return;
|
|
302
335
|
const host = hostRef.current;
|
|
303
336
|
if (!host) return;
|
|
304
337
|
|
|
338
|
+
// Downgrade any action this router isn't permitted to claim to no-op so it
|
|
339
|
+
// propagates untouched (no preventDefault / no dispatch). Identity pass-
|
|
340
|
+
// through when no allowlist is configured.
|
|
341
|
+
const claim = (action: RouterAction): RouterAction =>
|
|
342
|
+
claimableActions && action.kind !== 'no-op' && !claimableActions.has(action.kind)
|
|
343
|
+
? { kind: 'no-op' }
|
|
344
|
+
: action;
|
|
345
|
+
|
|
305
346
|
const dispatch = (action: RouterAction): void => {
|
|
306
347
|
switch (action.kind) {
|
|
307
348
|
case 'hover':
|
|
@@ -334,18 +375,20 @@ export function useInputRouter(opts: UseInputRouterOptions): void {
|
|
|
334
375
|
};
|
|
335
376
|
|
|
336
377
|
const onPointerMove = (e: PointerEvent): void => {
|
|
337
|
-
const action =
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
378
|
+
const action = claim(
|
|
379
|
+
classify({
|
|
380
|
+
type: 'pointermove',
|
|
381
|
+
button: e.button,
|
|
382
|
+
metaKey: e.metaKey,
|
|
383
|
+
ctrlKey: e.ctrlKey,
|
|
384
|
+
shiftKey: e.shiftKey,
|
|
385
|
+
altKey: e.altKey,
|
|
386
|
+
clientX: e.clientX,
|
|
387
|
+
clientY: e.clientY,
|
|
388
|
+
spaceHeld: isSpaceHeld?.() ?? false,
|
|
389
|
+
activeTool: getActiveTool(),
|
|
390
|
+
})
|
|
391
|
+
);
|
|
349
392
|
dispatch(action);
|
|
350
393
|
};
|
|
351
394
|
|
|
@@ -354,18 +397,20 @@ export function useInputRouter(opts: UseInputRouterOptions): void {
|
|
|
354
397
|
// their own clicks. The router is in capture phase, so we have to
|
|
355
398
|
// bail HERE before classify can claim the event.
|
|
356
399
|
if (isOverlayTarget(e.target)) return;
|
|
357
|
-
const action =
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
400
|
+
const action = claim(
|
|
401
|
+
classify({
|
|
402
|
+
type: 'pointerdown',
|
|
403
|
+
button: e.button,
|
|
404
|
+
metaKey: e.metaKey,
|
|
405
|
+
ctrlKey: e.ctrlKey,
|
|
406
|
+
shiftKey: e.shiftKey,
|
|
407
|
+
altKey: e.altKey,
|
|
408
|
+
clientX: e.clientX,
|
|
409
|
+
clientY: e.clientY,
|
|
410
|
+
spaceHeld: isSpaceHeld?.() ?? false,
|
|
411
|
+
activeTool: getActiveTool(),
|
|
412
|
+
})
|
|
413
|
+
);
|
|
369
414
|
if (action.kind !== 'no-op') {
|
|
370
415
|
// Suppress native behavior on every event the router claims —
|
|
371
416
|
// button presses don't fire, inputs don't focus, the canvas
|
|
@@ -386,18 +431,20 @@ export function useInputRouter(opts: UseInputRouterOptions): void {
|
|
|
386
431
|
*/
|
|
387
432
|
const onMouseDown = (e: MouseEvent): void => {
|
|
388
433
|
if (isOverlayTarget(e.target)) return;
|
|
389
|
-
const action =
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
434
|
+
const action = claim(
|
|
435
|
+
classify({
|
|
436
|
+
type: 'pointerdown',
|
|
437
|
+
button: e.button,
|
|
438
|
+
metaKey: e.metaKey,
|
|
439
|
+
ctrlKey: e.ctrlKey,
|
|
440
|
+
shiftKey: e.shiftKey,
|
|
441
|
+
altKey: e.altKey,
|
|
442
|
+
clientX: e.clientX,
|
|
443
|
+
clientY: e.clientY,
|
|
444
|
+
spaceHeld: isSpaceHeld?.() ?? false,
|
|
445
|
+
activeTool: getActiveTool(),
|
|
446
|
+
})
|
|
447
|
+
);
|
|
401
448
|
if (action.kind !== 'no-op') {
|
|
402
449
|
e.preventDefault();
|
|
403
450
|
e.stopImmediatePropagation();
|
|
@@ -414,41 +461,55 @@ export function useInputRouter(opts: UseInputRouterOptions): void {
|
|
|
414
461
|
if (isOverlayTarget(e.target)) return;
|
|
415
462
|
const tool = getActiveTool();
|
|
416
463
|
const mod = e.metaKey || e.ctrlKey;
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
464
|
+
// Map the click to the action kind the matching pointerdown would have
|
|
465
|
+
// produced, then honor the claim allowlist so a scoped router (the
|
|
466
|
+
// comment mount layer) doesn't suppress clicks it never claimed.
|
|
467
|
+
const wouldRouteKind: RouterAction['kind'] | null =
|
|
468
|
+
tool === 'comment'
|
|
469
|
+
? 'drop-comment'
|
|
470
|
+
: tool === 'move' && mod && e.button === 0
|
|
471
|
+
? 'select'
|
|
472
|
+
: e.button === 2
|
|
473
|
+
? 'context-menu'
|
|
474
|
+
: null;
|
|
475
|
+
if (wouldRouteKind && (!claimableActions || claimableActions.has(wouldRouteKind))) {
|
|
420
476
|
e.preventDefault();
|
|
421
477
|
e.stopImmediatePropagation();
|
|
422
478
|
}
|
|
423
479
|
};
|
|
424
480
|
|
|
425
481
|
const onContextMenu = (e: MouseEvent): void => {
|
|
482
|
+
const action = claim(
|
|
483
|
+
classify({
|
|
484
|
+
type: 'contextmenu',
|
|
485
|
+
clientX: e.clientX,
|
|
486
|
+
clientY: e.clientY,
|
|
487
|
+
metaKey: e.metaKey,
|
|
488
|
+
ctrlKey: e.ctrlKey,
|
|
489
|
+
shiftKey: e.shiftKey,
|
|
490
|
+
altKey: e.altKey,
|
|
491
|
+
activeTool: getActiveTool(),
|
|
492
|
+
})
|
|
493
|
+
);
|
|
494
|
+
if (action.kind === 'no-op') return; // not ours to claim — let it bubble
|
|
426
495
|
e.preventDefault();
|
|
427
496
|
e.stopImmediatePropagation();
|
|
428
|
-
const action = classify({
|
|
429
|
-
type: 'contextmenu',
|
|
430
|
-
clientX: e.clientX,
|
|
431
|
-
clientY: e.clientY,
|
|
432
|
-
metaKey: e.metaKey,
|
|
433
|
-
ctrlKey: e.ctrlKey,
|
|
434
|
-
shiftKey: e.shiftKey,
|
|
435
|
-
altKey: e.altKey,
|
|
436
|
-
activeTool: getActiveTool(),
|
|
437
|
-
});
|
|
438
497
|
dispatch(action);
|
|
439
498
|
};
|
|
440
499
|
|
|
441
500
|
const onKeyDown = (e: KeyboardEvent): void => {
|
|
442
|
-
const action =
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
501
|
+
const action = claim(
|
|
502
|
+
classify({
|
|
503
|
+
type: 'keydown',
|
|
504
|
+
key: e.key,
|
|
505
|
+
metaKey: e.metaKey,
|
|
506
|
+
ctrlKey: e.ctrlKey,
|
|
507
|
+
shiftKey: e.shiftKey,
|
|
508
|
+
altKey: e.altKey,
|
|
509
|
+
isEditable: isEditableTarget(e.target),
|
|
510
|
+
activeTool: getActiveTool(),
|
|
511
|
+
})
|
|
512
|
+
);
|
|
452
513
|
if (
|
|
453
514
|
action.kind === 'tool' ||
|
|
454
515
|
action.kind === 'escape' ||
|
|
@@ -486,7 +547,7 @@ export function useInputRouter(opts: UseInputRouterOptions): void {
|
|
|
486
547
|
} as EventListenerOptions);
|
|
487
548
|
doc.removeEventListener('keydown', onKeyDown, true);
|
|
488
549
|
};
|
|
489
|
-
}, [enabled, hostRef, getActiveTool, isSpaceHeld, callbacks]);
|
|
550
|
+
}, [enabled, hostRef, getActiveTool, isSpaceHeld, callbacks, claimableActions]);
|
|
490
551
|
}
|
|
491
552
|
|
|
492
553
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -42,7 +42,7 @@ const CHROME_CSS = `
|
|
|
42
42
|
line-height: 1;
|
|
43
43
|
letter-spacing: 0.02em;
|
|
44
44
|
color: var(--maude-hud-accent-fg, #fff);
|
|
45
|
-
border: 1.5px solid var(--bg-0, #fff);
|
|
45
|
+
border: 1.5px solid var(--maude-chrome-bg-0, #fff);
|
|
46
46
|
margin-left: -6px;
|
|
47
47
|
cursor: pointer;
|
|
48
48
|
position: relative;
|
|
@@ -70,20 +70,20 @@ const CHROME_CSS = `
|
|
|
70
70
|
right: 0;
|
|
71
71
|
min-width: 168px;
|
|
72
72
|
padding: 10px 12px;
|
|
73
|
-
background: var(--bg-1, #fff);
|
|
74
|
-
color: var(--fg-0, #111);
|
|
75
|
-
border: 1px solid var(--border
|
|
73
|
+
background: var(--maude-chrome-bg-1, #fff);
|
|
74
|
+
color: var(--maude-chrome-fg-0, #111);
|
|
75
|
+
border: 1px solid var(--maude-chrome-border, rgba(0,0,0,0.16));
|
|
76
76
|
border-radius: var(--radius-md, 4px);
|
|
77
77
|
font-size: 12px;
|
|
78
78
|
line-height: 1.35;
|
|
79
79
|
white-space: nowrap;
|
|
80
|
-
box-shadow: 0 6px 18px rgba(0,0,0,0.10);
|
|
80
|
+
box-shadow: 0 6px 18px var(--maude-chrome-shadow, rgba(0,0,0,0.10));
|
|
81
81
|
z-index: 11;
|
|
82
82
|
}
|
|
83
83
|
.dc-participant-popover__name {
|
|
84
84
|
font-weight: 600;
|
|
85
85
|
margin-bottom: 8px;
|
|
86
|
-
color: var(--fg-0, #111);
|
|
86
|
+
color: var(--maude-chrome-fg-0, #111);
|
|
87
87
|
letter-spacing: 0.01em;
|
|
88
88
|
}
|
|
89
89
|
.dc-participant-popover__btn {
|
|
@@ -102,11 +102,11 @@ const CHROME_CSS = `
|
|
|
102
102
|
}
|
|
103
103
|
.dc-participant-popover__btn:hover { background: var(--maude-hud-accent-hover, var(--maude-hud-accent, oklch(50% 0.170 50))); }
|
|
104
104
|
.dc-participant-popover__btn--stop {
|
|
105
|
-
background: var(--
|
|
106
|
-
color: var(--fg-0, #111);
|
|
107
|
-
border: 1px solid var(--border
|
|
105
|
+
background: color-mix(in oklab, var(--maude-chrome-fg-0, #111) 10%, transparent);
|
|
106
|
+
color: var(--maude-chrome-fg-0, #111);
|
|
107
|
+
border: 1px solid var(--maude-chrome-border, rgba(0,0,0,0.12));
|
|
108
108
|
}
|
|
109
|
-
.dc-participant-popover__btn--stop:hover { background: var(--
|
|
109
|
+
.dc-participant-popover__btn--stop:hover { background: color-mix(in oklab, var(--maude-chrome-fg-0, #111) 16%, transparent); }
|
|
110
110
|
@media (prefers-reduced-motion: reduce) {
|
|
111
111
|
.dc-participant { transition: none; }
|
|
112
112
|
}
|