@1agh/maude 0.15.0 → 0.17.1

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.
Files changed (53) hide show
  1. package/README.md +4 -2
  2. package/cli/commands/design.mjs +108 -2
  3. package/package.json +12 -18
  4. package/plugins/design/dev-server/annotations-context-toolbar.tsx +8 -8
  5. package/plugins/design/dev-server/annotations-layer.tsx +8 -10
  6. package/plugins/design/dev-server/api.ts +227 -3
  7. package/plugins/design/dev-server/bin/_enumerate-artboards-playwright.mjs +40 -0
  8. package/plugins/design/dev-server/bin/_html-playwright.mjs +129 -0
  9. package/plugins/design/dev-server/bin/_pdf-playwright.mjs +105 -0
  10. package/plugins/design/dev-server/bin/_png-playwright.mjs +143 -0
  11. package/plugins/design/dev-server/bin/_pptx-playwright.mjs +98 -0
  12. package/plugins/design/dev-server/bin/_svg-playwright.mjs +141 -0
  13. package/plugins/design/dev-server/canvas-lib.tsx +12 -13
  14. package/plugins/design/dev-server/canvas-shell.tsx +111 -9
  15. package/plugins/design/dev-server/client/app.jsx +71 -143
  16. package/plugins/design/dev-server/client/comments-overlay.css +381 -0
  17. package/plugins/design/dev-server/client/styles/3-shell.css +1 -10
  18. package/plugins/design/dev-server/client/styles/4-components.css +5 -161
  19. package/plugins/design/dev-server/client/styles.css +5 -160
  20. package/plugins/design/dev-server/comments-overlay.tsx +1156 -0
  21. package/plugins/design/dev-server/context-menu.tsx +36 -9
  22. package/plugins/design/dev-server/dist/client.bundle.js +52 -211
  23. package/plugins/design/dev-server/dist/styles.css +1 -218
  24. package/plugins/design/dev-server/export-dialog.tsx +401 -0
  25. package/plugins/design/dev-server/exporters/_browser-bundles.ts +89 -0
  26. package/plugins/design/dev-server/exporters/canva-handoff-prompt.ts +74 -0
  27. package/plugins/design/dev-server/exporters/canva.ts +126 -0
  28. package/plugins/design/dev-server/exporters/html.ts +103 -0
  29. package/plugins/design/dev-server/exporters/index.ts +135 -0
  30. package/plugins/design/dev-server/exporters/pdf.ts +109 -0
  31. package/plugins/design/dev-server/exporters/png.ts +136 -0
  32. package/plugins/design/dev-server/exporters/pptx.ts +263 -0
  33. package/plugins/design/dev-server/exporters/scope.ts +196 -0
  34. package/plugins/design/dev-server/exporters/svg.ts +122 -0
  35. package/plugins/design/dev-server/exporters/zip.ts +109 -0
  36. package/plugins/design/dev-server/http.ts +109 -0
  37. package/plugins/design/dev-server/input-router.tsx +21 -0
  38. package/plugins/design/dev-server/inspect.ts +1 -1
  39. package/plugins/design/dev-server/server.mjs +1 -1
  40. package/plugins/design/dev-server/test/canvas-meta-api.test.ts +0 -10
  41. package/plugins/design/dev-server/test/comments-api.test.ts +229 -0
  42. package/plugins/design/dev-server/test/exporters/canva.test.ts +64 -0
  43. package/plugins/design/dev-server/test/exporters/endpoint.test.ts +121 -0
  44. package/plugins/design/dev-server/test/exporters/history.test.ts +79 -0
  45. package/plugins/design/dev-server/test/exporters/html.test.ts +26 -0
  46. package/plugins/design/dev-server/test/exporters/pdf.test.ts +53 -0
  47. package/plugins/design/dev-server/test/exporters/png.test.ts +32 -0
  48. package/plugins/design/dev-server/test/exporters/pptx.test.ts +31 -0
  49. package/plugins/design/dev-server/test/exporters/scope.test.ts +0 -0
  50. package/plugins/design/dev-server/test/exporters/svg.test.ts +29 -0
  51. package/plugins/design/dev-server/test/exporters/zip.test.ts +105 -0
  52. package/plugins/design/dev-server/tool-palette.tsx +34 -16
  53. package/plugins/design/templates/_shell.html +33 -0
