@1agh/maude 0.16.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.
- package/README.md +4 -0
- package/cli/commands/design.mjs +108 -2
- package/package.json +12 -18
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +8 -8
- package/plugins/design/dev-server/annotations-layer.tsx +8 -10
- package/plugins/design/dev-server/api.ts +41 -0
- package/plugins/design/dev-server/bin/_enumerate-artboards-playwright.mjs +40 -0
- package/plugins/design/dev-server/bin/_html-playwright.mjs +129 -0
- package/plugins/design/dev-server/bin/_pdf-playwright.mjs +105 -0
- package/plugins/design/dev-server/bin/_png-playwright.mjs +143 -0
- package/plugins/design/dev-server/bin/_pptx-playwright.mjs +98 -0
- package/plugins/design/dev-server/bin/_svg-playwright.mjs +141 -0
- package/plugins/design/dev-server/canvas-lib.tsx +12 -13
- package/plugins/design/dev-server/canvas-shell.tsx +32 -4
- package/plugins/design/dev-server/client/app.jsx +18 -1
- package/plugins/design/dev-server/context-menu.tsx +36 -9
- package/plugins/design/dev-server/dist/client.bundle.js +11 -3
- package/plugins/design/dev-server/export-dialog.tsx +401 -0
- package/plugins/design/dev-server/exporters/_browser-bundles.ts +89 -0
- package/plugins/design/dev-server/exporters/canva-handoff-prompt.ts +74 -0
- package/plugins/design/dev-server/exporters/canva.ts +126 -0
- package/plugins/design/dev-server/exporters/html.ts +103 -0
- package/plugins/design/dev-server/exporters/index.ts +135 -0
- package/plugins/design/dev-server/exporters/pdf.ts +109 -0
- package/plugins/design/dev-server/exporters/png.ts +136 -0
- package/plugins/design/dev-server/exporters/pptx.ts +263 -0
- package/plugins/design/dev-server/exporters/scope.ts +196 -0
- package/plugins/design/dev-server/exporters/svg.ts +122 -0
- package/plugins/design/dev-server/exporters/zip.ts +109 -0
- package/plugins/design/dev-server/http.ts +80 -0
- package/plugins/design/dev-server/inspect.ts +1 -1
- package/plugins/design/dev-server/server.mjs +1 -1
- package/plugins/design/dev-server/test/exporters/canva.test.ts +64 -0
- package/plugins/design/dev-server/test/exporters/endpoint.test.ts +121 -0
- package/plugins/design/dev-server/test/exporters/history.test.ts +79 -0
- package/plugins/design/dev-server/test/exporters/html.test.ts +26 -0
- package/plugins/design/dev-server/test/exporters/pdf.test.ts +53 -0
- package/plugins/design/dev-server/test/exporters/png.test.ts +32 -0
- package/plugins/design/dev-server/test/exporters/pptx.test.ts +31 -0
- package/plugins/design/dev-server/test/exporters/scope.test.ts +0 -0
- package/plugins/design/dev-server/test/exporters/svg.test.ts +29 -0
- package/plugins/design/dev-server/test/exporters/zip.test.ts +105 -0
- package/plugins/design/dev-server/tool-palette.tsx +34 -16
- package/plugins/design/templates/_shell.html +33 -0
|
@@ -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(--
|
|
1497
|
-
border-radius:
|
|
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:
|
|
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
|
|
1539
|
-
border-radius:
|
|
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:
|
|
1544
|
+
box-shadow: 4px 4px 0 var(--fg-0, #1c1917);
|
|
1546
1545
|
}
|
|
1547
1546
|
.dc-zoom-tb button {
|
|
1548
1547
|
appearance: none;
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
type MenuItem,
|
|
52
52
|
useContextMenu,
|
|
53
53
|
} from './context-menu.tsx';
|
|
54
|
+
import { ExportDialogProvider } from './export-dialog.tsx';
|
|
54
55
|
import { type HoverTarget, resolveHoverTarget, useInputRouter } from './input-router.tsx';
|
|
55
56
|
import { ToolPalette } from './tool-palette.tsx';
|
|
56
57
|
import {
|
|
@@ -203,9 +204,11 @@ function CanvasCore({
|
|
|
203
204
|
);
|
|
204
205
|
|
|
205
206
|
return (
|
|
206
|
-
<
|
|
207
|
-
<
|
|
208
|
-
|
|
207
|
+
<ExportDialogProvider>
|
|
208
|
+
<ContextMenuProvider registry={registry}>
|
|
209
|
+
<CanvasRouter hostRef={hostRef}>{children}</CanvasRouter>
|
|
210
|
+
</ContextMenuProvider>
|
|
211
|
+
</ExportDialogProvider>
|
|
209
212
|
);
|
|
210
213
|
}
|
|
211
214
|
|
|
@@ -263,6 +266,23 @@ function buildRegistry(deps: {
|
|
|
263
266
|
onSelect: () => controller?.reset(),
|
|
264
267
|
};
|
|
265
268
|
|
|
269
|
+
// Phase 6.5 — context-menu → ExportDialog. Each entry dispatches a custom
|
|
270
|
+
// event the dialog provider listens for; this avoids prop-drilling the
|
|
271
|
+
// dialog handle through every menu callback. The scope arg prefills the
|
|
272
|
+
// dialog's scope dropdown so the user lands on the right resolution.
|
|
273
|
+
const exportItem = (id: string, label: string, scope: string, shortcut?: string): MenuItem => ({
|
|
274
|
+
id,
|
|
275
|
+
label,
|
|
276
|
+
shortcut,
|
|
277
|
+
onSelect: () => {
|
|
278
|
+
try {
|
|
279
|
+
window.dispatchEvent(new CustomEvent('maude:open-export', { detail: { scope } }));
|
|
280
|
+
} catch {
|
|
281
|
+
/* non-window environments */
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
|
|
266
286
|
return {
|
|
267
287
|
element: [
|
|
268
288
|
[
|
|
@@ -298,6 +318,7 @@ function buildRegistry(deps: {
|
|
|
298
318
|
},
|
|
299
319
|
},
|
|
300
320
|
],
|
|
321
|
+
[exportItem('export-selection', 'Export selection…', 'selection', '⌘E')],
|
|
301
322
|
[
|
|
302
323
|
{
|
|
303
324
|
id: 'hide',
|
|
@@ -334,8 +355,15 @@ function buildRegistry(deps: {
|
|
|
334
355
|
fitItem,
|
|
335
356
|
resetItem,
|
|
336
357
|
],
|
|
358
|
+
[exportItem('export-artboard', 'Export this artboard…', 'artboard')],
|
|
359
|
+
],
|
|
360
|
+
world: [
|
|
361
|
+
[fitItem, resetItem],
|
|
362
|
+
[
|
|
363
|
+
exportItem('export-canvas', 'Export canvas as separate…', 'canvas-as-separate'),
|
|
364
|
+
exportItem('export-project', 'Export project (ZIP)…', 'project-raw'),
|
|
365
|
+
],
|
|
337
366
|
],
|
|
338
|
-
world: [[fitItem, resetItem]],
|
|
339
367
|
overlay: [],
|
|
340
368
|
};
|
|
341
369
|
}
|
|
@@ -1861,8 +1861,25 @@ function App() {
|
|
|
1861
1861
|
const activeFileComments = (activePath && activePath !== SYSTEM_TAB) ? (commentsByFile[activePath] || []) : [];
|
|
1862
1862
|
const totalOpen = totalCounts(commentsByFile).open;
|
|
1863
1863
|
|
|
1864
|
+
// Suppress the native browser context menu across the shell — the canvas
|
|
1865
|
+
// input-router already handles right-click inside the canvas host, but
|
|
1866
|
+
// sidebar / menubar / statusbar / floating chrome would otherwise leak the
|
|
1867
|
+
// native menu on top of our `.dc-context-menu` (or alone, outside canvas).
|
|
1868
|
+
// Editable fields (search box, future text inputs) keep the native menu so
|
|
1869
|
+
// copy/paste still works.
|
|
1870
|
+
const onShellContextMenu = useCallback((e) => {
|
|
1871
|
+
const t = e.target;
|
|
1872
|
+
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || (t.isContentEditable))) {
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
e.preventDefault();
|
|
1876
|
+
}, []);
|
|
1877
|
+
|
|
1864
1878
|
return (
|
|
1865
|
-
<div
|
|
1879
|
+
<div
|
|
1880
|
+
className={'app' + (commentsPanelOpen ? ' with-rsidebar' : '') + (sidebarOpen ? '' : ' no-sidebar')}
|
|
1881
|
+
onContextMenu={onShellContextMenu}
|
|
1882
|
+
>
|
|
1866
1883
|
<Sidebar
|
|
1867
1884
|
groups={groups}
|
|
1868
1885
|
activePath={activePath}
|
|
@@ -74,6 +74,27 @@ function noop(name: string) {
|
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
// Phase 6.5 T9 — export hooks. The default registry items use noop() so the
|
|
78
|
+
// menu still renders when the dialog provider isn't mounted; consumers wire
|
|
79
|
+
// real `openExport(scope)` callbacks by passing a custom registry to
|
|
80
|
+
// <ContextMenuProvider extra>. Pattern matches the existing Phase 5 noop
|
|
81
|
+
// affordances.
|
|
82
|
+
function defaultExportItem(label: string, scopeHint: string): MenuItem {
|
|
83
|
+
return {
|
|
84
|
+
id: `export-${scopeHint}`,
|
|
85
|
+
label,
|
|
86
|
+
shortcut: scopeHint === 'selection' ? '⌘E' : undefined,
|
|
87
|
+
onSelect: () => {
|
|
88
|
+
const detail = { scope: scopeHint };
|
|
89
|
+
try {
|
|
90
|
+
window.dispatchEvent(new CustomEvent('maude:open-export', { detail }));
|
|
91
|
+
} catch {
|
|
92
|
+
/* SSR / non-window environments */
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
77
98
|
const DEFAULT_REGISTRY: ContextRegistry = {
|
|
78
99
|
element: [
|
|
79
100
|
[
|
|
@@ -82,6 +103,7 @@ const DEFAULT_REGISTRY: ContextRegistry = {
|
|
|
82
103
|
{ id: 'copy-id', label: 'Copy data-cd-id', onSelect: noop('copy-id') },
|
|
83
104
|
{ id: 'inspect', label: 'Inspect', shortcut: '⌥I', onSelect: noop('inspect') },
|
|
84
105
|
],
|
|
106
|
+
[defaultExportItem('Export selection…', 'selection')],
|
|
85
107
|
[
|
|
86
108
|
{ id: 'hide', label: 'Hide', shortcut: '⌘⇧H', onSelect: noop('hide') },
|
|
87
109
|
{ id: 'lock', label: 'Lock', shortcut: '⌘⇧L', onSelect: noop('lock') },
|
|
@@ -92,6 +114,7 @@ const DEFAULT_REGISTRY: ContextRegistry = {
|
|
|
92
114
|
{ id: 'rename', label: 'Rename', shortcut: '↵', onSelect: noop('rename-artboard') },
|
|
93
115
|
{ id: 'duplicate', label: 'Duplicate', shortcut: '⌘D', onSelect: noop('duplicate-artboard') },
|
|
94
116
|
],
|
|
117
|
+
[defaultExportItem('Export this artboard…', 'artboard')],
|
|
95
118
|
[
|
|
96
119
|
{ id: 'fit-one', label: 'Fit just this artboard', onSelect: noop('fit-one') },
|
|
97
120
|
{ id: 'reset-pos', label: 'Reset position', onSelect: noop('reset-artboard-pos') },
|
|
@@ -108,6 +131,10 @@ const DEFAULT_REGISTRY: ContextRegistry = {
|
|
|
108
131
|
{ id: 'fit-view', label: 'Fit to view', shortcut: '1', onSelect: noop('fit-view') },
|
|
109
132
|
{ id: 'reset-view', label: 'Reset view', shortcut: '⌘0', onSelect: noop('reset-view') },
|
|
110
133
|
],
|
|
134
|
+
[
|
|
135
|
+
defaultExportItem('Export project (ZIP)…', 'project-raw'),
|
|
136
|
+
defaultExportItem('Export canvas as separate…', 'canvas-as-separate'),
|
|
137
|
+
],
|
|
111
138
|
],
|
|
112
139
|
overlay: [],
|
|
113
140
|
};
|
|
@@ -131,15 +158,15 @@ const MENU_CSS = `
|
|
|
131
158
|
.dc-context-menu {
|
|
132
159
|
position: fixed;
|
|
133
160
|
z-index: 7;
|
|
134
|
-
background: var(--bg-1, #fff);
|
|
135
|
-
border: 1px solid var(--
|
|
136
|
-
border-radius:
|
|
137
|
-
box-shadow: var(--
|
|
161
|
+
background: var(--u-bg-2, var(--bg-1, #fff));
|
|
162
|
+
border: 1px solid var(--u-fg-0, #1c1917);
|
|
163
|
+
border-radius: 0;
|
|
164
|
+
box-shadow: 4px 4px 0 var(--u-fg-0, #1c1917);
|
|
138
165
|
padding: 4px;
|
|
139
166
|
min-width: 220px;
|
|
140
|
-
font:
|
|
167
|
+
font-family: var(--u-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
141
168
|
font-size: 12px;
|
|
142
|
-
color: var(--fg-0, rgba(20,15,10,0.92));
|
|
169
|
+
color: var(--u-fg-0, var(--fg-0, rgba(20,15,10,0.92)));
|
|
143
170
|
user-select: none;
|
|
144
171
|
}
|
|
145
172
|
.dc-context-menu .dc-menu-sep {
|
|
@@ -152,8 +179,8 @@ const MENU_CSS = `
|
|
|
152
179
|
justify-content: space-between;
|
|
153
180
|
align-items: center;
|
|
154
181
|
gap: 16px;
|
|
155
|
-
padding:
|
|
156
|
-
border-radius:
|
|
182
|
+
padding: 5px 12px;
|
|
183
|
+
border-radius: 0;
|
|
157
184
|
cursor: pointer;
|
|
158
185
|
background: transparent;
|
|
159
186
|
border: 0;
|
|
@@ -164,7 +191,7 @@ const MENU_CSS = `
|
|
|
164
191
|
}
|
|
165
192
|
.dc-context-menu .dc-menu-item:hover,
|
|
166
193
|
.dc-context-menu .dc-menu-item:focus-visible {
|
|
167
|
-
background: var(--bg-3, rgba(0,0,0,0.05));
|
|
194
|
+
background: var(--u-bg-3, var(--bg-3, rgba(0,0,0,0.05)));
|
|
168
195
|
outline: none;
|
|
169
196
|
}
|
|
170
197
|
.dc-context-menu .dc-menu-item[disabled] {
|