@1agh/maude 1.0.7 → 1.0.8
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/bin/_html-playwright.mjs +198 -19
- package/apps/studio/bin/_pdf-playwright.mjs +1 -0
- package/apps/studio/bin/_png-playwright.mjs +23 -6
- package/apps/studio/bin/_pptx-playwright.mjs +4 -1
- package/apps/studio/bin/_pw-launch.mjs +48 -0
- package/apps/studio/bin/_svg-playwright.mjs +45 -12
- package/apps/studio/bin/_video-playwright.mjs +123 -11
- package/apps/studio/canvas-edit.ts +33 -0
- package/apps/studio/canvas-lib.tsx +7 -0
- package/apps/studio/client/app.jsx +84 -23
- package/apps/studio/client/export-lane.js +37 -2
- package/apps/studio/dist/client.bundle.js +601 -601
- package/apps/studio/export-dialog.tsx +24 -19
- package/apps/studio/exporters/_runtime.ts +31 -1
- package/apps/studio/exporters/capture-chrome.ts +111 -0
- package/apps/studio/exporters/capture-core.ts +659 -3
- package/apps/studio/exporters/format-scopes.ts +88 -0
- package/apps/studio/exporters/html.ts +3 -2
- package/apps/studio/exporters/jobs.ts +180 -18
- package/apps/studio/exporters/pdf.ts +21 -1
- package/apps/studio/exporters/remote.ts +73 -4
- package/apps/studio/exporters/scope.ts +14 -3
- package/apps/studio/exporters/video.ts +75 -8
- package/apps/studio/http.ts +74 -7
- package/apps/studio/test/canvas-hide-chrome.test.ts +30 -0
- package/apps/studio/test/export-capture-hygiene.test.ts +675 -0
- package/apps/studio/test/export-e2e-lanes.test.ts +857 -0
- package/apps/studio/test/export-format-scope-coherence.test.ts +89 -0
- package/apps/studio/test/exporters/jobs.test.ts +73 -0
- package/apps/studio/test/exporters/scope.test.ts +33 -0
- package/apps/studio/test/fixtures/tiny-subset.woff2 +0 -0
- package/apps/studio/test/pdf-print-boxes.test.ts +33 -0
- package/apps/studio/whats-new.json +9 -0
- package/package.json +8 -8
|
@@ -26,6 +26,11 @@ import {
|
|
|
26
26
|
useState,
|
|
27
27
|
} from 'react';
|
|
28
28
|
|
|
29
|
+
import {
|
|
30
|
+
defaultScopeForFormat,
|
|
31
|
+
isScopeValidForFormat,
|
|
32
|
+
validScopesForFormat,
|
|
33
|
+
} from './exporters/format-scopes.ts';
|
|
29
34
|
import { useSelectionSetOptional } from './use-selection-set.tsx';
|
|
30
35
|
|
|
31
36
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -228,16 +233,6 @@ const SCOPE_META: Record<Scope, { label: string; description: string }> = {
|
|
|
228
233
|
},
|
|
229
234
|
};
|
|
230
235
|
|
|
231
|
-
const VALID_SCOPES_PER_FORMAT: Record<Format, Scope[]> = {
|
|
232
|
-
png: ['selection', 'artboard', 'canvas-as-separate'],
|
|
233
|
-
pdf: ['selection', 'artboard', 'canvas-as-separate'],
|
|
234
|
-
svg: ['selection', 'artboard', 'canvas-as-separate'],
|
|
235
|
-
html: ['artboard', 'canvas-as-separate'],
|
|
236
|
-
pptx: ['canvas-as-separate'],
|
|
237
|
-
canva: ['canvas-as-separate'],
|
|
238
|
-
zip: ['project-raw'],
|
|
239
|
-
};
|
|
240
|
-
|
|
241
236
|
export interface ExportHistoryEntry {
|
|
242
237
|
format: Format;
|
|
243
238
|
scope: Scope;
|
|
@@ -435,7 +430,13 @@ export function ExportDialogProvider({ children }: { children: ReactNode }): Rea
|
|
|
435
430
|
await loadHistory();
|
|
436
431
|
const last = history[0];
|
|
437
432
|
if (!last) return;
|
|
438
|
-
|
|
433
|
+
// ⌘⇧E replays a history entry verbatim. An entry whose (format, scope)
|
|
434
|
+
// pair is no longer legal — or never was — would otherwise be re-sent as
|
|
435
|
+
// an unrenderable job; fall back to that format's default scope.
|
|
436
|
+
const scope = isScopeValidForFormat(last.format, last.scope)
|
|
437
|
+
? last.scope
|
|
438
|
+
: defaultScopeForFormat(last.format);
|
|
439
|
+
await submit(last.format, scope, last.options ?? {});
|
|
439
440
|
}, [history, loadHistory, submit]);
|
|
440
441
|
|
|
441
442
|
const ctxValue = useMemo<ExportDialogValue>(
|
|
@@ -528,16 +529,20 @@ const DialogShell = (() => {
|
|
|
528
529
|
|
|
529
530
|
useEffect(() => {
|
|
530
531
|
if (!openState) return;
|
|
532
|
+
const next = openState.format ?? format;
|
|
531
533
|
if (openState.format) setFormat(openState.format);
|
|
532
|
-
|
|
533
|
-
|
|
534
|
+
// DDR-231 Phase 2 T4 — a hint the incoming FORMAT can't render (a
|
|
535
|
+
// context menu's `project-raw`, a re-opened zip entry) must not be
|
|
536
|
+
// adopted: it resolves to a file-tree target the render service refuses.
|
|
537
|
+
if (openState.scope && isScopeValidForFormat(next, openState.scope)) {
|
|
538
|
+
setScope(openState.scope);
|
|
539
|
+
}
|
|
540
|
+
}, [openState, format]);
|
|
534
541
|
|
|
535
|
-
// Keep the scope valid against the chosen format
|
|
542
|
+
// Keep the scope valid against the chosen format — one shared table,
|
|
543
|
+
// exporters/format-scopes.ts (DDR-231 Phase 2 T4).
|
|
536
544
|
useEffect(() => {
|
|
537
|
-
|
|
538
|
-
if (!valid.includes(scope)) {
|
|
539
|
-
setScope(valid[0] ?? 'artboard');
|
|
540
|
-
}
|
|
545
|
+
if (!isScopeValidForFormat(format, scope)) setScope(defaultScopeForFormat(format));
|
|
541
546
|
}, [format, scope]);
|
|
542
547
|
|
|
543
548
|
if (!openState) {
|
|
@@ -575,7 +580,7 @@ const DialogShell = (() => {
|
|
|
575
580
|
value={scope}
|
|
576
581
|
onChange={(e) => setScope(e.target.value as Scope)}
|
|
577
582
|
>
|
|
578
|
-
{
|
|
583
|
+
{validScopesForFormat(format).map((s) => (
|
|
579
584
|
<option key={s} value={s}>
|
|
580
585
|
{SCOPE_META[s].label}
|
|
581
586
|
</option>
|
|
@@ -126,7 +126,37 @@ export async function spawnShim(args: string[], opts: SpawnShimOptions): Promise
|
|
|
126
126
|
};
|
|
127
127
|
opts.signal?.addEventListener('abort', onAbort);
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
// Stream the shim's stderr LIVE to this process's stderr (prefixed) as well
|
|
130
|
+
// as buffering it for the exit-code error path. Buffering-only (the old
|
|
131
|
+
// `new Response(proc.stderr).text()`) hid every diagnostic until the child
|
|
132
|
+
// EXITED — so a shim that hung (a headless video encode that never resolves)
|
|
133
|
+
// produced total silence, and even on success the per-stage logs never
|
|
134
|
+
// surfaced. Live teeing makes the render worker's log show what a shim is
|
|
135
|
+
// doing while it does it, which is the only window CI/production has into it.
|
|
136
|
+
const shimTag = path.basename(args[0]).replace(/\.mjs$/, '');
|
|
137
|
+
const stderrPromise = (async () => {
|
|
138
|
+
let acc = '';
|
|
139
|
+
try {
|
|
140
|
+
const reader = proc.stderr.pipeThrough(new TextDecoderStream()).getReader();
|
|
141
|
+
let buf = '';
|
|
142
|
+
for (;;) {
|
|
143
|
+
const { done, value } = await reader.read();
|
|
144
|
+
if (done) break;
|
|
145
|
+
acc += value;
|
|
146
|
+
buf += value;
|
|
147
|
+
let nl = buf.indexOf('\n');
|
|
148
|
+
while (nl !== -1) {
|
|
149
|
+
process.stderr.write(`[${shimTag}] ${buf.slice(0, nl)}\n`);
|
|
150
|
+
buf = buf.slice(nl + 1);
|
|
151
|
+
nl = buf.indexOf('\n');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (buf) process.stderr.write(`[${shimTag}] ${buf}\n`);
|
|
155
|
+
} catch {
|
|
156
|
+
/* stream closed mid-read — the buffered `acc` still feeds the error path */
|
|
157
|
+
}
|
|
158
|
+
return acc;
|
|
159
|
+
})();
|
|
130
160
|
const stdoutLines: string[] = [];
|
|
131
161
|
let buffer = '';
|
|
132
162
|
const consumeLine = (line: string) => {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// capture-chrome.ts — the ONE list of editor-chrome selectors an export must
|
|
2
|
+
// never carry (DDR-231 Phase 2 T2).
|
|
3
|
+
//
|
|
4
|
+
// `_shell.html` has owned this list since the print-marks RCA, as the
|
|
5
|
+
// `<style id="canvas-hide-chrome" media="not all">` block that `?hide-chrome=1`
|
|
6
|
+
// flips to `media="all"`. That works for every host that NAVIGATES to the shell
|
|
7
|
+
// — the desktop / render-worker playwright spine does exactly that.
|
|
8
|
+
//
|
|
9
|
+
// The browser lane does not navigate: it captures the LIVE canvas the member is
|
|
10
|
+
// looking at, where the block is still `media="not all"` and every overlay is
|
|
11
|
+
// visible by design. The first cloud export shipped the artboard's own
|
|
12
|
+
// `.dc-artboard-label` header straight into the SVG because of it.
|
|
13
|
+
//
|
|
14
|
+
// So the list needs a second consumer, and a list with two consumers drifts —
|
|
15
|
+
// this file is the shared source, and `test/canvas-hide-chrome.test.ts` is the
|
|
16
|
+
// tripwire that fails the moment it and the `_shell.html` block disagree in
|
|
17
|
+
// either direction. Add a new overlay in ONE place and the test tells you the
|
|
18
|
+
// other.
|
|
19
|
+
//
|
|
20
|
+
// Pure data + a DOM helper — no Node imports (this file bundles for the
|
|
21
|
+
// browser through capture-core).
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Every editor-affordance selector suppressed during a capture. Kept in the
|
|
25
|
+
* same order and grouping as the `_shell.html` block so a diff of the two
|
|
26
|
+
* reads side by side.
|
|
27
|
+
*/
|
|
28
|
+
export const CAPTURE_HIDDEN_SELECTORS: readonly string[] = [
|
|
29
|
+
// Minimap + zoom + tool palette (and its portaled popovers)
|
|
30
|
+
'.dc-mm',
|
|
31
|
+
'.dc-zoom-tb',
|
|
32
|
+
'.dc-tool-palette',
|
|
33
|
+
'.dc-tp-popover',
|
|
34
|
+
'.dc-tp-shape-popover',
|
|
35
|
+
'.dc-context-menu',
|
|
36
|
+
// Presence — avatars (top-right) + live cursors
|
|
37
|
+
'.dc-participants',
|
|
38
|
+
'.dc-cursor-overlay',
|
|
39
|
+
// Selection + manipulation chrome
|
|
40
|
+
'.dc-cv-halo',
|
|
41
|
+
'.dc-cv-group-bbox',
|
|
42
|
+
'.dc-cv-eq-spacing-layer',
|
|
43
|
+
'.dc-multi-artboard-tb',
|
|
44
|
+
'.dc-elem-ctx-tb',
|
|
45
|
+
'.dc-snap-guide',
|
|
46
|
+
'.dc-snap-pill',
|
|
47
|
+
// Annotations (FigJam-style draw layer)
|
|
48
|
+
'.dc-annot-svg',
|
|
49
|
+
'.dc-annot-ctx',
|
|
50
|
+
'.dc-annot-resize-handle',
|
|
51
|
+
// Comments — pins, threads, composer, @mention popup
|
|
52
|
+
'.cm-layer',
|
|
53
|
+
'.cm-pin',
|
|
54
|
+
'.cm-thread',
|
|
55
|
+
'.cm-composer',
|
|
56
|
+
'.cm-mention-popup',
|
|
57
|
+
// Inspector pin overlay injected into every served shell (inspect.ts)
|
|
58
|
+
'.dgn-pin',
|
|
59
|
+
'#dgn-pin-layer',
|
|
60
|
+
// HUDs + banners
|
|
61
|
+
'.dc-ai-banner',
|
|
62
|
+
'.dc-undo-hud',
|
|
63
|
+
// Activity overlay — live "agent works here" chrome (Phase 13 / DDR-029)
|
|
64
|
+
'.dc-activity-rim',
|
|
65
|
+
// HMR "holding last good" toast (Phase 13.1 / DDR-077)
|
|
66
|
+
'.dc-hmr-holding',
|
|
67
|
+
// Artboard title/label button (editor affordance, not design content)
|
|
68
|
+
'.dc-artboard-label',
|
|
69
|
+
// Print/bleed/trim/margin guide overlay (feature-2-print-artboards)
|
|
70
|
+
'.dc-artboard-guides',
|
|
71
|
+
// Generic opt-in hooks for any future floating overlay
|
|
72
|
+
'[data-dc-overlay]',
|
|
73
|
+
'[data-mdcc-annotations]',
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
/** The CSS rule body both hosts apply. */
|
|
77
|
+
export function captureChromeCss(): string {
|
|
78
|
+
return `${CAPTURE_HIDDEN_SELECTORS.join(',')}{display:none !important}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const STYLE_ID = 'maude-capture-chrome';
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Hide editor chrome for the duration of a capture and return the undo.
|
|
85
|
+
*
|
|
86
|
+
* Idempotent and ref-counted: a multi-artboard capture calls this once per
|
|
87
|
+
* target through {@link import('./capture-core.ts').svgForElement}, and nesting
|
|
88
|
+
* (the shim host navigates with `?hide-chrome=1` AND runs this) must not leave
|
|
89
|
+
* a half-removed style behind. The live-canvas host MUST call the returned undo
|
|
90
|
+
* — the member is looking at this DOM.
|
|
91
|
+
*/
|
|
92
|
+
export function applyCaptureChrome(doc: Document): () => void {
|
|
93
|
+
const existing = doc.getElementById(STYLE_ID) as (HTMLStyleElement & { _refs?: number }) | null;
|
|
94
|
+
if (existing) {
|
|
95
|
+
existing._refs = (existing._refs ?? 1) + 1;
|
|
96
|
+
return () => releaseCaptureChrome(doc);
|
|
97
|
+
}
|
|
98
|
+
const style = doc.createElement('style') as HTMLStyleElement & { _refs?: number };
|
|
99
|
+
style.id = STYLE_ID;
|
|
100
|
+
style._refs = 1;
|
|
101
|
+
style.textContent = captureChromeCss();
|
|
102
|
+
(doc.head ?? doc.documentElement).appendChild(style);
|
|
103
|
+
return () => releaseCaptureChrome(doc);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function releaseCaptureChrome(doc: Document): void {
|
|
107
|
+
const style = doc.getElementById(STYLE_ID) as (HTMLStyleElement & { _refs?: number }) | null;
|
|
108
|
+
if (!style) return;
|
|
109
|
+
style._refs = (style._refs ?? 1) - 1;
|
|
110
|
+
if (style._refs <= 0) style.remove();
|
|
111
|
+
}
|