@@ -0,0 +1,122 @@
1
+ // Phase 6.5 T4 — SVG adapter.
2
+ //
3
+ // Walks the rendered DOM via Playwright (mirrors the screenshot path) and
4
+ // emits an SVG with <foreignObject>-wrapped HTML + concatenated stylesheets.
5
+ // Web fonts are NOT inlined in v1 — see plan T4 caveat + DDR (Safari +
6
+ // Illustrator render foreignObject inconsistently; vector text from the
7
+ // HTML payload remains editable in Illustrator/Inkscape on the happy path).
8
+
9
+ import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import path from 'node:path';
12
+
13
+ import JSZip from 'jszip';
14
+
15
+ import { getBrowserBundle } from './_browser-bundles.ts';
16
+ import {
17
+ type ExportContext,
18
+ type ExportOptions,
19
+ type ExportResult,
20
+ canvasShellUrl,
21
+ } from './index.ts';
22
+ import type { Target } from './scope.ts';
23
+
24
+ const SVG_PLAYWRIGHT = path.join(import.meta.dir, '..', 'bin', '_svg-playwright.mjs');
25
+
26
+ async function captureSvg(
27
+ target: Extract<Target, { kind: 'element' }>,
28
+ ctx: ExportContext,
29
+ outDir: string,
30
+ timeoutSec: number,
31
+ bundlePath: string
32
+ ): Promise<string[]> {
33
+ const args = [
34
+ SVG_PLAYWRIGHT,
35
+ '--url',
36
+ canvasShellUrl(ctx, target.file),
37
+ '--selector',
38
+ target.cssPath,
39
+ '--bundle-path',
40
+ bundlePath,
41
+ '--timeout',
42
+ String(timeoutSec),
43
+ ];
44
+ if (target.multi) {
45
+ args.push('--multi', '1', '--out-dir', outDir);
46
+ } else {
47
+ args.push('--widen-to-artboard', '1', '--out', path.join(outDir, `${target.canvasSlug}.svg`));
48
+ }
49
+
50
+ // Run via `node` so the shim's `import 'playwright'` resolves against
51
+ // dev-server/node_modules (playwright is a devDep). `npm exec` doesn't
52
+ // bridge the module path for ESM imports — confirmed against npm 10.x.
53
+ const proc = Bun.spawn(['node', ...args], {
54
+ cwd: path.dirname(SVG_PLAYWRIGHT),
55
+ stdout: 'pipe',
56
+ stderr: 'pipe',
57
+ });
58
+ const [stdout, stderr] = await Promise.all([
59
+ new Response(proc.stdout).text(),
60
+ new Response(proc.stderr).text(),
61
+ ]);
62
+ const code = await proc.exited;
63
+ if (code !== 0) {
64
+ throw new Error(`_svg-playwright exited ${code}: ${stderr.trim() || stdout.trim()}`);
65
+ }
66
+ return stdout
67
+ .split('\n')
68
+ .map((s) => s.trim())
69
+ .filter(Boolean);
70
+ }
71
+
72
+ export async function run(
73
+ targets: Target[],
74
+ options: ExportOptions,
75
+ ctx: ExportContext
76
+ ): Promise<ExportResult> {
77
+ if (!targets.length) {
78
+ return { filename: 'export.svg', contentType: 'image/svg+xml', body: new Uint8Array(0) };
79
+ }
80
+ const elementTargets = targets.filter(
81
+ (t): t is Extract<Target, { kind: 'element' }> => t.kind === 'element'
82
+ );
83
+ if (!elementTargets.length) {
84
+ throw new Error('svg adapter requires element targets (got file-tree)');
85
+ }
86
+ const timeoutSec = (options.timeoutSec as number | undefined) ?? 12;
87
+ const bundlePath = await getBrowserBundle('dom-to-svg', 'domToSvg');
88
+ const tmp = mkdtempSync(path.join(tmpdir(), 'maude-svg-'));
89
+
90
+ try {
91
+ const written: string[] = [];
92
+ for (const t of elementTargets) {
93
+ const paths = await captureSvg(t, ctx, tmp, timeoutSec, bundlePath);
94
+ written.push(...paths);
95
+ }
96
+ if (!written.length) {
97
+ return { filename: 'export.svg', contentType: 'image/svg+xml', body: new Uint8Array(0) };
98
+ }
99
+ const entries = written.map((p) => ({
100
+ name: path.basename(p),
101
+ bytes: new Uint8Array(readFileSync(p)),
102
+ }));
103
+ if (entries.length === 1) {
104
+ return {
105
+ filename: entries[0].name,
106
+ contentType: 'image/svg+xml',
107
+ body: entries[0].bytes,
108
+ };
109
+ }
110
+ const zip = new JSZip();
111
+ for (const e of entries) zip.file(e.name, e.bytes);
112
+ const zipBytes = await zip.generateAsync({ type: 'uint8array', compression: 'DEFLATE' });
113
+ const baseSlug = elementTargets[0]?.canvasSlug ?? 'export';
114
+ return {
115
+ filename: `${baseSlug}.svg.zip`,
116
+ contentType: 'application/zip',
117
+ body: zipBytes,
118
+ };
119
+ } finally {
120
+ rmSync(tmp, { recursive: true, force: true });
121
+ }
122
+ }
@@ -0,0 +1,109 @@
1
+ // Phase 6.5 T7 — project-raw ZIP adapter.
2
+ //
3
+ // Consumes one or more file-tree targets (typically just the single one the
4
+ // scope resolver returns for `project-raw`) and streams them into a ZIP.
5
+ // Excludes runtime + node_modules + dist by default; user can override via
6
+ // `options.exclude` (gitignore-style globs) or `options.include` (filter to
7
+ // `'system' | 'canvases' | 'assets' | 'meta'` subtrees).
8
+ //
9
+ // Memory profile: JSZip's `generateAsync({ type: 'uint8array' })` builds the
10
+ // archive in memory. Plan T7 calls for streaming via Bun's Response stream
11
+ // to bound RSS at large designRoots — that's a refinement once a real user
12
+ // hits the buffer ceiling. Until then, the buffered path keeps the contract
13
+ // simple (Uint8Array body matches every other adapter).
14
+
15
+ import { readFileSync } from 'node:fs';
16
+ import path from 'node:path';
17
+
18
+ import JSZip from 'jszip';
19
+
20
+ import type { ExportContext, ExportOptions, ExportResult } from './index.ts';
21
+ import type { Target } from './scope.ts';
22
+
23
+ type IncludeTag = 'system' | 'canvases' | 'assets' | 'meta';
24
+
25
+ function matchesGlob(p: string, glob: string): boolean {
26
+ // Tiny gitignore-style matcher. Supports `*`, `**`, and a trailing slash to
27
+ // mean "directory". Anything fancier (negation, brace expansion) is left to
28
+ // the user — surface in T13 docs.
29
+ const norm = glob.replace(/^\.\//, '').replace(/\/+$/, '');
30
+ if (norm.includes('**')) {
31
+ const re = new RegExp(
32
+ `^${norm
33
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
34
+ .replace(/\*\*/g, '.*')
35
+ .replace(/\*/g, '[^/]*')}(?:/.*)?$`
36
+ );
37
+ return re.test(p);
38
+ }
39
+ if (norm.includes('*')) {
40
+ const re = new RegExp(`^${norm.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*')}$`);
41
+ return re.test(p);
42
+ }
43
+ return p === norm || p.startsWith(`${norm}/`);
44
+ }
45
+
46
+ function isUnder(p: string, prefix: string): boolean {
47
+ return p === prefix || p.startsWith(`${prefix}/`);
48
+ }
49
+
50
+ function filterPaths(paths: string[], options: ExportOptions): string[] {
51
+ const exclude = Array.isArray(options.exclude) ? (options.exclude as string[]) : [];
52
+ const include = Array.isArray(options.include) ? (options.include as IncludeTag[]) : null;
53
+ const tagPrefix: Record<IncludeTag, string> = {
54
+ system: 'system',
55
+ canvases: 'ui',
56
+ assets: 'assets',
57
+ meta: '',
58
+ };
59
+ return paths.filter((p) => {
60
+ if (exclude.some((g) => matchesGlob(p, g))) return false;
61
+ if (include) {
62
+ const hit = include.some((tag) => {
63
+ if (tag === 'meta') {
64
+ return /\.(json|md|css)$/i.test(p) && !p.includes('/');
65
+ }
66
+ return isUnder(p, tagPrefix[tag]);
67
+ });
68
+ if (!hit) return false;
69
+ }
70
+ return true;
71
+ });
72
+ }
73
+
74
+ export async function run(
75
+ targets: Target[],
76
+ options: ExportOptions,
77
+ ctx: ExportContext
78
+ ): Promise<ExportResult> {
79
+ if (!targets.length) {
80
+ return { filename: 'project.zip', contentType: 'application/zip', body: new Uint8Array(0) };
81
+ }
82
+ const fileTreeTargets = targets.filter(
83
+ (t): t is Extract<Target, { kind: 'file-tree' }> => t.kind === 'file-tree'
84
+ );
85
+ if (!fileTreeTargets.length) {
86
+ throw new Error('zip adapter requires file-tree targets (got element)');
87
+ }
88
+
89
+ const allPaths = fileTreeTargets.flatMap((t) => t.paths);
90
+ const kept = filterPaths(allPaths, options);
91
+
92
+ const zip = new JSZip();
93
+ for (const rel of kept) {
94
+ const abs = path.join(ctx.designRoot, rel);
95
+ try {
96
+ const bytes = readFileSync(abs);
97
+ zip.file(rel, new Uint8Array(bytes));
98
+ } catch {
99
+ // File vanished between scope walk and read — skip.
100
+ }
101
+ }
102
+ const bytes = await zip.generateAsync({ type: 'uint8array', compression: 'DEFLATE' });
103
+ const projectName = path.basename(ctx.designRoot).replace(/^\./, '') || 'project';
104
+ return {
105
+ filename: `${projectName}.zip`,
106
+ contentType: 'application/zip',
107
+ body: bytes,
108
+ };
109
+ }
@@ -13,6 +13,8 @@ import { buildCanvasModule } from './canvas-build.ts';
13
13
  import { canvasLibPath } from './canvas-lib-resolver.ts';
14
14
  import { TranspileError } from './canvas-pipeline.ts';
15
15
  import type { Context } from './context.ts';
16
+ import { isFormat, isScope, runExport } from './exporters/index.ts';
17
+ import type { ActiveJsonShape } from './exporters/scope.ts';
16
18
  import type { Inspect } from './inspect.ts';
17
19
  import { canvasSlug, writeLocator } from './locator.ts';
18
20
  import { RUNTIME_PACKAGES, getRuntimeBundle, packageForSlug, slugFor } from './runtime-bundle.ts';
@@ -254,6 +256,15 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect): Http {
254
256
  return new Response('Method not allowed', { status: 405 });
255
257
  },
