@1agh/maude 0.16.0 → 0.17.2

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 (48) hide show
  1. package/README.md +4 -0
  2. package/cli/cli-wrapper.cjs +0 -0
  3. package/cli/commands/design.mjs +264 -16
  4. package/package.json +12 -18
  5. package/plugins/design/dev-server/annotations-context-toolbar.tsx +8 -8
  6. package/plugins/design/dev-server/annotations-layer.tsx +8 -10
  7. package/plugins/design/dev-server/api.ts +41 -0
  8. package/plugins/design/dev-server/bin/_enumerate-artboards-playwright.mjs +40 -0
  9. package/plugins/design/dev-server/bin/_html-playwright.mjs +129 -0
  10. package/plugins/design/dev-server/bin/_pdf-playwright.mjs +105 -0
  11. package/plugins/design/dev-server/bin/_png-playwright.mjs +143 -0
  12. package/plugins/design/dev-server/bin/_pptx-playwright.mjs +98 -0
  13. package/plugins/design/dev-server/bin/_svg-playwright.mjs +141 -0
  14. package/plugins/design/dev-server/build.ts +118 -6
  15. package/plugins/design/dev-server/canvas-lib.tsx +12 -13
  16. package/plugins/design/dev-server/canvas-pipeline.ts +5 -0
  17. package/plugins/design/dev-server/canvas-shell.tsx +32 -4
  18. package/plugins/design/dev-server/client/app.jsx +18 -1
  19. package/plugins/design/dev-server/context-menu.tsx +36 -9
  20. package/plugins/design/dev-server/dist/client.bundle.js +11 -3
  21. package/plugins/design/dev-server/export-dialog.tsx +401 -0
  22. package/plugins/design/dev-server/exporters/_browser-bundles.ts +89 -0
  23. package/plugins/design/dev-server/exporters/canva-handoff-prompt.ts +74 -0
  24. package/plugins/design/dev-server/exporters/canva.ts +126 -0
  25. package/plugins/design/dev-server/exporters/html.ts +103 -0
  26. package/plugins/design/dev-server/exporters/index.ts +135 -0
  27. package/plugins/design/dev-server/exporters/pdf.ts +109 -0
  28. package/plugins/design/dev-server/exporters/png.ts +136 -0
  29. package/plugins/design/dev-server/exporters/pptx.ts +263 -0
  30. package/plugins/design/dev-server/exporters/scope.ts +196 -0
  31. package/plugins/design/dev-server/exporters/svg.ts +122 -0
  32. package/plugins/design/dev-server/exporters/zip.ts +109 -0
  33. package/plugins/design/dev-server/http.ts +80 -0
  34. package/plugins/design/dev-server/inspect.ts +1 -1
  35. package/plugins/design/dev-server/server.mjs +1 -1
  36. package/plugins/design/dev-server/test/compile-entry.test.ts +134 -0
  37. package/plugins/design/dev-server/test/exporters/canva.test.ts +64 -0
  38. package/plugins/design/dev-server/test/exporters/endpoint.test.ts +121 -0
  39. package/plugins/design/dev-server/test/exporters/history.test.ts +79 -0
  40. package/plugins/design/dev-server/test/exporters/html.test.ts +26 -0
  41. package/plugins/design/dev-server/test/exporters/pdf.test.ts +53 -0
  42. package/plugins/design/dev-server/test/exporters/png.test.ts +32 -0
  43. package/plugins/design/dev-server/test/exporters/pptx.test.ts +31 -0
  44. package/plugins/design/dev-server/test/exporters/scope.test.ts +0 -0
  45. package/plugins/design/dev-server/test/exporters/svg.test.ts +29 -0
  46. package/plugins/design/dev-server/test/exporters/zip.test.ts +105 -0
  47. package/plugins/design/dev-server/tool-palette.tsx +34 -16
  48. package/plugins/design/templates/_shell.html +33 -0
package/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  A personal marketplace of Claude Code plugins. Two plugins today, plus a `maude` CLI for scaffolding and running the bundled dev tooling.
4
4
 
