@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,129 @@
1
+ // _html-playwright.mjs — playwright shim for the HTML exporter.
2
+ //
3
+ // Walks the rendered DOM, serializes the full document with stylesheets
4
+ // inlined, and emits a standalone `index.html`. Web fonts + remote images
5
+ // are NOT inlined in v1 — the doc references them by absolute URL so the
6
+ // resulting file works under file:// when the user has those origins
7
+ // available. Full asset inlining is a follow-up; see plan T5.
8
+ //
9
+ // Invocation (Bun.spawn from exporters/html.ts — not invoked directly):
10
+ // npm exec --package=playwright -- node _html-playwright.mjs \
11
+ // --url <url> --selector <css> --out <path> \
12
+ // [--widen-to-artboard] [--multi] [--out-dir <dir>] [--timeout 8]
13
+
14
+ import { mkdirSync, writeFileSync } from 'node:fs';
15
+ import { dirname, join } from 'node:path';
16
+ import { chromium } from 'playwright';
17
+
18
+ const args = Object.fromEntries(
19
+ process.argv.slice(2).reduce((acc, cur, i, all) => {
20
+ if (cur.startsWith('--')) acc.push([cur.slice(2), all[i + 1] ?? '1']);
21
+ return acc;
22
+ }, [])
23
+ );
24
+
25
+ const {
26
+ url,
27
+ selector,
28
+ out,
29
+ 'out-dir': outDir,
30
+ 'widen-to-artboard': widenFlag,
31
+ multi: multiFlag,
32
+ timeout = '8',
33
+ } = args;
34
+
35
+ if (!url) {
36
+ console.error('usage: _html-playwright.mjs --url <url> --selector <css> --out <path>');
37
+ process.exit(2);
38
+ }
39
+
40
+ const widen = widenFlag !== undefined;
41
+ const multi = multiFlag !== undefined;
42
+ const timeoutMs = Number(timeout) * 1000;
43
+
44
+ const browser = await chromium.launch();
45
+ try {
46
+ const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
47
+ const page = await ctx.newPage();
48
+ await page.goto(url, { waitUntil: 'networkidle', timeout: timeoutMs });
49
+ // Reset the world plane's CSS zoom + transform so the captured artboard
50
+ // outerHTML carries 1440×900 dimensions instead of the pan-zoomed thumb.
51
+ await page.evaluate(() => {
52
+ const world = document.querySelector('.dc-world');
53
+ if (world) {
54
+ world.style.zoom = '1';
55
+ world.style.transform = 'none';
56
+ }
57
+ for (const el of document.querySelectorAll('[data-dc-screen]')) {
58
+ el.style.left = '0px';
59
+ el.style.top = '0px';
60
+ }
61
+ });
62
+
63
+ const written = [];
64
+
65
+ if (multi) {
66
+ if (!outDir) {
67
+ console.error('_html-playwright: --multi requires --out-dir');
68
+ process.exit(2);
69
+ }
70
+ mkdirSync(outDir, { recursive: true });
71
+ const screens = await page.locator(selector ?? '[data-dc-screen]').all();
72
+ for (let i = 0; i < screens.length; i += 1) {
73
+ const handle = screens[i];
74
+ const id = (await handle.getAttribute('data-dc-screen')) ?? `artboard-${i + 1}`;
75
+ const html = await serializeOne(handle, false);
76
+ const target = join(outDir, `${id}.html`);
77
+ writeFileSync(target, html, 'utf8');
78
+ written.push(target);
79
+ }
80
+ } else {
81
+ if (!out) {
82
+ console.error('_html-playwright: --out required when --multi not set');
83
+ process.exit(2);
84
+ }
85
+ mkdirSync(dirname(out), { recursive: true });
86
+ const handle = page.locator(selector ?? '[data-dc-screen]:first-of-type').first();
87
+ await handle.waitFor({ state: 'visible', timeout: timeoutMs });
88
+ const html = await serializeOne(handle, widen);
89
+ writeFileSync(out, html, 'utf8');
90
+ written.push(out);
91
+ }
92
+
93
+ for (const w of written) console.log(w);
94
+ console.error(`✓ playwright wrote ${written.length} html file(s)`);
95
+ } finally {
96
+ await browser.close();
97
+ }
98
+
99
+ async function serializeOne(locator, widenToArtboard) {
100
+ return await locator.evaluate(
101
+ (el, opts) => {
102
+ const target = opts.widenToArtboard ? (el.closest('[data-dc-screen]') ?? el) : el;
103
+ const cssChunks = [];
104
+ for (const sheet of Array.from(document.styleSheets)) {
105
+ try {
106
+ for (const rule of Array.from(sheet.cssRules)) {
107
+ cssChunks.push(rule.cssText);
108
+ }
109
+ } catch {
110
+ // Cross-origin sheet — skip.
111
+ }
112
+ }
113
+ const styleBlock = `<style>${cssChunks.join('\n')}</style>`;
114
+ const innerHtml = target.outerHTML;
115
+ return `<!doctype html>
116
+ <html lang="en">
117
+ <head>
118
+ <meta charset="utf-8" />
119
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
120
+ <title>${document.title || 'Maude export'}</title>
121
+ <base href="${location.origin}/" />
122
+ ${styleBlock}
123
+ </head>
124
+ <body>${innerHtml}</body>
125
+ </html>`;
126
+ },
127
+ { widenToArtboard }
128
+ );
129
+ }
@@ -0,0 +1,105 @@
1
+ // _pdf-playwright.mjs — playwright shim for the PDF exporter.
2
+ //
3
+ // Drives Chromium's print-to-PDF (`page.pdf()`) directly so the output is a
4
+ // true vector PDF with selectable text — NOT a PNG embedded in a PDF wrapper.
5
+ // Per DDR-041 (PDF via page.pdf()): print-media emulation, font readiness
6
+ // wait, explicit page size matching the artboard rect.
7
+ //
8
+ // Invocation (Bun.spawn from exporters/pdf.ts — not invoked directly):
9
+ // node _pdf-playwright.mjs --url <url> --selector <css> --out <path>
10
+ // [--multi] [--out-dir <dir>] [--timeout 12]
11
+
12
+ import { mkdirSync, writeFileSync } from 'node:fs';
13
+ import { dirname, join } from 'node:path';
14
+ import { chromium } from 'playwright';
15
+
16
+ const args = Object.fromEntries(
17
+ process.argv.slice(2).reduce((acc, cur, i, all) => {
18
+ if (cur.startsWith('--')) acc.push([cur.slice(2), all[i + 1] ?? '1']);
19
+ return acc;
20
+ }, [])
21
+ );
22
+
23
+ const { url, selector, out, 'out-dir': outDir, multi: multiFlag, timeout = '12' } = args;
24
+
25
+ if (!url) {
26
+ console.error('usage: _pdf-playwright.mjs --url <url> --selector <css> --out <path>');
27
+ process.exit(2);
28
+ }
29
+
30
+ const multi = multiFlag !== undefined;
31
+ const timeoutMs = Number(timeout) * 1000;
32
+
33
+ const browser = await chromium.launch();
34
+ try {
35
+ const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
36
+ const page = await ctx.newPage();
37
+ await page.goto(url, { waitUntil: 'networkidle', timeout: timeoutMs });
38
+ // Wait for web fonts so `@font-face` glyphs land in the PDF instead of
39
+ // fallback Latin (puppeteer #3183 / playwright equivalent).
40
+ await page.evaluate(() => document.fonts.ready);
41
+ // Print-media emulation — Chromium's PDF output otherwise applies *screen*
42
+ // CSS and ignores `@media print` rules entirely.
43
+ await page.emulateMedia({ media: 'print' });
44
+
45
+ const written = [];
46
+ const screens = multi
47
+ ? await page.locator(selector ?? '[data-dc-screen]').all()
48
+ : [page.locator(selector ?? '[data-dc-screen]:first-of-type').first()];
49
+
50
+ if (multi && outDir) mkdirSync(outDir, { recursive: true });
51
+ else if (out) mkdirSync(dirname(out), { recursive: true });
52
+
53
+ // Reset the world plane's pan/zoom + transform so artboards render at
54
+ // their declared dimensions. CSS `zoom` on .dc-world actually shrinks
55
+ // layout, so getBoundingClientRect returns the post-zoom size unless
56
+ // we zero this. Done once per page; affects every artboard read below.
57
+ await page.evaluate(() => {
58
+ const world = document.querySelector('.dc-world');
59
+ if (world) {
60
+ world.style.zoom = '1';
61
+ world.style.transform = 'none';
62
+ }
63
+ });
64
+
65
+ for (let i = 0; i < screens.length; i += 1) {
66
+ const handle = screens[i];
67
+ // Pin each artboard to (0,0) right before its capture so the world's
68
+ // multi-artboard layout doesn't push the bbox off the viewport.
69
+ await handle.evaluate((el) => {
70
+ el.style.left = '0px';
71
+ el.style.top = '0px';
72
+ });
73
+ const rect = await handle.evaluate((el) => {
74
+ const r = el.getBoundingClientRect();
75
+ return { w: r.width, h: r.height, x: r.left, y: r.top };
76
+ });
77
+ // Set the page size to the artboard's pixel dimensions so the resulting
78
+ // PDF is exactly one artboard per page with no margin.
79
+ const targetPath = multi ? join(outDir, `artboard-${i + 1}.pdf`) : out;
80
+ // Crop trick: set the viewport to the artboard rect, scroll it into the
81
+ // top-left corner, then page.pdf() with matching width/height.
82
+ await page.setViewportSize({
83
+ width: Math.ceil(rect.w),
84
+ height: Math.ceil(rect.h),
85
+ });
86
+ await handle.evaluate((el) => {
87
+ el.scrollIntoView({ block: 'start', inline: 'start' });
88
+ window.scrollTo(0, 0);
89
+ });
90
+ const pdf = await page.pdf({
91
+ width: `${Math.ceil(rect.w)}px`,
92
+ height: `${Math.ceil(rect.h)}px`,
93
+ printBackground: true,
94
+ preferCSSPageSize: false,
95
+ margin: { top: 0, right: 0, bottom: 0, left: 0 },
96
+ });
97
+ writeFileSync(targetPath, pdf);
98
+ written.push(targetPath);
99
+ }
100
+
101
+ for (const w of written) console.log(w);
102
+ console.error(`✓ page.pdf wrote ${written.length} file(s)`);
103
+ } finally {
104
+ await browser.close();
105
+ }
@@ -0,0 +1,143 @@
1
+ // _png-playwright.mjs — playwright shim for the PNG exporter.
2
+ //
3
+ // Replaces the screenshot.sh / agent-browser path because agent-browser
4
+ // applies its own viewport sizing that doesn't honor our 1440x900 setup,
5
+ // producing clipped captures. With our own playwright we control the
6
+ // viewport exactly and crop to the target element's bounding box.
7
+
8
+ import { mkdirSync, writeFileSync } from 'node:fs';
9
+ import { dirname, join } from 'node:path';
10
+ import { chromium } from 'playwright';
11
+
12
+ const args = Object.fromEntries(
13
+ process.argv.slice(2).reduce((acc, cur, i, all) => {
14
+ if (cur.startsWith('--')) acc.push([cur.slice(2), all[i + 1] ?? '1']);
15
+ return acc;
16
+ }, [])
17
+ );
18
+
19
+ const {
20
+ url,
21
+ selector,
22
+ out,
23
+ 'out-dir': outDir,
24
+ 'widen-to-artboard': widenFlag,
25
+ multi: multiFlag,
26
+ timeout = '12',
27
+ scale = '1',
28
+ } = args;
29
+
30
+ if (!url) {
31
+ console.error('usage: _png-playwright.mjs --url <url> --selector <css> --out <path>');
32
+ process.exit(2);
33
+ }
34
+
35
+ const widen = widenFlag !== undefined;
36
+ const multi = multiFlag !== undefined;
37
+ const timeoutMs = Number(timeout) * 1000;
38
+ const deviceScaleFactor = Math.max(1, Math.min(4, Number(scale) || 1));
39
+
40
+ const browser = await chromium.launch();
41
+ try {
42
+ // 1440x900 matches the canvas viewport the design tool uses everywhere;
43
+ // exporters resize per-target before each shot to fit the artboard exactly.
44
+ const ctx = await browser.newContext({
45
+ viewport: { width: 1440, height: 900 },
46
+ deviceScaleFactor,
47
+ });
48
+ const page = await ctx.newPage();
49
+ await page.goto(url, { waitUntil: 'networkidle', timeout: timeoutMs });
50
+ await page.evaluate(() => document.fonts.ready);
51
+
52
+ const written = [];
53
+
54
+ const captureHandle = async (handle, target) => {
55
+ // Widen to artboard if requested — the selection's selector points at a
56
+ // descendant, but for "Export this artboard" we want the enclosing
57
+ // [data-dc-screen]. Done browser-side so the locator + bbox align.
58
+ const widenedHandle = widen
59
+ ? await handle.evaluateHandle((el) => el.closest('[data-dc-screen]') ?? el)
60
+ : handle;
61
+ // Reset the world plane's pan/zoom so every artboard renders at its
62
+ // declared native dimensions (1440×900 etc.). The dev-server uses CSS
63
+ // `zoom` (not `transform: scale`) on `.dc-world`, which actually shrinks
64
+ // layout — getBoundingClientRect returns 818×512 instead of 1440×900
65
+ // unless we zero both `zoom` and `transform` here.
66
+ await page.evaluate(
67
+ (sel) => {
68
+ const world = document.querySelector('.dc-world');
69
+ if (world) {
70
+ world.style.zoom = '1';
71
+ world.style.transform = 'none';
72
+ }
73
+ // Each artboard carries `style="left: …; top: …;"` so the world plane
74
+ // can position it as part of a multi-artboard layout. Pin the target
75
+ // to (0,0) so the screenshot clip starts at the viewport origin.
76
+ const ab = document.querySelector(sel);
77
+ if (ab) {
78
+ ab.style.left = '0px';
79
+ ab.style.top = '0px';
80
+ }
81
+ },
82
+ widen ? '[data-dc-screen]:first-of-type' : (selector ?? '[data-dc-screen]:first-of-type')
83
+ );
84
+ const rect = await widenedHandle.evaluate((el) => {
85
+ const r = el.getBoundingClientRect();
86
+ return { x: r.left, y: r.top, width: r.width, height: r.height };
87
+ });
88
+ // Resize the viewport to fit the artboard so the screenshot doesn't
89
+ // include the world plane's pan/zoom margin. The artboard then sits at
90
+ // (0,0) with its native dimensions.
91
+ await page.setViewportSize({
92
+ width: Math.max(1, Math.ceil(rect.width)),
93
+ height: Math.max(1, Math.ceil(rect.height)),
94
+ });
95
+ await widenedHandle.evaluate((el) => {
96
+ el.scrollIntoView({ block: 'start', inline: 'start' });
97
+ window.scrollTo(0, 0);
98
+ });
99
+ // After scroll, recompute rect — it's now anchored near (0,0).
100
+ const finalRect = await widenedHandle.evaluate((el) => {
101
+ const r = el.getBoundingClientRect();
102
+ return { x: r.left, y: r.top, width: r.width, height: r.height };
103
+ });
104
+ await page.screenshot({
105
+ path: target,
106
+ clip: {
107
+ x: Math.max(0, Math.floor(finalRect.x)),
108
+ y: Math.max(0, Math.floor(finalRect.y)),
109
+ width: Math.max(1, Math.ceil(finalRect.width)),
110
+ height: Math.max(1, Math.ceil(finalRect.height)),
111
+ },
112
+ });
113
+ written.push(target);
114
+ };
115
+
116
+ if (multi) {
117
+ if (!outDir) {
118
+ console.error('_png-playwright: --multi requires --out-dir');
119
+ process.exit(2);
120
+ }
121
+ mkdirSync(outDir, { recursive: true });
122
+ const screens = await page.locator(selector ?? '[data-dc-screen]').all();
123
+ for (let i = 0; i < screens.length; i += 1) {
124
+ const handle = screens[i];
125
+ const id = (await handle.getAttribute('data-dc-screen')) ?? `artboard-${i + 1}`;
126
+ await captureHandle(handle, join(outDir, `${id}.png`));
127
+ }
128
+ } else {
129
+ if (!out) {
130
+ console.error('_png-playwright: --out required when --multi not set');
131
+ process.exit(2);
132
+ }
133
+ mkdirSync(dirname(out), { recursive: true });
134
+ const handle = page.locator(selector ?? '[data-dc-screen]:first-of-type').first();
135
+ await handle.waitFor({ state: 'visible', timeout: timeoutMs });
136
+ await captureHandle(handle, out);
137
+ }
138
+
139
+ for (const w of written) console.log(w);
140
+ console.error(`✓ playwright wrote ${written.length} png file(s)`);
141
+ } finally {
142
+ await browser.close();
143
+ }
@@ -0,0 +1,98 @@
1
+ // _pptx-playwright.mjs — playwright shim for the PPTX exporter.
2
+ //
3
+ // Drives `dom-to-pptx` (atharva9167j) — computed-style + getBoundingClientRect
4
+ // architecture that respects the browser's layout result. Replaces our hand-
5
+ // rolled DOM walker; see DDR-043. Per artboard → one slide. dom-to-pptx ships
6
+ // a UMD bundle at `dist/dom-to-pptx.bundle.js` that exposes `window.domToPptx`.
7
+ //
8
+ // Invocation (Bun.spawn from exporters/pptx.ts — not invoked directly):
9
+ // node _pptx-playwright.mjs --url <url> --selector <css> --out <pptx-path>
10
+ // --bundle-path <umd.js> [--multi] [--timeout 12]
11
+
12
+ import { mkdirSync, writeFileSync } from 'node:fs';
13
+ import { dirname } from 'node:path';
14
+ import { chromium } from 'playwright';
15
+
16
+ const args = Object.fromEntries(
17
+ process.argv.slice(2).reduce((acc, cur, i, all) => {
18
+ if (cur.startsWith('--')) acc.push([cur.slice(2), all[i + 1] ?? '1']);
19
+ return acc;
20
+ }, [])
21
+ );
22
+
23
+ const { url, selector, out, 'bundle-path': bundlePath, multi: multiFlag, timeout = '15' } = args;
24
+
25
+ if (!url || !out || !bundlePath) {
26
+ console.error(
27
+ 'usage: _pptx-playwright.mjs --url <url> --selector <css> --out <pptx-path> --bundle-path <umd.js>'
28
+ );
29
+ process.exit(2);
30
+ }
31
+
32
+ const multi = multiFlag !== undefined;
33
+ const timeoutMs = Number(timeout) * 1000;
34
+
35
+ mkdirSync(dirname(out), { recursive: true });
36
+
37
+ const browser = await chromium.launch();
38
+ try {
39
+ const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
40
+ const page = await ctx.newPage();
41
+ await page.goto(url, { waitUntil: 'networkidle', timeout: timeoutMs });
42
+ await page.evaluate(() => document.fonts.ready);
43
+ // Reset the world plane's CSS zoom + transform so dom-to-pptx reads the
44
+ // artboard at its declared 1440×900 instead of the pan-zoomed thumbnail.
45
+ await page.evaluate(() => {
46
+ const world = document.querySelector('.dc-world');
47
+ if (world) {
48
+ world.style.zoom = '1';
49
+ world.style.transform = 'none';
50
+ }
51
+ for (const el of document.querySelectorAll('[data-dc-screen]')) {
52
+ el.style.left = '0px';
53
+ el.style.top = '0px';
54
+ }
55
+ });
56
+ // dom-to-pptx ships a UMD bundle; addScriptTag exposes `window.domToPptx`.
57
+ await page.addScriptTag({ path: bundlePath });
58
+
59
+ // Resolve target handle(s). Multi-mode iterates every artboard; single-
60
+ // mode widens to the closest artboard ancestor of the supplied selector
61
+ // (matches the SVG/HTML shims' behaviour for --selector).
62
+ const handles = multi
63
+ ? await page.locator(selector ?? '[data-dc-screen]').all()
64
+ : [page.locator(selector ?? '[data-dc-screen]:first-of-type').first()];
65
+
66
+ // dom-to-pptx is designed to walk ONE root and emit ONE pptx. For multi,
67
+ // we serialize each artboard separately then merge in Node via pptxgenjs.
68
+ // For v1 we just emit the FIRST artboard if multi=true and warn — proper
69
+ // merge lives in exporters/pptx.ts (which receives back the byte array).
70
+ const handle = handles[0];
71
+ if (!handle) {
72
+ console.error('_pptx-playwright: no target found');
73
+ process.exit(2);
74
+ }
75
+ await handle.waitFor({ state: 'visible', timeout: timeoutMs });
76
+
77
+ // Run dom-to-pptx inside the page. It returns a Blob; we serialise to a
78
+ // byte array for transport across the playwright boundary.
79
+ const bytesArray = await handle.evaluate(async (el) => {
80
+ const target = el.closest('[data-dc-screen]') ?? el;
81
+ // window.domToPptx is the UMD-injected entry.
82
+ const { exportToPptx } = /** @type any */ (window).domToPptx;
83
+ if (typeof exportToPptx !== 'function') {
84
+ throw new Error('dom-to-pptx bundle did not expose exportToPptx');
85
+ }
86
+ const blob = await exportToPptx(target, { filename: 'export.pptx' });
87
+ const ab = await blob.arrayBuffer();
88
+ return Array.from(new Uint8Array(ab));
89
+ });
90
+
91
+ writeFileSync(out, new Uint8Array(bytesArray));
92
+ console.log(out);
93
+ console.error(
94
+ `✓ dom-to-pptx wrote ${out} (${bytesArray.length} bytes, ${handles.length} artboard${handles.length === 1 ? '' : 's available — first emitted'})`
95
+ );
96
+ } finally {
97
+ await browser.close();
98
+ }
@@ -0,0 +1,141 @@
1
+ // _svg-playwright.mjs — playwright shim for the SVG exporter.
2
+ //
3
+ // Uses `dom-to-svg` (felixfbecker/dom-to-svg) to walk the rendered DOM and
4
+ // emit real SVG primitives — <rect>, <text>, <path>, <image> — that vector
5
+ // editors like Affinity Designer / Illustrator / Inkscape decompose
6
+ // correctly. The previous `<foreignObject>`-wrapping approach (DDR-038)
7
+ // renders pixel-perfect in Chrome but Affinity refuses to import it; see
8
+ // DDR-042 for the swap rationale.
9
+ //
10
+ // Bundled IIFE: exporters/_browser-bundles.ts pre-bundles dom-to-svg via
11
+ // Bun.build, caches the result under /tmp, and passes the path via
12
+ // --bundle-path. The shim loads it with addScriptTag.
13
+ //
14
+ // Invocation (Bun.spawn from exporters/svg.ts — not invoked directly):
15
+ // node _svg-playwright.mjs --url <url> --selector <css> --out <path>
16
+ // --bundle-path <iife.js> [--widen-to-artboard] [--multi]
17
+ // [--out-dir <dir>] [--timeout 12]
18
+
19
+ import { mkdirSync, writeFileSync } from 'node:fs';
20
+ import { dirname, join } from 'node:path';
21
+ import { chromium } from 'playwright';
22
+
23
+ const args = Object.fromEntries(
24
+ process.argv.slice(2).reduce((acc, cur, i, all) => {
25
+ if (cur.startsWith('--')) acc.push([cur.slice(2), all[i + 1] ?? '1']);
26
+ return acc;
27
+ }, [])
28
+ );
29
+
30
+ const {
31
+ url,
32
+ selector,
33
+ out,
34
+ 'out-dir': outDir,
35
+ 'bundle-path': bundlePath,
36
+ 'widen-to-artboard': widenFlag,
37
+ multi: multiFlag,
38
+ timeout = '12',
39
+ } = args;
40
+
41
+ if (!url || !bundlePath) {
42
+ console.error(
43
+ 'usage: _svg-playwright.mjs --url <url> --selector <css> --out <path> --bundle-path <iife.js>'
44
+ );
45
+ process.exit(2);
46
+ }
47
+
48
+ const widen = widenFlag !== undefined;
49
+ const multi = multiFlag !== undefined;
50
+ const timeoutMs = Number(timeout) * 1000;
51
+
52
+ const browser = await chromium.launch();
53
+ try {
54
+ const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
55
+ const page = await ctx.newPage();
56
+ await page.goto(url, { waitUntil: 'networkidle', timeout: timeoutMs });
57
+ await page.evaluate(() => document.fonts.ready);
58
+ // Reset the world plane's CSS zoom + transform so artboards render at
59
+ // declared dimensions before dom-to-svg walks the layout.
60
+ await page.evaluate(() => {
61
+ const world = document.querySelector('.dc-world');
62
+ if (world) {
63
+ world.style.zoom = '1';
64
+ world.style.transform = 'none';
65
+ }
66
+ for (const el of document.querySelectorAll('[data-dc-screen]')) {
67
+ el.style.left = '0px';
68
+ el.style.top = '0px';
69
+ }
70
+ });
71
+ // Inject dom-to-svg into the page. Bundle attaches its exports under
72
+ // `window.domToSvg`.
73
+ await page.addScriptTag({ path: bundlePath });
74
+
75
+ const written = [];
76
+
77
+ const serializeOne = async (handle) => {
78
+ return await handle.evaluate(
79
+ async (el, opts) => {
80
+ const target = opts.widenToArtboard ? (el.closest('[data-dc-screen]') ?? el) : el;
81
+ // window.domToSvg is the IIFE-injected entry.
82
+ const { elementToSVG, inlineResources } = /** @type any */ (window).domToSvg;
83
+ const svgDoc = elementToSVG(target);
84
+ // base64-embeds fonts + images so the SVG is portable outside the
85
+ // dev-server origin. Some external fetches fail silently — Affinity
86
+ // tolerates missing resources better than missing primitives.
87
+ try {
88
+ await inlineResources(svgDoc.documentElement);
89
+ } catch {
90
+ /* best-effort */
91
+ }
92
+ return new XMLSerializer().serializeToString(svgDoc);
93
+ },
94
+ { widenToArtboard: opts }
95
+ );
96
+ };
97
+ const opts = { widenToArtboard: widen };
98
+
99
+ if (multi) {
100
+ if (!outDir) {
101
+ console.error('_svg-playwright: --multi requires --out-dir');
102
+ process.exit(2);
103
+ }
104
+ mkdirSync(outDir, { recursive: true });
105
+ const screens = await page.locator(selector ?? '[data-dc-screen]').all();
106
+ for (let i = 0; i < screens.length; i += 1) {
107
+ const handle = screens[i];
108
+ const id = (await handle.getAttribute('data-dc-screen')) ?? `artboard-${i + 1}`;
109
+ const svg = await handle.evaluate(async (el) => {
110
+ // window.domToSvg is the IIFE-injected entry.
111
+ const { elementToSVG, inlineResources, formatXML } = /** @type any */ (window).domToSvg;
112
+ const svgDoc = elementToSVG(el);
113
+ try {
114
+ await inlineResources(svgDoc.documentElement);
115
+ } catch {
116
+ /* */
117
+ }
118
+ return formatXML(new XMLSerializer().serializeToString(svgDoc));
119
+ });
120
+ const target = join(outDir, `${id}.svg`);
121
+ writeFileSync(target, svg, 'utf8');
122
+ written.push(target);
123
+ }
124
+ } else {
125
+ if (!out) {
126
+ console.error('_svg-playwright: --out required when --multi not set');
127
+ process.exit(2);
128
+ }
129
+ mkdirSync(dirname(out), { recursive: true });
130
+ const handle = page.locator(selector ?? '[data-dc-screen]:first-of-type').first();
131
+ await handle.waitFor({ state: 'visible', timeout: timeoutMs });
132
+ const svg = await serializeOne(handle);
133
+ writeFileSync(out, svg, 'utf8');
134
+ written.push(out);
135
+ }
136
+
137
+ for (const w of written) console.log(w);
138
+ console.error(`✓ dom-to-svg wrote ${written.length} svg file(s)`);
139
+ } finally {
140
+ await browser.close();
141
+ }
@@ -1182,7 +1182,6 @@ function DesignCanvasInner({ children, controls }: DesignCanvasProps) {
1182
1182
  );
