@1agh/maude 0.41.0 → 0.42.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/apps/studio/api.ts +0 -41
- package/apps/studio/bin/_html-playwright.mjs +26 -4
- package/apps/studio/bin/_pdf-playwright.mjs +13 -2
- package/apps/studio/bin/_png-playwright.mjs +15 -2
- package/apps/studio/bin/_pptx-playwright.mjs +17 -4
- package/apps/studio/bin/_screenshot-playwright.mjs +17 -2
- package/apps/studio/bin/_svg-playwright.mjs +26 -4
- package/apps/studio/bin/screenshot.sh +53 -4
- package/apps/studio/client/app.jsx +25 -54
- package/apps/studio/client/export-center.jsx +426 -0
- package/apps/studio/client/styles/3-shell-maude.css +17 -0
- package/apps/studio/client/styles/4-components.css +150 -0
- package/apps/studio/config.schema.json +2 -2
- package/apps/studio/dist/client.bundle.js +17 -17
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/export-dialog.tsx +18 -25
- package/apps/studio/exporters/_runtime.ts +104 -0
- package/apps/studio/exporters/html.ts +12 -20
- package/apps/studio/exporters/index.ts +14 -2
- package/apps/studio/exporters/jobs.ts +334 -0
- package/apps/studio/exporters/pdf.ts +16 -20
- package/apps/studio/exporters/png.ts +12 -20
- package/apps/studio/exporters/pptx.ts +22 -23
- package/apps/studio/exporters/scope.ts +1 -0
- package/apps/studio/exporters/svg.ts +14 -22
- package/apps/studio/exporters/video.ts +15 -17
- package/apps/studio/git/service.ts +3 -1
- package/apps/studio/http.ts +145 -50
- package/apps/studio/server.ts +3 -1
- package/apps/studio/test/canvas-origin-gate.test.ts +6 -0
- package/apps/studio/test/dns-rebinding-guard.test.ts +27 -0
- package/apps/studio/test/export-center.test.tsx +287 -0
- package/apps/studio/test/export-shim-multi-capture.test.ts +83 -0
- package/apps/studio/test/exporters/history.test.ts +32 -3
- package/apps/studio/test/exporters/jobs.test.ts +263 -0
- package/apps/studio/whats-new.json +17 -0
- package/apps/studio/ws.ts +6 -0
- package/cli/lib/gitignore-block.mjs +1 -0
- package/package.json +8 -8
package/apps/studio/api.ts
CHANGED
|
@@ -180,16 +180,6 @@ export interface GitCommitter {
|
|
|
180
180
|
commits: number;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
// Phase 6.5 T10 — export history. Five-deep ring buffer of recent exports
|
|
184
|
-
// surfaced by the dialog's "Recent" tab + ⌘⇧E re-run.
|
|
185
|
-
export interface ExportHistoryEntry {
|
|
186
|
-
format: string;
|
|
187
|
-
scope: string;
|
|
188
|
-
options?: Record<string, unknown>;
|
|
189
|
-
filename: string;
|
|
190
|
-
at: string;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
183
|
export type CreateCanvasResult =
|
|
194
184
|
| { ok: true; file: string; rel: string; slug: string }
|
|
195
185
|
| { ok: false; status: number; error: string };
|
|
@@ -465,9 +455,6 @@ export interface Api {
|
|
|
465
455
|
// Aggregate data
|
|
466
456
|
buildIndexData(): Promise<unknown>;
|
|
467
457
|
buildSystemData(dsName?: string | null): Promise<unknown>;
|
|
468
|
-
// Export history (Phase 6.5 T10)
|
|
469
|
-
loadExportHistory(): Promise<ExportHistoryEntry[]>;
|
|
470
|
-
appendExportHistory(entry: ExportHistoryEntry): Promise<void>;
|
|
471
458
|
}
|
|
472
459
|
|
|
473
460
|
export interface ApiHooks {
|
|
@@ -3006,32 +2993,6 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
3006
2993
|
};
|
|
3007
2994
|
}
|
|
3008
2995
|
|
|
3009
|
-
// ---------- Export history (Phase 6.5 T10) ----------
|
|
3010
|
-
//
|
|
3011
|
-
// 5-deep ring buffer persisted at `<designRoot>/_export-history.json`.
|
|
3012
|
-
// Reads tolerate missing / malformed files (returns []). Writes truncate
|
|
3013
|
-
// to most-recent-first.
|
|
3014
|
-
|
|
3015
|
-
const HISTORY_PATH = path.join(paths.designRoot, '_export-history.json');
|
|
3016
|
-
const HISTORY_DEPTH = 5;
|
|
3017
|
-
|
|
3018
|
-
async function loadExportHistory(): Promise<ExportHistoryEntry[]> {
|
|
3019
|
-
try {
|
|
3020
|
-
const raw = await Bun.file(HISTORY_PATH).text();
|
|
3021
|
-
const arr = JSON.parse(raw);
|
|
3022
|
-
if (!Array.isArray(arr)) return [];
|
|
3023
|
-
return arr.slice(0, HISTORY_DEPTH);
|
|
3024
|
-
} catch {
|
|
3025
|
-
return [];
|
|
3026
|
-
}
|
|
3027
|
-
}
|
|
3028
|
-
|
|
3029
|
-
async function appendExportHistory(entry: ExportHistoryEntry): Promise<void> {
|
|
3030
|
-
const prev = await loadExportHistory();
|
|
3031
|
-
const next = [entry, ...prev].slice(0, HISTORY_DEPTH);
|
|
3032
|
-
await Bun.write(HISTORY_PATH, JSON.stringify(next, null, 2));
|
|
3033
|
-
}
|
|
3034
|
-
|
|
3035
2996
|
function tokenKind(name: string, value: string): string {
|
|
3036
2997
|
const n = name.toLowerCase();
|
|
3037
2998
|
const v = String(value).trim();
|
|
@@ -3273,7 +3234,5 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
3273
3234
|
reorderRevert,
|
|
3274
3235
|
buildIndexData,
|
|
3275
3236
|
buildSystemData,
|
|
3276
|
-
loadExportHistory,
|
|
3277
|
-
appendExportHistory,
|
|
3278
3237
|
};
|
|
3279
3238
|
}
|
|
@@ -62,14 +62,31 @@ try {
|
|
|
62
62
|
world.style.zoom = '1';
|
|
63
63
|
world.style.transform = 'none';
|
|
64
64
|
}
|
|
65
|
-
for (const el of document.querySelectorAll('[data-dc-screen]')) {
|
|
66
|
-
el.style.left = '0px';
|
|
67
|
-
el.style.top = '0px';
|
|
68
|
-
}
|
|
69
65
|
});
|
|
70
66
|
|
|
71
67
|
const written = [];
|
|
72
68
|
|
|
69
|
+
// Pin one artboard to (0,0) for its capture, returning a restore function.
|
|
70
|
+
// Per-target rather than a one-shot reset of every `[data-dc-screen]` — a
|
|
71
|
+
// multi-target loop must not leave an already-captured artboard sitting at
|
|
72
|
+
// the origin, overlapping the next target's geometry (the scatter bug).
|
|
73
|
+
const pinArtboard = async (handle) => {
|
|
74
|
+
const saved = await handle.evaluate((el) => {
|
|
75
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
76
|
+
const prev = { left: ab.style.left, top: ab.style.top };
|
|
77
|
+
ab.style.left = '0px';
|
|
78
|
+
ab.style.top = '0px';
|
|
79
|
+
return prev;
|
|
80
|
+
});
|
|
81
|
+
return async () => {
|
|
82
|
+
await handle.evaluate((el, prev) => {
|
|
83
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
84
|
+
ab.style.left = prev.left;
|
|
85
|
+
ab.style.top = prev.top;
|
|
86
|
+
}, saved);
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
73
90
|
if (multi) {
|
|
74
91
|
if (!outDir) {
|
|
75
92
|
console.error('_html-playwright: --multi requires --out-dir');
|
|
@@ -80,10 +97,13 @@ try {
|
|
|
80
97
|
for (let i = 0; i < screens.length; i += 1) {
|
|
81
98
|
const handle = screens[i];
|
|
82
99
|
const id = (await handle.getAttribute('data-dc-screen')) ?? `artboard-${i + 1}`;
|
|
100
|
+
const restore = await pinArtboard(handle);
|
|
83
101
|
const html = await serializeOne(handle, false);
|
|
102
|
+
await restore();
|
|
84
103
|
const target = join(outDir, `${id}.html`);
|
|
85
104
|
writeFileSync(target, html, 'utf8');
|
|
86
105
|
written.push(target);
|
|
106
|
+
console.log(`MAUDE_PROGRESS {"current":${i + 1},"total":${screens.length}}`);
|
|
87
107
|
}
|
|
88
108
|
} else {
|
|
89
109
|
if (!out) {
|
|
@@ -93,7 +113,9 @@ try {
|
|
|
93
113
|
mkdirSync(dirname(out), { recursive: true });
|
|
94
114
|
const handle = page.locator(selector ?? '[data-dc-screen]:first-of-type').first();
|
|
95
115
|
await handle.waitFor({ state: 'visible', timeout: timeoutMs });
|
|
116
|
+
const restore = await pinArtboard(handle);
|
|
96
117
|
const html = await serializeOne(handle, widen);
|
|
118
|
+
await restore();
|
|
97
119
|
writeFileSync(out, html, 'utf8');
|
|
98
120
|
written.push(out);
|
|
99
121
|
}
|
|
@@ -88,11 +88,15 @@ try {
|
|
|
88
88
|
: screens[i];
|
|
89
89
|
// Pin the captured element's own artboard to (0,0) right before its
|
|
90
90
|
// capture so the world's multi-artboard layout doesn't push the bbox off
|
|
91
|
-
// the viewport.
|
|
92
|
-
|
|
91
|
+
// the viewport. Save the prior value so it can be restored right after
|
|
92
|
+
// this artboard's PDF is written — otherwise every artboard captured
|
|
93
|
+
// earlier in the loop stays stacked at (0,0) and bleeds into later pages.
|
|
94
|
+
const savedPos = await handle.evaluate((el) => {
|
|
93
95
|
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
96
|
+
const prev = { left: ab.style.left, top: ab.style.top };
|
|
94
97
|
ab.style.left = '0px';
|
|
95
98
|
ab.style.top = '0px';
|
|
99
|
+
return prev;
|
|
96
100
|
});
|
|
97
101
|
const rect = await handle.evaluate((el) => {
|
|
98
102
|
const r = el.getBoundingClientRect();
|
|
@@ -120,6 +124,13 @@ try {
|
|
|
120
124
|
});
|
|
121
125
|
writeFileSync(targetPath, pdf);
|
|
122
126
|
written.push(targetPath);
|
|
127
|
+
// Restore the artboard's original position now that its page is written.
|
|
128
|
+
await handle.evaluate((el, prev) => {
|
|
129
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
130
|
+
ab.style.left = prev.left;
|
|
131
|
+
ab.style.top = prev.top;
|
|
132
|
+
}, savedPos);
|
|
133
|
+
if (multi) console.log(`MAUDE_PROGRESS {"current":${i + 1},"total":${screens.length}}`);
|
|
123
134
|
}
|
|
124
135
|
|
|
125
136
|
for (const w of written) console.log(w);
|
|
@@ -82,11 +82,16 @@ try {
|
|
|
82
82
|
// artboard #2 (or any non-first selection's host) left the wrong artboard
|
|
83
83
|
// at the origin (item 5 root cause). Each artboard carries
|
|
84
84
|
// `style="left:…; top:…;"` for the world layout — zero the right one so the
|
|
85
|
-
// screenshot clip starts at the viewport origin.
|
|
86
|
-
|
|
85
|
+
// screenshot clip starts at the viewport origin. Save the prior value so it
|
|
86
|
+
// can be restored after this artboard's capture — otherwise every artboard
|
|
87
|
+
// processed earlier in a `--multi` loop is left stacked at (0,0) and bleeds
|
|
88
|
+
// into every subsequent target's clip region (the scatter bug).
|
|
89
|
+
const savedPos = await widenedHandle.evaluate((el) => {
|
|
87
90
|
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
91
|
+
const prev = { left: ab.style.left, top: ab.style.top };
|
|
88
92
|
ab.style.left = '0px';
|
|
89
93
|
ab.style.top = '0px';
|
|
94
|
+
return prev;
|
|
90
95
|
});
|
|
91
96
|
const rect = await widenedHandle.evaluate((el) => {
|
|
92
97
|
const r = el.getBoundingClientRect();
|
|
@@ -118,6 +123,13 @@ try {
|
|
|
118
123
|
},
|
|
119
124
|
});
|
|
120
125
|
written.push(target);
|
|
126
|
+
// Restore the artboard's original position now that its capture is done,
|
|
127
|
+
// so it doesn't stay pinned at (0,0) and corrupt the next target's clip.
|
|
128
|
+
await widenedHandle.evaluate((el, prev) => {
|
|
129
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
130
|
+
ab.style.left = prev.left;
|
|
131
|
+
ab.style.top = prev.top;
|
|
132
|
+
}, savedPos);
|
|
121
133
|
};
|
|
122
134
|
|
|
123
135
|
if (multi) {
|
|
@@ -131,6 +143,7 @@ try {
|
|
|
131
143
|
const handle = screens[i];
|
|
132
144
|
const id = (await handle.getAttribute('data-dc-screen')) ?? `artboard-${i + 1}`;
|
|
133
145
|
await captureHandle(handle, join(outDir, `${id}.png`));
|
|
146
|
+
console.log(`MAUDE_PROGRESS {"current":${i + 1},"total":${screens.length}}`);
|
|
134
147
|
}
|
|
135
148
|
} else {
|
|
136
149
|
if (!out) {
|
|
@@ -48,10 +48,6 @@ try {
|
|
|
48
48
|
world.style.zoom = '1';
|
|
49
49
|
world.style.transform = 'none';
|
|
50
50
|
}
|
|
51
|
-
for (const el of document.querySelectorAll('[data-dc-screen]')) {
|
|
52
|
-
el.style.left = '0px';
|
|
53
|
-
el.style.top = '0px';
|
|
54
|
-
}
|
|
55
51
|
});
|
|
56
52
|
// dom-to-pptx ships a UMD bundle; addScriptTag exposes `window.domToPptx`.
|
|
57
53
|
await page.addScriptTag({ path: bundlePath });
|
|
@@ -74,6 +70,17 @@ try {
|
|
|
74
70
|
}
|
|
75
71
|
await handle.waitFor({ state: 'visible', timeout: timeoutMs });
|
|
76
72
|
|
|
73
|
+
// Pin this artboard to (0,0) for its capture, restoring afterwards so a
|
|
74
|
+
// future multi-slide implementation doesn't inherit a stacked-at-origin bug
|
|
75
|
+
// (the scatter bug fixed in the png/pdf/svg/html shims).
|
|
76
|
+
const savedPos = await handle.evaluate((el) => {
|
|
77
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
78
|
+
const prev = { left: ab.style.left, top: ab.style.top };
|
|
79
|
+
ab.style.left = '0px';
|
|
80
|
+
ab.style.top = '0px';
|
|
81
|
+
return prev;
|
|
82
|
+
});
|
|
83
|
+
|
|
77
84
|
// Run dom-to-pptx inside the page. It returns a Blob; we serialise to a
|
|
78
85
|
// byte array for transport across the playwright boundary.
|
|
79
86
|
const bytesArray = await handle.evaluate(async (el) => {
|
|
@@ -88,6 +95,12 @@ try {
|
|
|
88
95
|
return Array.from(new Uint8Array(ab));
|
|
89
96
|
});
|
|
90
97
|
|
|
98
|
+
await handle.evaluate((el, prev) => {
|
|
99
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
100
|
+
ab.style.left = prev.left;
|
|
101
|
+
ab.style.top = prev.top;
|
|
102
|
+
}, savedPos);
|
|
103
|
+
|
|
91
104
|
writeFileSync(out, new Uint8Array(bytesArray));
|
|
92
105
|
console.log(out);
|
|
93
106
|
console.error(
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Invocation (called by screenshot.sh — not directly by users):
|
|
7
7
|
// npm exec --package=playwright -- node _screenshot-playwright.mjs \
|
|
8
|
-
// --url <url> [--selector <css>] --out <path> [--timeout 8]
|
|
8
|
+
// --url <url> [--selector <css>] --out <path> [--timeout 8] [--theme <name>]
|
|
9
9
|
//
|
|
10
10
|
// First invocation may install chromium (~150 MB). Subsequent runs reuse cache.
|
|
11
11
|
|
|
@@ -20,7 +20,7 @@ const args = Object.fromEntries(
|
|
|
20
20
|
}, [])
|
|
21
21
|
);
|
|
22
22
|
|
|
23
|
-
const { url, selector, out, timeout = '8' } = args;
|
|
23
|
+
const { url, selector, out, timeout = '8', theme } = args;
|
|
24
24
|
if (!url || !out) {
|
|
25
25
|
console.error(
|
|
26
26
|
'usage: _screenshot-playwright.mjs --url <url> [--selector <css>] --out <path> [--timeout 8]'
|
|
@@ -37,6 +37,21 @@ try {
|
|
|
37
37
|
const page = await ctx.newPage();
|
|
38
38
|
await page.goto(url, { waitUntil: 'networkidle', timeout: timeoutMs });
|
|
39
39
|
|
|
40
|
+
if (theme) {
|
|
41
|
+
// Dual-theme reality check (/design:new step 9): force every DS artboard
|
|
42
|
+
// theme wrapper to `theme` before capture, deterministically, instead of
|
|
43
|
+
// relying on whatever the canvas is pinned to. No-op if the canvas has no
|
|
44
|
+
// `[data-theme]` elements (e.g. a single-theme DS).
|
|
45
|
+
const n = await page.evaluate((t) => {
|
|
46
|
+
const els = document.querySelectorAll('[data-theme]');
|
|
47
|
+
els.forEach((el) => {
|
|
48
|
+
el.setAttribute('data-theme', t);
|
|
49
|
+
});
|
|
50
|
+
return els.length;
|
|
51
|
+
}, theme);
|
|
52
|
+
console.error(`→ theme override: forced data-theme="${theme}" on ${n} element(s)`);
|
|
53
|
+
}
|
|
54
|
+
|
|
40
55
|
if (selector) {
|
|
41
56
|
const loc = page.locator(selector).first();
|
|
42
57
|
await loc.waitFor({ state: 'visible', timeout: timeoutMs });
|
|
@@ -70,10 +70,6 @@ try {
|
|
|
70
70
|
world.style.zoom = '1';
|
|
71
71
|
world.style.transform = 'none';
|
|
72
72
|
}
|
|
73
|
-
for (const el of document.querySelectorAll('[data-dc-screen]')) {
|
|
74
|
-
el.style.left = '0px';
|
|
75
|
-
el.style.top = '0px';
|
|
76
|
-
}
|
|
77
73
|
});
|
|
78
74
|
// Inject dom-to-svg into the page. Bundle attaches its exports under
|
|
79
75
|
// `window.domToSvg`.
|
|
@@ -81,6 +77,27 @@ try {
|
|
|
81
77
|
|
|
82
78
|
const written = [];
|
|
83
79
|
|
|
80
|
+
// Pin one artboard to (0,0) for its capture, returning a restore function.
|
|
81
|
+
// Per-target rather than a one-shot reset of every `[data-dc-screen]` — a
|
|
82
|
+
// multi-target loop must not leave an already-captured artboard sitting at
|
|
83
|
+
// the origin, overlapping the next target's geometry (the scatter bug).
|
|
84
|
+
const pinArtboard = async (handle) => {
|
|
85
|
+
const saved = await handle.evaluate((el) => {
|
|
86
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
87
|
+
const prev = { left: ab.style.left, top: ab.style.top };
|
|
88
|
+
ab.style.left = '0px';
|
|
89
|
+
ab.style.top = '0px';
|
|
90
|
+
return prev;
|
|
91
|
+
});
|
|
92
|
+
return async () => {
|
|
93
|
+
await handle.evaluate((el, prev) => {
|
|
94
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
95
|
+
ab.style.left = prev.left;
|
|
96
|
+
ab.style.top = prev.top;
|
|
97
|
+
}, saved);
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
84
101
|
// Single in-page serializer used by BOTH branches (single + multi) so they
|
|
85
102
|
// can't drift again (the `formatXML` 500 bug came from a divergent copy).
|
|
86
103
|
// Note: dom-to-svg exports only elementToSVG / inlineResources /
|
|
@@ -228,11 +245,14 @@ try {
|
|
|
228
245
|
for (let i = 0; i < screens.length; i += 1) {
|
|
229
246
|
const handle = screens[i];
|
|
230
247
|
const id = (await handle.getAttribute('data-dc-screen')) ?? `artboard-${i + 1}`;
|
|
248
|
+
const restore = await pinArtboard(handle);
|
|
231
249
|
// Each multi handle is already a `[data-dc-screen]` artboard — no widen.
|
|
232
250
|
const svg = await serializeOne(handle, false);
|
|
251
|
+
await restore();
|
|
233
252
|
const target = join(outDir, `${id}.svg`);
|
|
234
253
|
writeFileSync(target, svg, 'utf8');
|
|
235
254
|
written.push(target);
|
|
255
|
+
console.log(`MAUDE_PROGRESS {"current":${i + 1},"total":${screens.length}}`);
|
|
236
256
|
}
|
|
237
257
|
} else {
|
|
238
258
|
if (!out) {
|
|
@@ -242,7 +262,9 @@ try {
|
|
|
242
262
|
mkdirSync(dirname(out), { recursive: true });
|
|
243
263
|
const handle = page.locator(selector ?? '[data-dc-screen]:first-of-type').first();
|
|
244
264
|
await handle.waitFor({ state: 'visible', timeout: timeoutMs });
|
|
265
|
+
const restore = await pinArtboard(handle);
|
|
245
266
|
const svg = await serializeOne(handle, widen);
|
|
267
|
+
await restore();
|
|
246
268
|
writeFileSync(out, svg, 'utf8');
|
|
247
269
|
written.push(out);
|
|
248
270
|
}
|
|
@@ -9,12 +9,18 @@
|
|
|
9
9
|
# [--screen <id> | --element <id> | --selector <css> | --full]
|
|
10
10
|
# [--all-screens] [--out <path>] [--out-dir <dir>]
|
|
11
11
|
# [--timeout 8] [--engine auto|agent-browser|playwright]
|
|
12
|
-
# [--root <repo>]
|
|
12
|
+
# [--theme <name>] [--root <repo>]
|
|
13
13
|
#
|
|
14
14
|
# Notes:
|
|
15
15
|
# - Exactly one of --screen / --element / --selector / --full is required
|
|
16
16
|
# (or --all-screens which loops over every [data-dc-screen]/[data-dc-slot]).
|
|
17
17
|
# - --out required for single-shot modes; --out-dir required for --all-screens.
|
|
18
|
+
# - --theme <name> forces every `[data-theme]` element (DS artboard wrappers)
|
|
19
|
+
# to that value BEFORE capture, via a DOM eval — does not touch the actual
|
|
20
|
+
# canvas file or the running server's state. Used to capture a DS's
|
|
21
|
+
# alternate theme deterministically for the dual-theme reality check
|
|
22
|
+
# (/design:new step 9) instead of relying on whatever the canvas is pinned
|
|
23
|
+
# to. No-op if the canvas has no `[data-theme]` elements.
|
|
18
24
|
# - URL resolution: --url > --port + _active.json > _server.json + _active.json.
|
|
19
25
|
# - Stdout: written PNG paths, one per line (composable in for-loops).
|
|
20
26
|
# - Stderr: diagnostic, engine choice, timing.
|
|
@@ -30,6 +36,7 @@ TIMEOUT=8
|
|
|
30
36
|
ENGINE="auto"
|
|
31
37
|
ALL_SCREENS=0
|
|
32
38
|
ROOT=""
|
|
39
|
+
THEME=""
|
|
33
40
|
|
|
34
41
|
while [ $# -gt 0 ]; do
|
|
35
42
|
case "$1" in
|
|
@@ -44,9 +51,10 @@ while [ $# -gt 0 ]; do
|
|
|
44
51
|
--out-dir) OUT_DIR="$2"; shift 2 ;;
|
|
45
52
|
--timeout) TIMEOUT="$2"; shift 2 ;;
|
|
46
53
|
--engine) ENGINE="$2"; shift 2 ;;
|
|
54
|
+
--theme) THEME="$2"; shift 2 ;;
|
|
47
55
|
--root) ROOT="$2"; shift 2 ;;
|
|
48
56
|
--help|-h)
|
|
49
|
-
sed -n '2,
|
|
57
|
+
sed -n '2,27p' "$0" | sed 's/^# \?//'
|
|
50
58
|
exit 0
|
|
51
59
|
;;
|
|
52
60
|
*)
|
|
@@ -57,6 +65,20 @@ while [ $# -gt 0 ]; do
|
|
|
57
65
|
done
|
|
58
66
|
|
|
59
67
|
# ---------- arg validation ----------
|
|
68
|
+
# --theme ultimately traces back to config.json's designSystems[].themes[]
|
|
69
|
+
# (untrusted, same posture as prep.sh's MOODBOARD_VARIANTS). Callers (e.g.
|
|
70
|
+
# new.md step 9) build an --out-dir path from this value — reject anything
|
|
71
|
+
# outside a safe slug charset here too, at the shared sink, rather than
|
|
72
|
+
# trusting every caller to re-derive this guard.
|
|
73
|
+
if [ -n "$THEME" ]; then
|
|
74
|
+
case "$THEME" in
|
|
75
|
+
*[!A-Za-z0-9._-]*|"")
|
|
76
|
+
echo "screenshot.sh: --theme '$THEME' contains unsafe characters (allowed: A-Za-z0-9._-)" >&2
|
|
77
|
+
exit 2
|
|
78
|
+
;;
|
|
79
|
+
esac
|
|
80
|
+
fi
|
|
81
|
+
|
|
60
82
|
if [ $ALL_SCREENS -eq 1 ]; then
|
|
61
83
|
[ -z "$OUT_DIR" ] && { echo "screenshot.sh: --all-screens needs --out-dir" >&2; exit 2; }
|
|
62
84
|
else
|
|
@@ -189,10 +211,12 @@ pw_screenshot() {
|
|
|
189
211
|
return 1
|
|
190
212
|
fi
|
|
191
213
|
echo "→ playwright engine; first invocation may install chromium (~150MB, one-off)" >&2
|
|
214
|
+
local theme_args=()
|
|
215
|
+
[ -n "$THEME" ] && theme_args=(--theme "$THEME")
|
|
192
216
|
if [ -n "$css" ]; then
|
|
193
|
-
npm exec --yes --package=playwright -- node "$pw_script" --url "$URL" --selector "$css" --out "$out" --timeout "$TIMEOUT" >&2 || return 1
|
|
217
|
+
npm exec --yes --package=playwright -- node "$pw_script" --url "$URL" --selector "$css" --out "$out" --timeout "$TIMEOUT" "${theme_args[@]}" >&2 || return 1
|
|
194
218
|
else
|
|
195
|
-
npm exec --yes --package=playwright -- node "$pw_script" --url "$URL" --out "$out" --timeout "$TIMEOUT" >&2 || return 1
|
|
219
|
+
npm exec --yes --package=playwright -- node "$pw_script" --url "$URL" --out "$out" --timeout "$TIMEOUT" "${theme_args[@]}" >&2 || return 1
|
|
196
220
|
fi
|
|
197
221
|
[ -s "$out" ] || { echo "✗ playwright wrote no file: $out" >&2; return 1; }
|
|
198
222
|
return 0
|
|
@@ -224,9 +248,34 @@ navigate_once() {
|
|
|
224
248
|
echo "→ no DC mount detected after ${TIMEOUT}s — proceeding (may be a non-canvas page)" >&2
|
|
225
249
|
sleep 1
|
|
226
250
|
fi
|
|
251
|
+
apply_theme_override
|
|
227
252
|
fi
|
|
228
253
|
}
|
|
229
254
|
|
|
255
|
+
# ---------- --theme override (dual-theme reality check) ----------
|
|
256
|
+
# Forces every element carrying a `data-theme` attribute (DS artboard theme
|
|
257
|
+
# wrappers) to $THEME, so the caller can deterministically capture a DS's
|
|
258
|
+
# alternate theme instead of whatever the canvas happens to be pinned to.
|
|
259
|
+
# agent-browser only — the playwright path applies it inside
|
|
260
|
+
# _screenshot-playwright.mjs (passed via --theme) since that shim owns its own
|
|
261
|
+
# page.evaluate. No-op when --theme wasn't passed or no [data-theme] elements
|
|
262
|
+
# exist (e.g. a single-theme DS or a non-canvas page).
|
|
263
|
+
apply_theme_override() {
|
|
264
|
+
[ -z "$THEME" ] && return 0
|
|
265
|
+
[ "$ENGINE" = "agent-browser" ] || return 0
|
|
266
|
+
# THEME ultimately traces back to config.json's designSystems[].themes[]
|
|
267
|
+
# array — treated as untrusted input elsewhere in this codebase (see
|
|
268
|
+
# prep.sh's MOODBOARD_VARIANTS clamp). JSON-encode it via jq so it lands as
|
|
269
|
+
# a properly-escaped JS string literal in the eval'd expression below,
|
|
270
|
+
# instead of naively splicing the raw value into a JS string (which a
|
|
271
|
+
# crafted config value could break out of).
|
|
272
|
+
local theme_json
|
|
273
|
+
theme_json=$(jq -Rn --arg t "$THEME" '$t' 2>/dev/null) || return 0
|
|
274
|
+
local n
|
|
275
|
+
n=$("$AB" eval "(function(t){var els=document.querySelectorAll('[data-theme]');els.forEach(function(el){el.setAttribute('data-theme',t)});return els.length})($theme_json)" 2>/dev/null)
|
|
276
|
+
echo "→ theme override: forced data-theme=\"$THEME\" on $(printf '%s' "$n" | tr -d '[:space:]') element(s)" >&2
|
|
277
|
+
}
|
|
278
|
+
|
|
230
279
|
capture() {
|
|
231
280
|
local css="$1"
|
|
232
281
|
local out="$2"
|
|
@@ -29,11 +29,11 @@ import {
|
|
|
29
29
|
onUpdateReady,
|
|
30
30
|
pickMediaFile,
|
|
31
31
|
restartToUpdate,
|
|
32
|
-
saveExport,
|
|
33
32
|
} from './github.js';
|
|
34
33
|
import { COLLAB_TOUR } from './tour/collab-tour.js';
|
|
35
34
|
import { TourOverlay } from './tour/overlay.jsx';
|
|
36
35
|
import { USAGE_TOUR } from './tour/usage-tour.js';
|
|
36
|
+
import { ExportBadge, ExportPanel, ExportToast, useExportCenter } from './export-center.jsx';
|
|
37
37
|
import { useWhatsNew, WhatsNewPanel, WhatsNewToast } from './whats-new.jsx';
|
|
38
38
|
|
|
39
39
|
const USAGE_TOUR_STORE = 'mdcc-usage-tour-seen';
|
|
@@ -1079,7 +1079,10 @@ function ExportDialog({
|
|
|
1079
1079
|
if (activeArtboardId) options.artboardId = activeArtboardId;
|
|
1080
1080
|
if (selection?.selector) options.selection = selection;
|
|
1081
1081
|
try {
|
|
1082
|
-
|
|
1082
|
+
// feature-background-export-notification-center — enqueue and close
|
|
1083
|
+
// immediately; the menubar notification center owns status, progress,
|
|
1084
|
+
// and completion (download / native Save…) from here on.
|
|
1085
|
+
const r = await fetch('/_api/export-jobs', {
|
|
1083
1086
|
method: 'POST',
|
|
1084
1087
|
headers: { 'content-type': 'application/json' },
|
|
1085
1088
|
body: JSON.stringify({ format: card.format, scope, options }),
|
|
@@ -1089,38 +1092,11 @@ function ExportDialog({
|
|
|
1089
1092
|
setBusy(false);
|
|
1090
1093
|
return;
|
|
1091
1094
|
}
|
|
1092
|
-
|
|
1093
|
-
const fn = /filename="([^"]+)"/.exec(disp);
|
|
1094
|
-
const filename = (fn && fn[1]) || `export.${card.format}`;
|
|
1095
|
-
const blob = await r.blob();
|
|
1096
|
-
if (isNativeApp()) {
|
|
1097
|
-
// Native: WKWebView swallows the `<a download>` blob (the file lands in
|
|
1098
|
-
// an unknown default with no prompt). Route through a real OS save dialog
|
|
1099
|
-
// so the export OFFERS a location. Cancel → soft no-op.
|
|
1100
|
-
const bytes = Array.from(new Uint8Array(await blob.arrayBuffer()));
|
|
1101
|
-
const savedPath = await saveExport(filename, bytes);
|
|
1102
|
-
if (savedPath) {
|
|
1103
|
-
setStatus({ ok: true, msg: `Saved to ${savedPath}` });
|
|
1104
|
-
loadRecent();
|
|
1105
|
-
} else {
|
|
1106
|
-
setStatus({ ok: true, msg: 'Save cancelled' });
|
|
1107
|
-
}
|
|
1108
|
-
} else {
|
|
1109
|
-
const url = URL.createObjectURL(blob);
|
|
1110
|
-
const a = document.createElement('a');
|
|
1111
|
-
a.href = url;
|
|
1112
|
-
a.download = filename;
|
|
1113
|
-
document.body.appendChild(a);
|
|
1114
|
-
a.click();
|
|
1115
|
-
a.remove();
|
|
1116
|
-
URL.revokeObjectURL(url);
|
|
1117
|
-
setStatus({ ok: true, msg: `Exported ${filename}` });
|
|
1118
|
-
loadRecent();
|
|
1119
|
-
}
|
|
1095
|
+
onClose();
|
|
1120
1096
|
} catch (err) {
|
|
1121
1097
|
setStatus({ ok: false, msg: err && err.message ? err.message : String(err) });
|
|
1098
|
+
setBusy(false);
|
|
1122
1099
|
}
|
|
1123
|
-
setBusy(false);
|
|
1124
1100
|
}
|
|
1125
1101
|
|
|
1126
1102
|
return (
|
|
@@ -2680,6 +2656,7 @@ function Menubar({
|
|
|
2680
2656
|
onOpenWhatsNew,
|
|
2681
2657
|
onOpenReadiness,
|
|
2682
2658
|
whatsNewCount,
|
|
2659
|
+
exportCenter,
|
|
2683
2660
|
artboardCount = 0,
|
|
2684
2661
|
presence = null,
|
|
2685
2662
|
inspectorOpen,
|
|
@@ -2993,6 +2970,7 @@ function Menubar({
|
|
|
2993
2970
|
<StIcon name="sparkle" size={15} />
|
|
2994
2971
|
</button>
|
|
2995
2972
|
)}
|
|
2973
|
+
{exportCenter && <ExportBadge center={exportCenter} />}
|
|
2996
2974
|
<button
|
|
2997
2975
|
type="button"
|
|
2998
2976
|
className="st-whatsnew"
|
|
@@ -7049,6 +7027,7 @@ function App() {
|
|
|
7049
7027
|
// toggles on its own and coexists with Inspector/Changes/Comments/Chat.
|
|
7050
7028
|
const toggleTimeline = useCallback(() => setTimelineOpen((v) => !v), []);
|
|
7051
7029
|
const whatsNew = useWhatsNew(MDCC_VERSION);
|
|
7030
|
+
const exportCenter = useExportCenter();
|
|
7052
7031
|
// Phase 29 (E4) — first-run onboarding wizard. The native shell boots a minimal
|
|
7053
7032
|
// "welcome" project on first launch; we ask it whether this is a first run and, if
|
|
7054
7033
|
// so, show the wizard OVER the (empty) canvas browser. Completing any door switches
|
|
@@ -7696,6 +7675,10 @@ function App() {
|
|
|
7696
7675
|
);
|
|
7697
7676
|
} catch {}
|
|
7698
7677
|
}
|
|
7678
|
+
} else if (m.type === 'export:job' && m.payload) {
|
|
7679
|
+
// feature-background-export-notification-center — full-snapshot
|
|
7680
|
+
// job state on every queued/running/progress/done/failed change.
|
|
7681
|
+
exportCenter.upsert(m.payload);
|
|
7699
7682
|
} else if (m.type === 'sync:status' && m.payload) {
|
|
7700
7683
|
// Phase 9 Task 8 — hub connection state for the offline banner.
|
|
7701
7684
|
setSyncStatus(m.payload);
|
|
@@ -8854,7 +8837,12 @@ function App() {
|
|
|
8854
8837
|
} catch {}
|
|
8855
8838
|
};
|
|
8856
8839
|
try {
|
|
8857
|
-
|
|
8840
|
+
// feature-background-export-notification-center — enqueue and reply
|
|
8841
|
+
// with the job id immediately; the notification center (which
|
|
8842
|
+
// already owns the WS connection) is the single place status/
|
|
8843
|
+
// progress/completion live from here, regardless of which dialog
|
|
8844
|
+
// created the job.
|
|
8845
|
+
const r = await fetch('/_api/export-jobs', {
|
|
8858
8846
|
method: 'POST',
|
|
8859
8847
|
headers: { 'content-type': 'application/json' },
|
|
8860
8848
|
body: JSON.stringify(payload),
|
|
@@ -8863,28 +8851,8 @@ function App() {
|
|
|
8863
8851
|
reply({ ok: false, error: (await r.text()) || String(r.status) });
|
|
8864
8852
|
return;
|
|
8865
8853
|
}
|
|
8866
|
-
const
|
|
8867
|
-
|
|
8868
|
-
const filename = (fn && fn[1]) || 'export';
|
|
8869
|
-
const blob = await r.blob();
|
|
8870
|
-
if (isNativeApp()) {
|
|
8871
|
-
// Native: route the bridged in-canvas export through the OS save dialog
|
|
8872
|
-
// too (WKWebView swallows the `<a download>` blob). Mirror the shell
|
|
8873
|
-
// ExportDialog's doExport native branch.
|
|
8874
|
-
const bytes = Array.from(new Uint8Array(await blob.arrayBuffer()));
|
|
8875
|
-
const savedPath = await saveExport(filename, bytes);
|
|
8876
|
-
reply(savedPath ? { ok: true, filename } : { ok: false, error: 'Save cancelled' });
|
|
8877
|
-
return;
|
|
8878
|
-
}
|
|
8879
|
-
const url = URL.createObjectURL(blob);
|
|
8880
|
-
const a = document.createElement('a');
|
|
8881
|
-
a.href = url;
|
|
8882
|
-
a.download = filename;
|
|
8883
|
-
document.body.appendChild(a);
|
|
8884
|
-
a.click();
|
|
8885
|
-
a.remove();
|
|
8886
|
-
URL.revokeObjectURL(url);
|
|
8887
|
-
reply({ ok: true, filename });
|
|
8854
|
+
const { jobId } = await r.json();
|
|
8855
|
+
reply({ ok: true, jobId });
|
|
8888
8856
|
} catch (err) {
|
|
8889
8857
|
reply({ ok: false, error: err && err.message ? err.message : String(err) });
|
|
8890
8858
|
}
|
|
@@ -10011,6 +9979,7 @@ function App() {
|
|
|
10011
9979
|
<UpdateBanner update={updateReady} onDismiss={() => setUpdateReady(null)} />
|
|
10012
9980
|
<SyncBanner status={syncStatus} />
|
|
10013
9981
|
{!usageNudge && !tourSteps && <WhatsNewToast wn={whatsNew} />}
|
|
9982
|
+
{!usageNudge && !tourSteps && <ExportToast center={exportCenter} />}
|
|
10014
9983
|
{gitLifecycle && (
|
|
10015
9984
|
<div role="status" aria-live="polite" className="st-banner st-banner--info">
|
|
10016
9985
|
<span className="st-banner-dot" aria-hidden="true" />
|
|
@@ -10064,6 +10033,7 @@ function App() {
|
|
|
10064
10033
|
onOpenReadiness={() => setReadinessOpen(true)}
|
|
10065
10034
|
onOpenWhatsNew={whatsNew.openPanel}
|
|
10066
10035
|
whatsNewCount={whatsNew.unseen.length}
|
|
10036
|
+
exportCenter={exportCenter}
|
|
10067
10037
|
artboardCount={activeArtboards}
|
|
10068
10038
|
inspectorOpen={inspectorOpen}
|
|
10069
10039
|
inspectorTab={inspectorTab}
|
|
@@ -10730,6 +10700,7 @@ function App() {
|
|
|
10730
10700
|
}}
|
|
10731
10701
|
/>
|
|
10732
10702
|
<WhatsNewPanel wn={whatsNew} onStartTour={startTour} />
|
|
10703
|
+
<ExportPanel center={exportCenter} />
|
|
10733
10704
|
<ReadinessDialog open={readinessOpen} onClose={() => setReadinessOpen(false)} />
|
|
10734
10705
|
{usageNudge && !tourSteps && !collabNudge && (
|
|
10735
10706
|
<div className="mdcc-tour-nudge" role="status" aria-live="polite">
|