5
+ <!-- Demo video lives at https://github.com/1aGh/maude/releases/download/v0.16.0/demo.mp4 once uploaded.
6
+ Until then the inline tag below stays a soft 404; landing page at https://maude.iagh.cz autoplays the same file. -->
7
+ <video src="https://github.com/1aGh/maude/releases/download/v0.16.0/demo.mp4" controls muted playsinline poster="https://maude.iagh.cz/demo-poster.jpg" width="800"></video>
8
+
5
9
  > **📚 Full docs: https://maude.iagh.cz** (or browse the source under [`site/content/docs/`](./site/content/docs/) until the public URL lands).
6
10
  > Contributing? See [CONTRIBUTING.md](./CONTRIBUTING.md). Security? See [SECURITY.md](./SECURITY.md).
7
11
 
File without changes
@@ -1,9 +1,10 @@
1
- import { spawn } from 'node:child_process';
1
+ import { execSync, spawn } from 'node:child_process';
2
2
  import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
3
- import { basename, join, resolve } from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import { basename, dirname, join, resolve } from 'node:path';
4
5
  import { parseArgs } from '../lib/argv.mjs';
5
6
 
6
- const SUBCOMMANDS = new Set(['serve', 'init', 'help']);
7
+ const SUBCOMMANDS = new Set(['serve', 'init', 'export', 'help']);
7
8
 