1183
1183
 
1184
1184
  const showMiniMap = controls?.minimap !== false;
1185
- const showToolbar = controls?.toolbar !== false;
1186
1185
 
1187
1186
  // Drag-state bus (Phase 4.2). Single source of truth: only one artboard
1188
1187
  // drag is active at a time. DCArtboards write here when their local drag
@@ -1492,15 +1491,15 @@ const OVERLAY_CSS = `
1492
1491
  bottom: 16px;
1493
1492
  width: 196px;
1494
1493
  height: 132px;
1495
- background: var(--bg-1, rgba(255,255,255,0.98));
1496
- border: 1px solid var(--u-border-2, rgba(0,0,0,0.08));
1497
- border-radius: 8px;
1498
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1494
+ background: var(--bg-2, var(--bg-1, rgba(255,255,255,0.98)));
1495
+ border: 1px solid var(--fg-0, #1c1917);
1496
+ border-radius: 0;
1497
+ font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
1499
1498
  font-size: 10px;
1500
- color: rgba(40,30,20,0.7);
1499
+ color: var(--fg-1, rgba(40,30,20,0.7));
1501
1500
  z-index: 6;
1502
1501
  user-select: none;
1503
- box-shadow: 0 6px 24px rgba(0,0,0,0.08);
1502
+ box-shadow: 4px 4px 0 var(--fg-0, #1c1917);
1504
1503
  overflow: hidden;
1505
1504
  }
1506
1505
  .dc-mm-hd {
@@ -1534,15 +1533,15 @@ const OVERLAY_CSS = `
1534
1533
  transform: translateX(-50%);
1535
1534
  display: flex;
1536
1535
  align-items: stretch;
1537
- background: rgba(255,255,255,0.94);
1538
- border: 1px solid rgba(0,0,0,0.12);
1539
- border-radius: 6px;
1536
+ background: var(--bg-2, rgba(255,255,255,0.94));
1537
+ border: 1px solid var(--fg-0, #1c1917);
1538
+ border-radius: 0;
1540
1539
  overflow: hidden;
1541
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1540
+ font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
1542
1541
  font-size: 11px;
1543
- color: rgba(40,30,20,0.85);
1542
+ color: var(--fg-1, rgba(40,30,20,0.85));
1544
1543
  z-index: 6;
1545
- box-shadow: 0 4px 16px rgba(0,0,0,0.06);
1544
+ box-shadow: 4px 4px 0 var(--fg-0, #1c1917);
1546
1545
  }
1547
1546
  .dc-zoom-tb button {
1548
1547
  appearance: none;