@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.
Files changed (34) hide show
  1. package/apps/studio/bin/_html-playwright.mjs +198 -19
  2. package/apps/studio/bin/_pdf-playwright.mjs +1 -0
  3. package/apps/studio/bin/_png-playwright.mjs +23 -6
  4. package/apps/studio/bin/_pptx-playwright.mjs +4 -1
  5. package/apps/studio/bin/_pw-launch.mjs +48 -0
  6. package/apps/studio/bin/_svg-playwright.mjs +45 -12
  7. package/apps/studio/bin/_video-playwright.mjs +123 -11
  8. package/apps/studio/canvas-edit.ts +33 -0
  9. package/apps/studio/canvas-lib.tsx +7 -0
  10. package/apps/studio/client/app.jsx +84 -23
  11. package/apps/studio/client/export-lane.js +37 -2
  12. package/apps/studio/dist/client.bundle.js +601 -601
  13. package/apps/studio/export-dialog.tsx +24 -19
  14. package/apps/studio/exporters/_runtime.ts +31 -1
  15. package/apps/studio/exporters/capture-chrome.ts +111 -0
  16. package/apps/studio/exporters/capture-core.ts +659 -3
  17. package/apps/studio/exporters/format-scopes.ts +88 -0
  18. package/apps/studio/exporters/html.ts +3 -2
  19. package/apps/studio/exporters/jobs.ts +180 -18
  20. package/apps/studio/exporters/pdf.ts +21 -1
  21. package/apps/studio/exporters/remote.ts +73 -4
  22. package/apps/studio/exporters/scope.ts +14 -3
  23. package/apps/studio/exporters/video.ts +75 -8
  24. package/apps/studio/http.ts +74 -7
  25. package/apps/studio/test/canvas-hide-chrome.test.ts +30 -0
  26. package/apps/studio/test/export-capture-hygiene.test.ts +675 -0
  27. package/apps/studio/test/export-e2e-lanes.test.ts +857 -0
  28. package/apps/studio/test/export-format-scope-coherence.test.ts +89 -0
  29. package/apps/studio/test/exporters/jobs.test.ts +73 -0
  30. package/apps/studio/test/exporters/scope.test.ts +33 -0
  31. package/apps/studio/test/fixtures/tiny-subset.woff2 +0 -0
  32. package/apps/studio/test/pdf-print-boxes.test.ts +33 -0
  33. package/apps/studio/whats-new.json +9 -0
  34. 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
- await submit(last.format, last.scope, last.options ?? {});
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
- if (openState.scope) setScope(openState.scope);
533
- }, [openState]);
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
- const valid = VALID_SCOPES_PER_FORMAT[format];
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
- {VALID_SCOPES_PER_FORMAT[format].map((s) => (
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
- const stderrPromise = new Response(proc.stderr).text();
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
+ }