8
9
  export async function run({ args, pkgRoot }) {
9
10
  const { positional } = parseArgs(args);
@@ -25,10 +26,13 @@ export async function run({ args, pkgRoot }) {
25
26
  if (sub === 'init') {
26
27
  return runInit({ args, pkgRoot });
27
28
  }
29
+ if (sub === 'export') {
30
+ return runExport({ args });
31
+ }
28
32
  }
29
33
 
30
34
  function usage() {
31
- return `maude design <serve|init> [options]
35
+ return `maude design <serve|init|export> [options]
32
36
 
33
37
  serve [--port N] [--root PATH]
34
38
  Start the design plugin's dev server in the current repo. Equivalent
@@ -45,28 +49,54 @@ function usage() {
45
49
  --discovery-payload <path> reads a JSON file with answers + tokens and
46
50
  scaffolds Core + the derived specimens deterministically (this is the
47
51
  path skill 'design-system' uses when shelling out from Claude Code).
52
+
53
+ export <format> [--scope selection|artboard|canvas-as-separate|project-raw]
54
+ [--port N] [--out <path>] [--option key=value ...]
55
+ Drive the same POST /_api/export endpoint the UI uses. Auto-detects
56
+ port from .design/_server.json; requires a running dev server. The
57
+ response body is written to --out (default: current dir, server-
58
+ supplied filename). Formats: png pdf svg html pptx canva zip.
48
59
  `;
49
60
  }
50
61
 
51
62
  async function runServe({ args, pkgRoot }) {
52
63
  const forwarded = args.slice(args.indexOf('serve') + 1);
64
+ const fs = await import('node:fs');
53
65
 
54
66
  // Resolution order:
55
- // 1. Side-channel from postinstall (cli/.platform-binary-path) — direct
56
- // native exec, zero Node startup tax. DDR-015.
57
- // 2. Bun + server.ts (local dev or postinstall-skipped install).
58
- // 3. Legacy server.mjs on Node last-resort fallback for boxes without
59
- // bun. Should only fire mid-migration; removed in v1.0.
67
+ // 1. Side-channel cache from postinstall (cli/.platform-binary-path).
68
+ // 2. Lazy resolve of @1agh/maude-<slug>/maude (postinstall was skipped
69
+ // Bun global, --ignore-scripts, Docker layer without scripts, etc.).
70
+ // 3. (only in local dev tree) Bun + server.ts from source.
71
+ // 4. Hard-fail with actionable hint.
72
+ // Maintainers hacking on the dev-server source can force #3 with
73
+ // MAUDE_FORCE_SOURCE=1.
74
+ const forceSource = process.env.MAUDE_FORCE_SOURCE === '1';
60
75
  const sideChannel = resolve(pkgRoot, 'cli', '.platform-binary-path');
61
76
  let binPath = null;
62
- try {
63
- const { readFileSync, existsSync } = await import('node:fs');
64
- if (existsSync(sideChannel)) {
65
- const candidate = readFileSync(sideChannel, 'utf8').trim();
66
- if (candidate && existsSync(candidate)) binPath = candidate;
77
+
78
+ if (!forceSource) {
79
+ try {
80
+ if (fs.existsSync(sideChannel)) {
81
+ const candidate = fs.readFileSync(sideChannel, 'utf8').trim();
82
+ if (candidate && fs.existsSync(candidate)) binPath = candidate;
83
+ }
84
+ } catch {
85
+ /* fall through to lazy resolve */
86
+ }
87
+
88
+ if (!binPath) {
89
+ const resolved = lazyResolveBinary({ pkgRoot, fs });
90
+ if (resolved.binPath) {
91
+ binPath = resolved.binPath;
92
+ // Cache for next invocation (best-effort — read-only fs is fine).
93
+ try {
94
+ fs.writeFileSync(sideChannel, binPath, 'utf8');
95
+ } catch {
96
+ /* read-only fs / no permission — non-fatal */
97
+ }
98
+ }
67
99
  }
68
- } catch {
69
- /* fall through */
70
100
  }
71
101
 
72
102
  if (binPath) {
@@ -79,6 +109,30 @@ async function runServe({ args, pkgRoot }) {
79
109
  return;
80
110
  }
81
111
 
112
+ // No binary found. Two possibilities:
113
+ // - Production install missing platform package — hard-fail with guidance.
114
+ // - Local dev tree (claude-design checkout) — fall through to source.
115
+ const inDevTree = isLocalDevTree(pkgRoot, fs);
116
+
117
+ if (!inDevTree && !forceSource) {
118
+ const slug = detectPlatformSlug();
119
+ const siblingHint = slug ? resolve(pkgRoot, '..', `maude-${slug}`) : '(unknown platform)';
120
+ process.stderr.write(
121
+ `maude design serve: platform binary not found.\n\n Expected: @1agh/maude-${slug || '<platform>'}/maude\n Looked in: ${siblingHint}\n\nLikely causes:\n • Installer skipped postinstall (Bun global, npm --ignore-scripts,\n pnpm strict-scripts, Docker layer rebuilds).\n • Optional dependency for your platform did not install.\n • Global 'maude' is a leftover 'npm link' to a source checkout.\n\nFix (clean reinstall):\n cd ~ # NOT inside a maude source repo\n npm uninstall -g @1agh/maude # remove any stale link\n npm i -g @1agh/maude # real tarball install + postinstall\n\nOr re-run postinstall on the existing install:\n npm rebuild -g @1agh/maude\n\nPlatform: ${process.platform}-${process.arch}\n`
122
+ );
123
+ process.exit(1);
124
+ }
125
+
126
+ // Local dev tree (or forced source). Verify deps before invoking source —
127
+ // catches the npm/pnpm/oxc-parser native-binding bug (npm#4828) up front.
128
+ const missing = checkDevDeps({ pkgRoot });
129
+ if (missing.length) {
130
+ process.stderr.write(
131
+ `maude design serve: missing dev-server dependencies in local checkout.\n\n Missing: ${missing.join(', ')}\n Repo: ${pkgRoot}\n\nThis repo uses pnpm (see packageManager in package.json). Install deps:\n cd ${pkgRoot} && pnpm install\n\nIf you already ran 'npm install' here, the npm optional-deps bug\n(npm#4828) may have left native bindings broken. Reset first:\n rm -rf node_modules package-lock.json && pnpm install\n`
132
+ );
133
+ process.exit(1);
134
+ }
135
+
82
136
  const tsEntry = resolve(pkgRoot, 'plugins', 'design', 'dev-server', 'server.ts');
83
137
  const mjsEntry = resolve(pkgRoot, 'plugins', 'design', 'dev-server', 'server.mjs');
84
138
 
@@ -98,6 +152,200 @@ async function runServe({ args, pkgRoot }) {
98
152
  });
99
153
  }
100
154
 