256
258
 
259
+ '/_api/git-committers': async (req: Request) => {
260
+ // Phase 6 — feed for the @mention autocomplete in composer + reply box.
261
+ // GET → top-20 committers on HEAD (`git shortlog -sne | head -20`)
262
+ // already cached server-side for 60 s.
263
+ if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
264
+ const committers = await api.gitCommitters();
265
+ return Response.json({ committers }, { headers: { 'Cache-Control': 'no-store' } });
266
+ },
267
+
257
268
  '/_api/annotations': async (req: Request) => {
258
269
  // Phase 5 — `<designRoot>/<slug>.annotations.svg` read / overwrite.
259
270
  // GET ?file=<repo-relative-canvas-path> → SVG text (empty if absent)
@@ -286,6 +297,84 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect): Http {
286
297
  return new Response('Method not allowed', { status: 405 });
287
298
  },
288
299
 
300
+ '/_api/export-history': async (req: Request) => {
301
+ // Phase 6.5 T10 — read-only recent-exports feed for the dialog's
302
+ // Recent tab. Writes happen as a side-effect of `/_api/export`.
303
+ if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
304
+ const history = await api.loadExportHistory();
305
+ return Response.json({ history }, { headers: { 'Cache-Control': 'no-store' } });
306
+ },
307
+
308
+ '/_api/export': async (req: Request) => {
309
+ // Phase 6.5 — single dispatch endpoint for the export pipeline.
310
+ // POST body { format, scope, options? } → binary stream with
311
+ // Content-Disposition + Content-Type set by the adapter.
312
+ if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
313
+ const body = await readJson<{
314
+ format?: unknown;
315
+ scope?: unknown;
316
+ options?: Record<string, unknown>;
317
+ }>(req, 64 * 1024);
318
+ if (!body) return new Response('body required', { status: 400 });
319
+ if (!isFormat(body.format)) return new Response('unknown or missing format', { status: 400 });
320
+ if (!isScope(body.scope)) return new Response('unknown or missing scope', { status: 400 });
321
+ // `inspect.state` is the live `_active.json` — readers narrow to the
322
+ // resolver's subset locally so the export pipeline doesn't pin the
323
+ // wider ActiveState interface.
324
+ const activeJson = inspect.state as unknown as ActiveJsonShape;
325
+ try {
326
+ const result = await runExport({
327
+ format: body.format,
328
+ scope: body.scope,
329
+ options: body.options ?? {},
330
+ resolve: { activeJson, designRoot: ctx.paths.designRoot, repoRoot: ctx.paths.repoRoot },
331
+ ctx: {
332
+ designRoot: ctx.paths.designRoot,
333
+ repoRoot: ctx.paths.repoRoot,
334
+ // Adapters reach back into the server via this origin only when
335
+ // they need Playwright rendering (PNG / PDF / SVG / HTML). The
336
+ // host that received this request is, by definition, the one
337
+ // serving the canvas.
338
+ serverOrigin: new URL(req.url).origin,
339
+ // Mirror `client/app.jsx:85` — the per-DS tokensCssRel wins over
340
+ // the legacy top-level default (which still points at the pre-
341
+ // multi-DS layout `system/colors_and_type.css`). Without the
342
+ // per-DS path, the standalone `_canvas-shell.html` 404s on the
343
+ // tokens link and the rendered DOM uses `var(--bg-0)` unresolved
344
+ // → screenshots come out blank. See canvasShellUrl().
345
+ tokensCssRel: ctx.cfg.designSystems?.[0]?.tokensCssRel ?? ctx.cfg.tokensCssRel,
346
+ },
347
+ });
348
+ // Fire-and-forget history append — failure here doesn't block the
349
+ // download. Synchronous await keeps the order: history reflects the
350
+ // export the moment the client sees a 200.
351
+ try {
352
+ await api.appendExportHistory({
353
+ format: body.format,
354
+ scope: body.scope,
355
+ options: body.options ?? {},
356
+ filename: result.filename,
357
+ at: new Date().toISOString(),
358
+ });
359
+ } catch {
360
+ /* ignore — history is best-effort */
361
+ }
362
+ // Bun.serve accepts Uint8Array directly; the cast satisfies the
363
+ // SharedArrayBuffer-strict BodyInit narrowing on @types/bun.
364
+ return new Response(result.body as unknown as BodyInit, {
365
+ status: 200,
366
+ headers: {
367
+ 'Content-Type': result.contentType,
368
+ 'Content-Disposition': `attachment; filename="${result.filename}"`,
369
+ 'Cache-Control': 'no-store',
370
+ },
371
+ });
372
+ } catch (err) {
373
+ const msg = err instanceof Error ? err.message : String(err);
374
+ return new Response(`export failed: ${msg}`, { status: 500 });
375
+ }
376
+ },
377
+
289
378
  '/_canvas-state': async (req: Request) => {
290
379
  const url = new URL(req.url);
291
380
  if (req.method === 'GET') {
@@ -325,6 +414,26 @@ export function createHttp(ctx: Context, api: Api, inspect: Inspect): Http {
325
414
  const url = new URL(req.url);
326
415
  const pathname = url.pathname;
327
416
 
417
+ // Phase 6 — POST /_api/comments/<id>/reply. Dynamic path, so it lives in
418
+ // the fall-through instead of the static `routes` map. `<id>` is the
419
+ // c_<hex> id of the parent comment; body is `{ body, author? }`. Bodies
420
+ // share the same 4000-char cap as a top-level comment.
421
+ const replyMatch = pathname.match(/^\/_api\/comments\/([A-Za-z0-9_]+)\/reply$/);
422
+ if (replyMatch) {
423
+ if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
424
+ const id = replyMatch[1] ?? '';
425
+ const body = await readJson<{ body?: string; author?: string }>(req);
426
+ if (!body || typeof body.body !== 'string' || !body.body.trim()) {
427
+ return new Response('body.body required', { status: 400 });
428
+ }
429
+ const next = await api.commentsAddReply(id, {
430
+ body: body.body,
431
+ author: typeof body.author === 'string' ? body.author : undefined,
432
+ });
433
+ if (!next) return new Response('Not found', { status: 404 });
434
+ return Response.json(next, { headers: { 'Cache-Control': 'no-store' } });
435
+ }
436
+
328
437
  // Bundled client assets (preferred path — bundle from dist/).
329
438
  if (pathname.startsWith('/_client/')) {
330
439
  const rel = decodeURIComponent(pathname.slice('/_client/'.length));
@@ -240,6 +240,21 @@ export function isEditableTarget(t: EventTarget | null): boolean {
240
240
  return false;
241
241
  }
242
242
 
243
+ /**
244
+ * Phase 6 — the comments overlay (pins / composer / thread popover / mention
245
+ * popup) lives INSIDE the canvas world, which means its DOM nodes are inside
246
+ * the input-router's capture host. Without an explicit bail-out the router
247
+ * would `preventDefault + stopImmediatePropagation` every click on a
248
+ * composer button while comment mode is active, blocking Save / Cancel.
249
+ *
250
+ * We treat the overlay nodes like editable form widgets — the router yields,
251
+ * the React event handler runs.
252
+ */
253
+ export function isOverlayTarget(t: EventTarget | null): boolean {
254
+ if (!t || !(t as Element).closest) return false;
255
+ return !!(t as Element).closest('.cm-composer, .cm-thread, .cm-mention-popup, .cm-pin');
256
+ }
257
+
243
258
  export function useInputRouter(opts: UseInputRouterOptions): void {
244
259
  const { hostRef, getActiveTool, isSpaceHeld, callbacks, enabled = true } = opts;
245
260
 
@@ -290,6 +305,10 @@ export function useInputRouter(opts: UseInputRouterOptions): void {
290
305
  };
291
306
 
292
307
  const onPointerDown = (e: PointerEvent): void => {
308
+ // Phase 6 — overlay surfaces (composer / thread / mention popup) own
309
+ // their own clicks. The router is in capture phase, so we have to
310
+ // bail HERE before classify can claim the event.
311
+ if (isOverlayTarget(e.target)) return;
293
312
  const action = classify({
294
313
  type: 'pointerdown',
295
314
  button: e.button,
@@ -321,6 +340,7 @@ export function useInputRouter(opts: UseInputRouterOptions): void {
321
340
  * stop their twin mousedown.
322
341
  */
323
342
  const onMouseDown = (e: MouseEvent): void => {
343
+ if (isOverlayTarget(e.target)) return;
324
344
  const action = classify({
325
345
  type: 'pointerdown',
326
346
  button: e.button,
@@ -346,6 +366,7 @@ export function useInputRouter(opts: UseInputRouterOptions): void {
346
366
  * matching pointerdown (re-classify with the same modifiers).
347
367
  */
348
368
  const onClick = (e: MouseEvent): void => {
369
+ if (isOverlayTarget(e.target)) return;
349
370
  const tool = getActiveTool();
350
371
  const mod = e.metaKey || e.ctrlKey;
351
372
  const wouldRoute =
@@ -227,7 +227,7 @@ const INSPECTOR_SCRIPT = `
227
227
 
228
228
  var styleEl = document.createElement('style');
229
229
  styleEl.textContent = [
230
- '.dgn-pin { position: absolute; top: 0; left: 0; z-index: 2147483646; width: 22px; height: 22px; padding: 0; border: 0; border-radius: 999px 999px 999px 4px; background: #facc15; color: #1c1917; font: 600 11px/22px ui-sans-serif, system-ui, sans-serif; text-align: center; cursor: pointer; box-shadow: 0 2px 6px rgba(0,0,0,0.5), 0 0 0 1px rgba(0,0,0,0.4); transition: filter 120ms; transform-origin: bottom left; will-change: transform; }',
230
+ '.dgn-pin { position: absolute; top: 0; left: 0; z-index: 2147483646; width: 22px; height: 22px; padding: 0; border: 0; border-radius: 999px 999px 999px 4px; background: #facc15; color: #1c1917; font: 600 11px/22px var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); text-align: center; cursor: pointer; box-shadow: 0 2px 6px rgba(0,0,0,0.5), 0 0 0 1px rgba(0,0,0,0.4); transition: filter 120ms; transform-origin: bottom left; will-change: transform; }',
231
231
  '.dgn-pin:hover { filter: brightness(1.1); outline: 2px solid rgba(0,0,0,0.3); }',
232
232
  '.dgn-pin.resolved { background: #22c55e; color: #052e16; }',
233
233
  '.dgn-pin.focused { box-shadow: 0 4px 12px rgba(0,0,0,0.6), 0 0 0 2px #fff; outline: 2px solid #fff; }'
@@ -573,7 +573,7 @@ const INSPECTOR_SCRIPT = `
573
573
  '.dgn-insp-selected { outline: 2px solid #00D4E4 !important; outline-offset: 1px !important; box-shadow: 0 0 0 4px rgba(0,212,228,0.18) !important; }',
574
574
  '.dgn-insp-label { position: fixed; z-index: 2147483647; font: 11px/1 ui-monospace,SFMono-Regular,Menlo,monospace; background: #00D4E4; color: #000; padding: 4px 8px; border-radius: 4px; pointer-events: none; box-shadow: 0 2px 8px rgba(0,0,0,0.4); transform: translate(0, -110%); white-space: nowrap; max-width: 320px; overflow: hidden; text-overflow: ellipsis; }',
575
575
  '.dgn-insp-label.warn { background: #ef4444; color: #fff; }',
576
- '.dgn-pin { position: absolute; top: 0; left: 0; z-index: 2147483646; width: 22px; height: 22px; padding: 0; border: 0; border-radius: 999px 999px 999px 4px; background: #facc15; color: #1c1917; font: 600 11px/22px ui-sans-serif, system-ui, sans-serif; text-align: center; cursor: pointer; box-shadow: 0 2px 6px rgba(0,0,0,0.5), 0 0 0 1px rgba(0,0,0,0.4); transition: filter 120ms; transform-origin: bottom left; will-change: transform; }',
576
+ '.dgn-pin { position: absolute; top: 0; left: 0; z-index: 2147483646; width: 22px; height: 22px; padding: 0; border: 0; border-radius: 999px 999px 999px 4px; background: #facc15; color: #1c1917; font: 600 11px/22px var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); text-align: center; cursor: pointer; box-shadow: 0 2px 6px rgba(0,0,0,0.5), 0 0 0 1px rgba(0,0,0,0.4); transition: filter 120ms; transform-origin: bottom left; will-change: transform; }',
577
577
  '.dgn-pin:hover { filter: brightness(1.1); outline: 2px solid rgba(0,0,0,0.3); }',
578
578
  '.dgn-pin.resolved { background: #22c55e; color: #052e16; }',
579
579
  '.dgn-pin.focused { box-shadow: 0 4px 12px rgba(0,0,0,0.6), 0 0 0 2px #fff; outline: 2px solid #fff; }'
@@ -24,16 +24,6 @@ interface MetaShape {
24
24
  [k: string]: unknown;
25
25
  }
26
26
 
27
- function seedCanvas(designRoot: string, name = 'Phase4.tsx', meta?: MetaShape): string {
28
- const ui = join(designRoot, 'ui');
29
- mkdirSync(ui, { recursive: true });
30
- const tsxPath = join(ui, name);
31
- writeFileSync(tsxPath, 'export default function P(){return <main/>}\n');
32
- const metaPath = tsxPath.replace(/\.tsx$/, '.meta.json');
33
- if (meta) writeFileSync(metaPath, JSON.stringify(meta, null, 2));
34
- return tsxPath.replace(`${designRoot.replace(/\.design$/, '')}`, '').replace(/^\/+/, '');
35
- }
36
-
37
27
  function repoRel(designRoot: string, abs: string): string {
38
28
  // designRoot ends in `.design`. repoRoot is its parent.
39
29
  const repoRoot = designRoot.replace(/\.design$/, '').replace(/\/+$/, '');