@1agh/maude 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -8
- package/plugins/design/dependencies.json +30 -2
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +481 -78
- package/plugins/design/dev-server/annotations-layer.tsx +817 -170
- package/plugins/design/dev-server/api.ts +15 -1
- package/plugins/design/dev-server/bin/_enumerate-artboards-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_html-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pdf-playwright.mjs +25 -8
- package/plugins/design/dev-server/bin/_png-playwright.mjs +20 -20
- package/plugins/design/dev-server/bin/_pptx-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pw-launch.mjs +61 -0
- package/plugins/design/dev-server/bin/_screenshot-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_svg-playwright.mjs +125 -19
- package/plugins/design/dev-server/bin/smoke.sh +114 -12
- package/plugins/design/dev-server/canvas-arrowheads.ts +220 -0
- package/plugins/design/dev-server/canvas-comment-mount.tsx +8 -24
- package/plugins/design/dev-server/canvas-cursors.ts +130 -82
- package/plugins/design/dev-server/canvas-icons.tsx +169 -0
- package/plugins/design/dev-server/canvas-shell.tsx +113 -89
- package/plugins/design/dev-server/client/app.jsx +1084 -417
- package/plugins/design/dev-server/dist/client.bundle.js +242 -20
- package/plugins/design/dev-server/dist/comment-mount.js +40 -62
- package/plugins/design/dev-server/export-dialog.tsx +189 -1
- package/plugins/design/dev-server/exporters/html.ts +4 -1
- package/plugins/design/dev-server/exporters/index.ts +4 -1
- package/plugins/design/dev-server/exporters/pdf.ts +7 -1
- package/plugins/design/dev-server/exporters/png.ts +21 -2
- package/plugins/design/dev-server/exporters/pptx.ts +330 -201
- package/plugins/design/dev-server/exporters/scope.ts +107 -11
- package/plugins/design/dev-server/exporters/svg.ts +4 -1
- package/plugins/design/dev-server/http.ts +40 -9
- package/plugins/design/dev-server/input-router.tsx +9 -2
- package/plugins/design/dev-server/test/annotations-draw-modifiers.test.ts +206 -0
- package/plugins/design/dev-server/test/annotations-layer.test.ts +83 -3
- package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +243 -0
- package/plugins/design/dev-server/test/canvas-cursors.test.ts +95 -6
- package/plugins/design/dev-server/test/canvas-route.test.ts +4 -1
- package/plugins/design/dev-server/test/exporters/png.test.ts +24 -1
- package/plugins/design/dev-server/test/exporters/pptx-deck.test.ts +112 -0
- package/plugins/design/dev-server/test/exporters/pw-launch.test.ts +49 -0
- package/plugins/design/dev-server/test/exporters/scope.test.ts +0 -0
- package/plugins/design/dev-server/test/fixtures/phase-21-annotations.svg +1 -0
- package/plugins/design/dev-server/test/input-router.test.ts +11 -4
- package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +32 -0
- package/plugins/design/dev-server/test/use-annotation-resize.test.ts +200 -0
- package/plugins/design/dev-server/test/use-tool-mode.test.tsx +9 -11
- package/plugins/design/dev-server/tool-palette.tsx +140 -11
- package/plugins/design/dev-server/use-annotation-resize.tsx +208 -61
- package/plugins/design/dev-server/use-tool-mode.tsx +55 -9
- package/plugins/design/templates/_shell.html +36 -9
|
@@ -194,6 +194,11 @@ const ANNOTATION_SVG_ELEMENTS = new Set([
|
|
|
194
194
|
'ellipse',
|
|
195
195
|
'line',
|
|
196
196
|
'polyline',
|
|
197
|
+
// Phase 24 — `polygon` (diamond / triangle shape primitives + closed arrowhead
|
|
198
|
+
// outlines + diamond heads) and `circle` (circle arrowhead). Inert shape
|
|
199
|
+
// elements with no script capability — same safety class as the rest.
|
|
200
|
+
'polygon',
|
|
201
|
+
'circle',
|
|
197
202
|
'text',
|
|
198
203
|
]);
|
|
199
204
|
|
|
@@ -240,7 +245,16 @@ export function sanitizeAnnotationSvg(svg: string): string {
|
|
|
240
245
|
// 3. Attribute denylist on the surviving allowlisted elements — the legit
|
|
241
246
|
// vocabulary uses no on*= / style= / *href=, so stripping them closes
|
|
242
247
|
// inline handlers, CSS url(javascript:), and entity-encoded hrefs.
|
|
243
|
-
|
|
248
|
+
// The leading boundary is a LOOKBEHIND on whitespace / quote / slash
|
|
249
|
+
// (not a consumed `\s`) so a handler glued to the previous attribute's
|
|
250
|
+
// closing quote — `<circle r="2"onload="…"/>`, which HTML/SVG parsers
|
|
251
|
+
// accept as a distinct attribute — is also stripped (Phase 24 security
|
|
252
|
+
// review, DDR-067). The non-consuming lookbehind leaves the preceding
|
|
253
|
+
// quote intact so the legit attribute it belonged to survives.
|
|
254
|
+
.replace(
|
|
255
|
+
/(?<=[\s"'/])(?:on[a-z]+|style|(?:[\w-]+:)?href)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi,
|
|
256
|
+
''
|
|
257
|
+
)
|
|
244
258
|
);
|
|
245
259
|
}
|
|
246
260
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// merge. Lives as a subprocess (not a direct import) so `bun build --compile`
|
|
5
5
|
// of the dev-server binary doesn't pull in playwright + chromium-bidi deep deps.
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { launchChromium } from './_pw-launch.mjs';
|
|
8
8
|
|
|
9
9
|
const args = process.argv.slice(2);
|
|
10
10
|
let url;
|
|
@@ -24,7 +24,7 @@ if (!url) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const timeoutMs = timeoutSec * 1000;
|
|
27
|
-
const browser = await
|
|
27
|
+
const browser = await launchChromium();
|
|
28
28
|
try {
|
|
29
29
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
|
30
30
|
const page = await ctx.newPage();
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
15
15
|
import { dirname, join } from 'node:path';
|
|
16
|
-
import {
|
|
16
|
+
import { launchChromium } from './_pw-launch.mjs';
|
|
17
17
|
|
|
18
18
|
const args = Object.fromEntries(
|
|
19
19
|
process.argv.slice(2).reduce((acc, cur, i, all) => {
|
|
@@ -41,7 +41,7 @@ const widen = widenFlag !== undefined;
|
|
|
41
41
|
const multi = multiFlag !== undefined;
|
|
42
42
|
const timeoutMs = Number(timeout) * 1000;
|
|
43
43
|
|
|
44
|
-
const browser = await
|
|
44
|
+
const browser = await launchChromium();
|
|
45
45
|
try {
|
|
46
46
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
|
47
47
|
const page = await ctx.newPage();
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { dirname, join } from 'node:path';
|
|
14
|
-
import {
|
|
14
|
+
import { launchChromium } from './_pw-launch.mjs';
|
|
15
15
|
|
|
16
16
|
const args = Object.fromEntries(
|
|
17
17
|
process.argv.slice(2).reduce((acc, cur, i, all) => {
|
|
@@ -20,17 +20,26 @@ const args = Object.fromEntries(
|
|
|
20
20
|
}, [])
|
|
21
21
|
);
|
|
22
22
|
|
|
23
|
-
const {
|
|
23
|
+
const {
|
|
24
|
+
url,
|
|
25
|
+
selector,
|
|
26
|
+
out,
|
|
27
|
+
'out-dir': outDir,
|
|
28
|
+
'widen-to-artboard': widenFlag,
|
|
29
|
+
multi: multiFlag,
|
|
30
|
+
timeout = '12',
|
|
31
|
+
} = args;
|
|
24
32
|
|
|
25
33
|
if (!url) {
|
|
26
34
|
console.error('usage: _pdf-playwright.mjs --url <url> --selector <css> --out <path>');
|
|
27
35
|
process.exit(2);
|
|
28
36
|
}
|
|
29
37
|
|
|
38
|
+
const widen = widenFlag !== undefined;
|
|
30
39
|
const multi = multiFlag !== undefined;
|
|
31
40
|
const timeoutMs = Number(timeout) * 1000;
|
|
32
41
|
|
|
33
|
-
const browser = await
|
|
42
|
+
const browser = await launchChromium();
|
|
34
43
|
try {
|
|
35
44
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
|
36
45
|
const page = await ctx.newPage();
|
|
@@ -63,12 +72,20 @@ try {
|
|
|
63
72
|
});
|
|
64
73
|
|
|
65
74
|
for (let i = 0; i < screens.length; i += 1) {
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
//
|
|
75
|
+
// Widen a descendant selector to its enclosing artboard when requested
|
|
76
|
+
// (artboard-via-descendant fallback); selection / artboard-by-id targets
|
|
77
|
+
// pass through unchanged.
|
|
78
|
+
const handle =
|
|
79
|
+
widen && !multi
|
|
80
|
+
? await screens[i].evaluateHandle((el) => el.closest('[data-dc-screen]') ?? el)
|
|
81
|
+
: screens[i];
|
|
82
|
+
// Pin the captured element's own artboard to (0,0) right before its
|
|
83
|
+
// capture so the world's multi-artboard layout doesn't push the bbox off
|
|
84
|
+
// the viewport.
|
|
69
85
|
await handle.evaluate((el) => {
|
|
70
|
-
|
|
71
|
-
|
|
86
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
87
|
+
ab.style.left = '0px';
|
|
88
|
+
ab.style.top = '0px';
|
|
72
89
|
});
|
|
73
90
|
const rect = await handle.evaluate((el) => {
|
|
74
91
|
const r = el.getBoundingClientRect();
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { dirname, join } from 'node:path';
|
|
10
|
-
import {
|
|
10
|
+
import { launchChromium } from './_pw-launch.mjs';
|
|
11
11
|
|
|
12
12
|
const args = Object.fromEntries(
|
|
13
13
|
process.argv.slice(2).reduce((acc, cur, i, all) => {
|
|
@@ -37,7 +37,7 @@ const multi = multiFlag !== undefined;
|
|
|
37
37
|
const timeoutMs = Number(timeout) * 1000;
|
|
38
38
|
const deviceScaleFactor = Math.max(1, Math.min(4, Number(scale) || 1));
|
|
39
39
|
|
|
40
|
-
const browser = await
|
|
40
|
+
const browser = await launchChromium();
|
|
41
41
|
try {
|
|
42
42
|
// 1440x900 matches the canvas viewport the design tool uses everywhere;
|
|
43
43
|
// exporters resize per-target before each shot to fit the artboard exactly.
|
|
@@ -63,24 +63,24 @@ try {
|
|
|
63
63
|
// `zoom` (not `transform: scale`) on `.dc-world`, which actually shrinks
|
|
64
64
|
// layout — getBoundingClientRect returns 818×512 instead of 1440×900
|
|
65
65
|
// unless we zero both `zoom` and `transform` here.
|
|
66
|
-
await page.evaluate(
|
|
67
|
-
(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
);
|
|
66
|
+
await page.evaluate(() => {
|
|
67
|
+
const world = document.querySelector('.dc-world');
|
|
68
|
+
if (world) {
|
|
69
|
+
world.style.zoom = '1';
|
|
70
|
+
world.style.transform = 'none';
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
// Pin the captured element's OWN artboard to (0,0). Previously this reset
|
|
74
|
+
// `[data-dc-screen]:first-of-type` regardless of the target, so capturing
|
|
75
|
+
// artboard #2 (or any non-first selection's host) left the wrong artboard
|
|
76
|
+
// at the origin (item 5 root cause). Each artboard carries
|
|
77
|
+
// `style="left:…; top:…;"` for the world layout — zero the right one so the
|
|
78
|
+
// screenshot clip starts at the viewport origin.
|
|
79
|
+
await widenedHandle.evaluate((el) => {
|
|
80
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
81
|
+
ab.style.left = '0px';
|
|
82
|
+
ab.style.top = '0px';
|
|
83
|
+
});
|
|
84
84
|
const rect = await widenedHandle.evaluate((el) => {
|
|
85
85
|
const r = el.getBoundingClientRect();
|
|
86
86
|
return { x: r.left, y: r.top, width: r.width, height: r.height };
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { dirname } from 'node:path';
|
|
14
|
-
import {
|
|
14
|
+
import { launchChromium } from './_pw-launch.mjs';
|
|
15
15
|
|
|
16
16
|
const args = Object.fromEntries(
|
|
17
17
|
process.argv.slice(2).reduce((acc, cur, i, all) => {
|
|
@@ -34,7 +34,7 @@ const timeoutMs = Number(timeout) * 1000;
|
|
|
34
34
|
|
|
35
35
|
mkdirSync(dirname(out), { recursive: true });
|
|
36
36
|
|
|
37
|
-
const browser = await
|
|
37
|
+
const browser = await launchChromium();
|
|
38
38
|
try {
|
|
39
39
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
|
40
40
|
const page = await ctx.newPage();
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// _pw-launch.mjs — shared Chromium launcher for the export / screenshot shims.
|
|
2
|
+
//
|
|
3
|
+
// Every renderer shim (_png/_pdf/_svg/_html/_pptx/_enumerate-artboards/
|
|
4
|
+
// _screenshot-playwright.mjs) opens a Chromium instance with `chromium.launch()`.
|
|
5
|
+
// Playwright the npm package can be present while its browser BINARY is not — the
|
|
6
|
+
// package install and `npx playwright install chromium` are separate steps, and
|
|
7
|
+
// the dependency check `npx playwright --version` only proves the former. When
|
|
8
|
+
// the binary is missing, `launch()` rejects with a multi-line stack trace that
|
|
9
|
+
// `exporters/*.ts` rethrows and `/_api/export` surfaces verbatim as an opaque
|
|
10
|
+
// 500 ("nefunguji exporty"). This helper funnels that one failure mode through a
|
|
11
|
+
// single place: it prints one actionable line and exits with a distinct code, so
|
|
12
|
+
// the export endpoint's 500 body becomes the remediation instead of a stack. Any
|
|
13
|
+
// OTHER launch error is rethrown untouched — real diagnostics are preserved.
|
|
14
|
+
//
|
|
15
|
+
// See `.ai/logs/rca/issue-nefunguji-exporty.md`.
|
|
16
|
+
|
|
17
|
+
import { chromium } from 'playwright';
|
|
18
|
+
|
|
19
|
+
/** Exit code the shims surface for "Chromium browser binary not installed". */
|
|
20
|
+
export const NO_BROWSER_EXIT = 3;
|
|
21
|
+
|
|
22
|
+
/** One-line, copy-pasteable remediation shown in the export 500 body. */
|
|
23
|
+
export const INSTALL_HINT =
|
|
24
|
+
"Playwright's Chromium browser isn't installed. Run: npx playwright install chromium";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* True when a `chromium.launch()` rejection is the "browser binary not
|
|
28
|
+
* downloaded" case (as opposed to a sandbox / port / crash failure). Pure +
|
|
29
|
+
* exported so it's unit-testable without spawning a browser. Matches the stable
|
|
30
|
+
* fragments Playwright emits across versions:
|
|
31
|
+
* "browserType.launch: Executable doesn't exist at <path>"
|
|
32
|
+
* "Please run the following command to download new browsers"
|
|
33
|
+
* "playwright install"
|
|
34
|
+
*/
|
|
35
|
+
export function isMissingBrowserError(message) {
|
|
36
|
+
const m = String(message ?? '');
|
|
37
|
+
return (
|
|
38
|
+
/Executable doesn'?t exist/i.test(m) ||
|
|
39
|
+
/please run the following command to download/i.test(m) ||
|
|
40
|
+
/playwright install/i.test(m)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Launch Chromium, mapping the missing-binary failure to a clean exit. Drop-in
|
|
46
|
+
* for `chromium.launch(opts)` — same signature, same resolved Browser.
|
|
47
|
+
*/
|
|
48
|
+
export async function launchChromium(opts) {
|
|
49
|
+
try {
|
|
50
|
+
return await chromium.launch(opts);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
53
|
+
if (isMissingBrowserError(msg)) {
|
|
54
|
+
// Single clean line — the export endpoint pipes shim stderr into the 500
|
|
55
|
+
// body, so this is what the user reads. No stack trace.
|
|
56
|
+
console.error(INSTALL_HINT);
|
|
57
|
+
process.exit(NO_BROWSER_EXIT);
|
|
58
|
+
}
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { mkdirSync } from 'node:fs';
|
|
13
13
|
import { dirname } from 'node:path';
|
|
14
|
-
import {
|
|
14
|
+
import { launchChromium } from './_pw-launch.mjs';
|
|
15
15
|
|
|
16
16
|
const args = Object.fromEntries(
|
|
17
17
|
process.argv.slice(2).reduce((acc, cur, i, all) => {
|
|
@@ -31,7 +31,7 @@ if (!url || !out) {
|
|
|
31
31
|
const timeoutMs = Number(timeout) * 1000;
|
|
32
32
|
mkdirSync(dirname(out), { recursive: true });
|
|
33
33
|
|
|
34
|
-
const browser = await
|
|
34
|
+
const browser = await launchChromium();
|
|
35
35
|
try {
|
|
36
36
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
|
37
37
|
const page = await ctx.newPage();
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import { dirname, join } from 'node:path';
|
|
21
|
-
import {
|
|
21
|
+
import { launchChromium } from './_pw-launch.mjs';
|
|
22
22
|
|
|
23
23
|
const args = Object.fromEntries(
|
|
24
24
|
process.argv.slice(2).reduce((acc, cur, i, all) => {
|
|
@@ -49,7 +49,7 @@ const widen = widenFlag !== undefined;
|
|
|
49
49
|
const multi = multiFlag !== undefined;
|
|
50
50
|
const timeoutMs = Number(timeout) * 1000;
|
|
51
51
|
|
|
52
|
-
const browser = await
|
|
52
|
+
const browser = await launchChromium();
|
|
53
53
|
try {
|
|
54
54
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
|
55
55
|
const page = await ctx.newPage();
|
|
@@ -74,27 +74,142 @@ try {
|
|
|
74
74
|
|
|
75
75
|
const written = [];
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
// Single in-page serializer used by BOTH branches (single + multi) so they
|
|
78
|
+
// can't drift again (the `formatXML` 500 bug came from a divergent copy).
|
|
79
|
+
// Note: dom-to-svg exports only elementToSVG / inlineResources /
|
|
80
|
+
// documentToSVG — there is NO `formatXML` pretty-printer; serialize straight
|
|
81
|
+
// to a string. dom-to-svg also does NOT paint the captured root's background
|
|
82
|
+
// fill, so the artboard's background was dropped (item 4) — we prepend a
|
|
83
|
+
// backdrop <rect> from the artboard's effective (visible) background color.
|
|
84
|
+
const serializeOne = async (handle, widenToArtboard) => {
|
|
78
85
|
return await handle.evaluate(
|
|
79
86
|
async (el, opts) => {
|
|
80
87
|
const target = opts.widenToArtboard ? (el.closest('[data-dc-screen]') ?? el) : el;
|
|
81
88
|
// window.domToSvg is the IIFE-injected entry.
|
|
82
89
|
const { elementToSVG, inlineResources } = /** @type any */ (window).domToSvg;
|
|
90
|
+
|
|
91
|
+
// Convert ANY computed color (modern Chromium serializes `oklch(...)`
|
|
92
|
+
// verbatim from getComputedStyle) to sRGB rgb()/rgba(). Affinity
|
|
93
|
+
// Designer / older Illustrator / Inkscape don't parse CSS Color 4
|
|
94
|
+
// (`oklch`, `color()`) and silently drop the fill — the artboard then
|
|
95
|
+
// shows as transparent (the exact symptom: bg vanished in Affinity).
|
|
96
|
+
// A 1×1 canvas normalizes any CSS color string to sRGB bytes. Returns
|
|
97
|
+
// null if the value can't be painted (keep the rect off rather than
|
|
98
|
+
// emit a wrong colour).
|
|
99
|
+
const _srgbCache = new Map();
|
|
100
|
+
const _srgbCanvas = document.createElement('canvas');
|
|
101
|
+
_srgbCanvas.width = 1;
|
|
102
|
+
_srgbCanvas.height = 1;
|
|
103
|
+
const _srgbCtx = _srgbCanvas.getContext('2d');
|
|
104
|
+
const toSrgb = (color) => {
|
|
105
|
+
if (_srgbCache.has(color)) return _srgbCache.get(color);
|
|
106
|
+
let result = null;
|
|
107
|
+
try {
|
|
108
|
+
if (_srgbCtx) {
|
|
109
|
+
_srgbCtx.clearRect(0, 0, 1, 1);
|
|
110
|
+
_srgbCtx.fillStyle = '#000';
|
|
111
|
+
_srgbCtx.fillStyle = color; // ignored (stays #000) if syntax unsupported
|
|
112
|
+
_srgbCtx.fillRect(0, 0, 1, 1);
|
|
113
|
+
const [r, g, b, a] = _srgbCtx.getImageData(0, 0, 1, 1).data;
|
|
114
|
+
if (a !== 0) {
|
|
115
|
+
result =
|
|
116
|
+
a === 255
|
|
117
|
+
? `rgb(${r}, ${g}, ${b})`
|
|
118
|
+
: `rgba(${r}, ${g}, ${b}, ${(a / 255).toFixed(3)})`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
result = null;
|
|
123
|
+
}
|
|
124
|
+
_srgbCache.set(color, result);
|
|
125
|
+
return result;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const isOpaque = (bg) =>
|
|
129
|
+
!!bg &&
|
|
130
|
+
bg !== 'transparent' &&
|
|
131
|
+
!/^rgba\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\)$/.test(bg) &&
|
|
132
|
+
!/^rgba\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0(\.0+)?\s*\)$/.test(bg);
|
|
133
|
+
// Effective background of the artboard region: the DS theme bg usually
|
|
134
|
+
// sits on an inner full-bleed wrapper (`.app` / `.mdcc`), occasionally
|
|
135
|
+
// on the artboard element itself, sometimes on body. Descend first to
|
|
136
|
+
// find a near-full-bleed opaque bg, then fall back to ancestors. An
|
|
137
|
+
// intentionally transparent artboard returns null → stays transparent.
|
|
138
|
+
const effectiveBg = (node) => {
|
|
139
|
+
const abRect = node.getBoundingClientRect();
|
|
140
|
+
const abArea = Math.max(1, abRect.width * abRect.height);
|
|
141
|
+
const queue = [node];
|
|
142
|
+
let guard = 0;
|
|
143
|
+
while (queue.length && guard < 80) {
|
|
144
|
+
const e = queue.shift();
|
|
145
|
+
guard += 1;
|
|
146
|
+
const bg = getComputedStyle(e).backgroundColor;
|
|
147
|
+
if (isOpaque(bg)) {
|
|
148
|
+
const r = e.getBoundingClientRect();
|
|
149
|
+
if (r.width * r.height >= abArea * 0.6) return bg;
|
|
150
|
+
}
|
|
151
|
+
for (const c of e.children) queue.push(c);
|
|
152
|
+
}
|
|
153
|
+
let cur = node.parentElement;
|
|
154
|
+
let up = 0;
|
|
155
|
+
while (cur && up < 12) {
|
|
156
|
+
const bg = getComputedStyle(cur).backgroundColor;
|
|
157
|
+
if (isOpaque(bg)) return bg;
|
|
158
|
+
cur = cur.parentElement;
|
|
159
|
+
up += 1;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const bg = effectiveBg(target);
|
|
165
|
+
// Normalize to sRGB so the fill survives in vector editors (item 4 /
|
|
166
|
+
// oklch follow-up). If conversion fails, skip the backdrop rather than
|
|
167
|
+
// emit an unparseable fill.
|
|
168
|
+
const fillColor = bg ? toSrgb(bg) : null;
|
|
83
169
|
const svgDoc = elementToSVG(target);
|
|
170
|
+
const root = svgDoc.documentElement;
|
|
171
|
+
if (fillColor) {
|
|
172
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
173
|
+
const rect = svgDoc.createElementNS(NS, 'rect');
|
|
174
|
+
const vb = (root.getAttribute('viewBox') || '').split(/\s+/).map(Number);
|
|
175
|
+
if (vb.length === 4 && vb.every((n) => !Number.isNaN(n))) {
|
|
176
|
+
rect.setAttribute('x', String(vb[0]));
|
|
177
|
+
rect.setAttribute('y', String(vb[1]));
|
|
178
|
+
rect.setAttribute('width', String(vb[2]));
|
|
179
|
+
rect.setAttribute('height', String(vb[3]));
|
|
180
|
+
} else {
|
|
181
|
+
rect.setAttribute('x', '0');
|
|
182
|
+
rect.setAttribute('y', '0');
|
|
183
|
+
rect.setAttribute('width', '100%');
|
|
184
|
+
rect.setAttribute('height', '100%');
|
|
185
|
+
}
|
|
186
|
+
rect.setAttribute('fill', fillColor);
|
|
187
|
+
root.insertBefore(rect, root.firstChild);
|
|
188
|
+
}
|
|
84
189
|
// base64-embeds fonts + images so the SVG is portable outside the
|
|
85
190
|
// dev-server origin. Some external fetches fail silently — Affinity
|
|
86
191
|
// tolerates missing resources better than missing primitives.
|
|
87
192
|
try {
|
|
88
|
-
await inlineResources(
|
|
193
|
+
await inlineResources(root);
|
|
89
194
|
} catch {
|
|
90
195
|
/* best-effort */
|
|
91
196
|
}
|
|
92
|
-
|
|
197
|
+
let out = new XMLSerializer().serializeToString(svgDoc);
|
|
198
|
+
// dom-to-svg copies computed colours through VERBATIM, and modern
|
|
199
|
+
// Chromium serializes DS tokens as `oklch(...)` — Affinity Designer /
|
|
200
|
+
// older Illustrator / Inkscape can't parse CSS Color 4 and drop every
|
|
201
|
+
// such fill/stroke (the artboard bg + many element colours vanish).
|
|
202
|
+
// Rewrite every CSS Color 4 colour function (oklch/oklab/lch/lab/color)
|
|
203
|
+
// anywhere in the serialized SVG to its sRGB rgb()/rgba() equivalent.
|
|
204
|
+
// Each token is a standalone colour (no nested parens), so a single
|
|
205
|
+
// `\([^)]*\)` match is safe; toSrgb returns null on an unconvertible
|
|
206
|
+
// value and we keep the original rather than corrupt it.
|
|
207
|
+
out = out.replace(/(?:oklch|oklab|lch|lab|color)\([^)]*\)/gi, (m) => toSrgb(m) || m);
|
|
208
|
+
return out;
|
|
93
209
|
},
|
|
94
|
-
{ widenToArtboard
|
|
210
|
+
{ widenToArtboard }
|
|
95
211
|
);
|
|
96
212
|
};
|
|
97
|
-
const opts = { widenToArtboard: widen };
|
|
98
213
|
|
|
99
214
|
if (multi) {
|
|
100
215
|
if (!outDir) {
|
|
@@ -106,17 +221,8 @@ try {
|
|
|
106
221
|
for (let i = 0; i < screens.length; i += 1) {
|
|
107
222
|
const handle = screens[i];
|
|
108
223
|
const id = (await handle.getAttribute('data-dc-screen')) ?? `artboard-${i + 1}`;
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
});
|
|
224
|
+
// Each multi handle is already a `[data-dc-screen]` artboard — no widen.
|
|
225
|
+
const svg = await serializeOne(handle, false);
|
|
120
226
|
const target = join(outDir, `${id}.svg`);
|
|
121
227
|
writeFileSync(target, svg, 'utf8');
|
|
122
228
|
written.push(target);
|
|
@@ -129,7 +235,7 @@ try {
|
|
|
129
235
|
mkdirSync(dirname(out), { recursive: true });
|
|
130
236
|
const handle = page.locator(selector ?? '[data-dc-screen]:first-of-type').first();
|
|
131
237
|
await handle.waitFor({ state: 'visible', timeout: timeoutMs });
|
|
132
|
-
const svg = await serializeOne(handle);
|
|
238
|
+
const svg = await serializeOne(handle, widen);
|
|
133
239
|
writeFileSync(out, svg, 'utf8');
|
|
134
240
|
written.push(out);
|
|
135
241
|
}
|