155
+ // Mirrors cli/install.cjs:detectSlug — kept in sync intentionally. Both forms
156
+ // run during the lifecycle of a single install (postinstall + first serve).
157
+ function detectPlatformSlug() {
158
+ const p = process.platform;
159
+ const a = process.arch;
160
+ if (p === 'darwin') {
161
+ if (a === 'arm64') return 'darwin-arm64';
162
+ try {
163
+ const t = execSync('sysctl -n sysctl.proc_translated', {
164
+ stdio: ['ignore', 'pipe', 'ignore'],
165
+ })
166
+ .toString()
167
+ .trim();
168
+ if (t === '1') return 'darwin-arm64';
169
+ } catch {
170
+ /* not under Rosetta */
171
+ }
172
+ return 'darwin-x64';
173
+ }
174
+ if (p === 'linux') {
175
+ let isMusl = false;
176
+ try {
177
+ const report = process.report?.getReport?.();
178
+ isMusl = !report?.header?.glibcVersionRuntime;
179
+ } catch {
180
+ /* default to glibc */
181
+ }
182
+ if (a === 'arm64') return isMusl ? 'linux-arm64-musl' : 'linux-arm64';
183
+ return isMusl ? 'linux-x64-musl' : 'linux-x64';
184
+ }
185
+ if (p === 'win32') return 'win32-x64';
186
+ return null;
187
+ }
188
+
189
+ function lazyResolveBinary({ pkgRoot, fs }) {
190
+ const slug = detectPlatformSlug();
191
+ if (!slug) return { binPath: null, slug: null };
192
+ const filename = process.platform === 'win32' ? 'maude.exe' : 'maude';
193
+
194
+ // Flat layout — production npm/bun global install. pkgRoot is
195
+ // <node_modules>/@1agh/maude, sibling is <node_modules>/@1agh/maude-<slug>.
196
+ const sibling = resolve(pkgRoot, '..', `maude-${slug}`, filename);
197
+ if (fs.existsSync(sibling)) {
198
+ try {
199
+ fs.chmodSync(sibling, 0o755);
200
+ } catch {
201
+ /* read-only — ignore */
202
+ }
203
+ return { binPath: sibling, slug };
204
+ }
205
+
206
+ // Nested layout — pnpm with isolation, or dev tree's own node_modules.
207
+ try {
208
+ const require = createRequire(import.meta.url);
209
+ const manifest = require.resolve(`@1agh/maude-${slug}/package.json`, {
210
+ paths: [pkgRoot, resolve(pkgRoot, 'cli')],
211
+ });
212
+ const fromManifest = resolve(dirname(manifest), filename);
213
+ if (fs.existsSync(fromManifest)) {
214
+ try {
215
+ fs.chmodSync(fromManifest, 0o755);
216
+ } catch {
217
+ /* ignore */
218
+ }
219
+ return { binPath: fromManifest, slug };
220
+ }
221
+ } catch {
222
+ /* platform package not installed */
223
+ }
224
+
225
+ return { binPath: null, slug };
226
+ }
227
+
228
+ // Mirrors cli/install.cjs:isLocalDev — the `packages/` directory is only present
229
+ // in the source checkout, never in a published npm tarball (excluded from
230
+ // package.json:files).
231
+ function isLocalDevTree(pkgRoot, fs) {
232
+ return fs.existsSync(resolve(pkgRoot, 'packages', 'maude-darwin-arm64', 'package.json'));
233
+ }
234
+
235
+ function checkDevDeps({ pkgRoot }) {
236
+ const require = createRequire(import.meta.url);
237
+ // The dev-server source path imports these as bare specifiers. If they
238
+ // resolve, bun/node will load them; if they don't, the spawn will fail
239
+ // with a less actionable error.
240
+ const required = ['magic-string', 'oxc-parser'];
241
+ const missing = [];
242
+ const paths = [resolve(pkgRoot, 'plugins', 'design', 'dev-server'), resolve(pkgRoot)];
243
+ for (const dep of required) {
244
+ try {
245
+ require.resolve(dep, { paths });
246
+ } catch {
247
+ missing.push(dep);
248
+ }
249
+ }
250
+ return missing;
251
+ }
252
+
253
+ async function runExport({ args }) {
254
+ // `maude design export <format> [--scope ...] [--port N] [--out <path>] [--option key=value]`
255
+ const subArgs = args.slice(args.indexOf('export') + 1);
256
+ const { positional, flags } = parseArgs(subArgs, {
257
+ booleans: ['help'],
258
+ });
259
+ if (flags.help) {
260
+ process.stdout.write(usage());
261
+ return;
262
+ }
263
+
264
+ const format = positional[0];
265
+ const VALID_FORMATS = new Set(['png', 'pdf', 'svg', 'html', 'pptx', 'canva', 'zip']);
266
+ if (!format || !VALID_FORMATS.has(format)) {
267
+ process.stderr.write(
268
+ `maude design export: missing or unknown <format>. Try one of: ${Array.from(VALID_FORMATS).join(', ')}\n`
269
+ );
270
+ process.exit(2);
271
+ }
272
+
273
+ const scope = flags.scope ?? 'canvas-as-separate';
274
+ const VALID_SCOPES = new Set(['selection', 'artboard', 'canvas-as-separate', 'project-raw']);
275
+ if (!VALID_SCOPES.has(scope)) {
276
+ process.stderr.write(`maude design export: unknown --scope "${scope}"\n`);
277
+ process.exit(2);
278
+ }
279
+
280
+ // Resolve port: --port > .design/_server.json
281
+ let port = flags.port ? Number(flags.port) : null;
282
+ if (!port) {
283
+ try {
284
+ const raw = await readFile(resolve(process.cwd(), '.design', '_server.json'), 'utf8');
285
+ port = JSON.parse(raw).port;
286
+ } catch {
287
+ /* no server.json — handled below */
288
+ }
289
+ }
290
+ if (!port) {
291
+ process.stderr.write(
292
+ 'maude design export: no --port given and .design/_server.json not found. Start the dev server first (`maude design serve`).\n'
293
+ );
294
+ process.exit(1);
295
+ }
296
+
297
+ // Collect `--option key=value` repeated flags into an object.
298
+ const options = {};
299
+ const repeated = collectRepeatedFlag(subArgs, '--option');
300
+ for (const item of repeated) {
301
+ const eq = item.indexOf('=');
302
+ if (eq < 0) {
303
+ process.stderr.write(
304
+ `maude design export: invalid --option "${item}" (expected key=value)\n`
305
+ );
306
+ process.exit(2);
307
+ }
308
+ const key = item.slice(0, eq);
309
+ const value = item.slice(eq + 1);
310
+ // Coerce common JSON-ish values: true/false, numbers, arrays via comma.
311
+ options[key] = value === 'true' ? true : value === 'false' ? false : value;
312
+ }
313
+
314
+ const url = `http://localhost:${port}/_api/export`;
315
+ const r = await fetch(url, {
316
+ method: 'POST',
317
+ headers: { 'content-type': 'application/json' },
318
+ body: JSON.stringify({ format, scope, options }),
319
+ });
320
+ if (!r.ok) {
321
+ const text = await r.text();
322
+ process.stderr.write(`maude design export: server returned ${r.status}: ${text}\n`);
323
+ process.exit(1);
324
+ }
325
+
326
+ const disp = r.headers.get('content-disposition') ?? '';
327
+ const serverFilename = /filename="([^"]+)"/.exec(disp)?.[1] ?? `export.${format}`;
328
+ const outPath = flags.out
329
+ ? resolve(process.cwd(), flags.out)
330
+ : resolve(process.cwd(), serverFilename);
331
+ const bytes = new Uint8Array(await r.arrayBuffer());
332
+ await writeFile(outPath, bytes);
333
+ process.stdout.write(`maude design export: wrote ${outPath} (${bytes.byteLength} bytes)\n`);
334
+ }
335
+
336
+ function collectRepeatedFlag(argv, name) {
337
+ const out = [];
338
+ for (let i = 0; i < argv.length; i += 1) {
339
+ if (argv[i] === name && argv[i + 1] !== undefined) {
340
+ out.push(argv[i + 1]);
341
+ i += 1;
342
+ } else if (argv[i].startsWith(`${name}=`)) {
343
+ out.push(argv[i].slice(name.length + 1));
344
+ }
345
+ }
346
+ return out;
347
+ }
348
+
101
349
  async function runInit({ args, pkgRoot }) {
102
350
  const subArgs = args.slice(args.indexOf('init') + 1);
103
351
  const { flags } = parseArgs(subArgs, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1agh/maude",
3
- "version": "0.16.0",
3
+ "version": "0.17.2",
4
4
  "description": "Marketplace of Claude Code plugins by Michal Dovrtěl: `design` (canvas-first design iteration) + `flow` (generic agentic workflow loop with .ai second brain). Ships the `maude` CLI (with `mdcc` legacy alias) to scaffold workspace, run the design dev server, and manage configs.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -34,18 +34,20 @@
34
34
  "video:smoke": "bash scripts/video/smoke/run.sh",
35
35
  "video:smoke:terminal": "vhs scripts/video/smoke/terminal.tape",
36
36
  "video:smoke:browser": "playwright test --config scripts/video/smoke/playwright.config.ts",
37
- "video:smoke:card": "remotion render scripts/video/smoke/card/index.tsx SmokeCard scripts/video/.work/smoke/card.mp4 --mute",
37
+ "video:smoke:card": "cd scripts/video/final && pnpm run render SmokeCard ../.work/smoke/card.mp4 --mute",
38
+ "video:render": "cd scripts/video/final && pnpm run render",
39
+ "video:studio": "cd scripts/video/final && pnpm run studio",
38
40
  "postinstall": "node cli/install.cjs",
39
41
  "prepublishOnly": "bash scripts/check-version-parity.sh"
40
42
  },
41
43
  "optionalDependencies": {
42
- "@1agh/maude-darwin-arm64": "0.16.0",
43
- "@1agh/maude-darwin-x64": "0.16.0",
44
- "@1agh/maude-linux-arm64": "0.16.0",
45
- "@1agh/maude-linux-arm64-musl": "0.16.0",
46
- "@1agh/maude-linux-x64": "0.16.0",
47
- "@1agh/maude-linux-x64-musl": "0.16.0",
48
- "@1agh/maude-win32-x64": "0.16.0"
44
+ "@1agh/maude-darwin-arm64": "0.17.2",
45
+ "@1agh/maude-darwin-x64": "0.17.2",
46
+ "@1agh/maude-linux-arm64": "0.17.2",
47
+ "@1agh/maude-linux-arm64-musl": "0.17.2",
48
+ "@1agh/maude-linux-x64": "0.17.2",
49
+ "@1agh/maude-linux-x64-musl": "0.17.2",
50
+ "@1agh/maude-win32-x64": "0.17.2"
49
51
  },
50
52
  "files": [
51
53
  "cli",
@@ -81,14 +83,6 @@
81
83
  "devDependencies": {
82
84
  "@biomejs/biome": "^1.9.4",
83
85
  "@changesets/cli": "^2.27.10",
84
- "@playwright/test": "^1.60.0",
85
- "@remotion/bundler": "^4.0.463",
86
- "@remotion/cli": "^4.0.463",
87
- "@remotion/renderer": "^4.0.463",
88
- "@types/react": "^19.2.14",
89
- "@types/react-dom": "^19.2.3",
90
- "react": "^19.2.6",
91
- "react-dom": "^19.2.6",
92
- "remotion": "^4.0.463"
86
+ "@playwright/test": "^1.60.0"
93
87
  }
94
88
  }
@@ -36,14 +36,14 @@ const TOOLBAR_CSS = `
36
36
  display: flex;
37
37
  align-items: center;
38
38
  gap: 6px;
39
- background: var(--bg-1, rgba(255,255,255,0.98));
40
- border: 1px solid var(--u-border-2, rgba(0,0,0,0.08));
41
- border-radius: 8px;
39
+ background: var(--u-bg-2, var(--bg-1, rgba(255,255,255,0.98)));
40
+ border: 1px solid var(--u-fg-0, #1c1917);
41
+ border-radius: 0;
42
42
  padding: 6px 8px;
43
- box-shadow: 0 8px 24px rgba(0,0,0,0.10);
44
- font-family: ui-sans-serif, system-ui, sans-serif;
43
+ box-shadow: 4px 4px 0 var(--u-fg-0, #1c1917);
44
+ font-family: var(--u-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
45
45
  font-size: 12px;
46
- color: var(--fg-0, #1a1a1a);
46
+ color: var(--u-fg-0, var(--fg-0, #1a1a1a));
47
47
  user-select: none;
48
48
  pointer-events: auto;
49
49
  }
@@ -79,8 +79,8 @@ const TOOLBAR_CSS = `
79
79
  .dc-annot-ctx-btn {
80
80
  appearance: none;
81
81
  background: transparent;
82
- border: 1px solid rgba(0,0,0,0.12);
83
- border-radius: 4px;
82
+ border: 1px solid var(--u-fg-0, rgba(0,0,0,0.6));
83
+ border-radius: 0;
84
84
  padding: 3px 8px;
85
85
  font: inherit;
86
86
  color: inherit;
@@ -118,9 +118,7 @@ const STROKE_WIDTH_THIN = 2;
118
118
  const STROKE_WIDTH_THICK = 6;
119
119
  type Thickness = typeof STROKE_WIDTH_THIN | typeof STROKE_WIDTH_THICK;
120
120
 
121
- const FONT_SIZE_SMALL = 12;
122
121
  const FONT_SIZE_MEDIUM = 14;
123
- const FONT_SIZE_LARGE = 20;
124
122
  const DEFAULT_FONT_SIZE = FONT_SIZE_MEDIUM;
125
123
 
126
124
  // ─────────────────────────────────────────────────────────────────────────────
@@ -459,15 +457,15 @@ const ANNOT_CSS = `
459
457
  display: flex;
460
458
  align-items: center;
461
459
  gap: 8px;
462
- background: var(--bg-1, rgba(255,255,255,0.98));
463
- border: 1px solid var(--u-border-2, rgba(0,0,0,0.08));
464
- border-radius: 8px;
460
+ background: var(--u-bg-2, var(--bg-1, rgba(255,255,255,0.98)));
461
+ border: 1px solid var(--u-fg-0, #1c1917);
462
+ border-radius: 0;
465
463
  padding: 6px 10px;
466
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
464
+ font-family: var(--u-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
467
465
  font-size: 11px;
468
- color: rgba(40,30,20,0.85);
466
+ color: var(--u-fg-0, rgba(40,30,20,0.85));
469
467
  z-index: 6;
470
- box-shadow: 0 6px 24px rgba(0,0,0,0.08);
468
+ box-shadow: 4px 4px 0 var(--u-fg-0, #1c1917);
471
469
  user-select: none;
472
470
  }
473
471
  .dc-annot-chrome .dc-annot-swatches { display: flex; gap: 4px; }
@@ -1481,7 +1479,7 @@ function TextEditor({
1481
1479
  textAlign: 'center',
1482
1480
  color: existing?.color ?? '#1a1a1a',
1483
1481
  fontSize: `${fontSize}px`,
1484
- fontFamily: 'ui-sans-serif, system-ui, sans-serif',
1482
+ fontFamily: 'var(--u-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace)',
1485
1483
  lineHeight: 1.25,
1486
1484
  outline: 'none',
1487
1485
  background: 'transparent',
@@ -1568,7 +1566,7 @@ function StrokeNode({
1568
1566
  fontSize={stroke.fontSize}
1569
1567
  textAnchor="middle"
1570
1568
  dominantBaseline="middle"
1571
- style={{ fontFamily: 'ui-sans-serif, system-ui, sans-serif' }}
1569
+ style={{ fontFamily: 'var(--u-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace)' }}
1572
1570
  >
1573
1571
  {stroke.text}
1574
1572
  </text>
@@ -119,6 +119,16 @@ export interface GitCommitter {
119
119
  commits: number;
120
120
  }
121
121
 
122
+ // Phase 6.5 T10 — export history. Five-deep ring buffer of recent exports
123
+ // surfaced by the dialog's "Recent" tab + ⌘⇧E re-run.
124
+ export interface ExportHistoryEntry {
125
+ format: string;
126
+ scope: string;
127
+ options?: Record<string, unknown>;
128
+ filename: string;
129
+ at: string;
130
+ }
131
+
122
132
  export interface Api {
123
133
  // File tree
124
134
  fileSlug(file: string): string;
@@ -146,6 +156,9 @@ export interface Api {
146
156
  // Aggregate data
147
157
  buildIndexData(): Promise<unknown>;
148
158
  buildSystemData(): Promise<unknown>;
159
+ // Export history (Phase 6.5 T10)
160
+ loadExportHistory(): Promise<ExportHistoryEntry[]>;
161
+ appendExportHistory(entry: ExportHistoryEntry): Promise<void>;
149
162
  }
150
163
 
151
164
  export function createApi(ctx: Context, onCommentsChanged: (file: string) => void): Api {
@@ -694,6 +707,32 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
694
707
  };
695
708
  }
696
709
 
710
+ // ---------- Export history (Phase 6.5 T10) ----------
711
+ //
712
+ // 5-deep ring buffer persisted at `<designRoot>/_export-history.json`.
713
+ // Reads tolerate missing / malformed files (returns []). Writes truncate
714
+ // to most-recent-first.
715
+
716
+ const HISTORY_PATH = path.join(paths.designRoot, '_export-history.json');
717
+ const HISTORY_DEPTH = 5;
718
+
719
+ async function loadExportHistory(): Promise<ExportHistoryEntry[]> {
720
+ try {
721
+ const raw = await Bun.file(HISTORY_PATH).text();
722
+ const arr = JSON.parse(raw);
723
+ if (!Array.isArray(arr)) return [];
724
+ return arr.slice(0, HISTORY_DEPTH);
725
+ } catch {
726
+ return [];
727
+ }
728
+ }
729
+
730
+ async function appendExportHistory(entry: ExportHistoryEntry): Promise<void> {
731
+ const prev = await loadExportHistory();
732
+ const next = [entry, ...prev].slice(0, HISTORY_DEPTH);
733
+ await Bun.write(HISTORY_PATH, JSON.stringify(next, null, 2));
734
+ }
735
+
697
736
  function tokenKind(name: string, value: string): string {
698
737
  const n = name.toLowerCase();
699
738
  const v = String(value).trim();
@@ -853,5 +892,7 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
853
892
  saveAnnotations,
854
893
  buildIndexData,
855
894
  buildSystemData,
895
+ loadExportHistory,
896
+ appendExportHistory,
856
897
  };
857
898
  }
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ // Tiny playwright shim that lists `[data-dc-screen]` IDs on a canvas-shell URL,
3
+ // one per line on stdout. Spawned by `exporters/pptx.ts` for canvas-as-separate
4
+ // merge. Lives as a subprocess (not a direct import) so `bun build --compile`
5
+ // of the dev-server binary doesn't pull in playwright + chromium-bidi deep deps.
6
+
7
+ import { chromium } from 'playwright';
8
+
9
+ const args = process.argv.slice(2);
10
+ let url;
11
+ let timeoutSec = 20;
12
+ for (let i = 0; i < args.length; i += 1) {
13
+ if (args[i] === '--url') {
14
+ i += 1;
15
+ url = args[i];
16
+ } else if (args[i] === '--timeout') {
17
+ i += 1;
18
+ timeoutSec = Number(args[i]);
19
+ }
20
+ }
21
+ if (!url) {
22
+ console.error('usage: _enumerate-artboards-playwright.mjs --url <url> [--timeout <sec>]');
23
+ process.exit(2);
24
+ }
25
+
26
+ const timeoutMs = timeoutSec * 1000;
27
+ const browser = await chromium.launch();
28
+ try {
29
+ const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
30
+ const page = await ctx.newPage();
31
+ await page.goto(url, { waitUntil: 'networkidle', timeout: timeoutMs });
32
+ const ids = await page.evaluate(() =>
33
+ Array.from(document.querySelectorAll('[data-dc-screen]')).map(
34
+ (el) => el.getAttribute('data-dc-screen') ?? ''
35
+ )
36
+ );
37
+ for (const id of ids.filter(Boolean)) console.log(id);
38
+ } finally {
39
+ await browser.close();
40
+ }