@1agh/maude 0.39.1 → 0.41.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/README.md +1 -1
- package/apps/studio/ai-banner.tsx +2 -2
- package/apps/studio/annotations-context-toolbar.tsx +44 -1
- package/apps/studio/annotations-layer.tsx +306 -5
- package/apps/studio/annotations-model.ts +107 -6
- package/apps/studio/api.ts +1316 -67
- package/apps/studio/artboard-marquee.tsx +1 -1
- package/apps/studio/bin/_canvas-rects-playwright.mjs +55 -0
- package/apps/studio/bin/_canvas-rects-static.mjs +166 -0
- package/apps/studio/bin/_fetch-asset.mjs +556 -0
- package/apps/studio/bin/_html-playwright.mjs +9 -1
- package/apps/studio/bin/_pdf-playwright.mjs +8 -1
- package/apps/studio/bin/_png-playwright.mjs +8 -1
- package/apps/studio/bin/_pw-launch.mjs +54 -0
- package/apps/studio/bin/_svg-playwright.mjs +8 -1
- package/apps/studio/bin/_video-playwright.mjs +452 -0
- package/apps/studio/bin/annotate.mjs +576 -34
- package/apps/studio/bin/canvas-rects.sh +152 -0
- package/apps/studio/bin/fetch-asset.sh +34 -0
- package/apps/studio/bin/prep.sh +8 -1
- package/apps/studio/bin/read-annotations.mjs +138 -7
- package/apps/studio/bin/smoke.sh +42 -6
- package/apps/studio/build.ts +21 -0
- package/apps/studio/canvas-comment-mount.tsx +138 -4
- package/apps/studio/canvas-edit.ts +2625 -111
- package/apps/studio/canvas-lib.tsx +238 -2
- package/apps/studio/canvas-list-watch.ts +6 -2
- package/apps/studio/canvas-shell.tsx +514 -20
- package/apps/studio/client/app.jsx +2480 -52
- package/apps/studio/client/comments-overlay.css +130 -126
- package/apps/studio/client/github.js +15 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +860 -0
- package/apps/studio/client/panels/timeline-parse.js +229 -0
- package/apps/studio/client/panels/timeline-snap.js +55 -0
- package/apps/studio/client/styles/3-shell-maude.css +155 -0
- package/apps/studio/comments-overlay.tsx +148 -41
- package/apps/studio/config.schema.json +14 -0
- package/apps/studio/context-menu.tsx +16 -6
- package/apps/studio/context.ts +113 -1
- package/apps/studio/contextual-toolbar.tsx +262 -4
- package/apps/studio/cursors-overlay.tsx +4 -4
- package/apps/studio/dist/client.bundle.js +89 -17
- package/apps/studio/dist/comment-mount.js +59 -1
- package/apps/studio/dist/runtime/.min-sizes.json +11 -1
- package/apps/studio/dist/runtime/@remotion_media.js +491 -0
- package/apps/studio/dist/runtime/@remotion_player.js +56 -0
- package/apps/studio/dist/runtime/@remotion_transitions.js +300 -0
- package/apps/studio/dist/runtime/@remotion_transitions_clock-wipe.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_fade.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_flip.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_none.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_slide.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_wipe.js +1 -0
- package/apps/studio/dist/runtime/REMOTION-LICENSE.md +28 -0
- package/apps/studio/dist/runtime/remotion.js +42 -0
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +127 -1
- package/apps/studio/drag-state.ts +24 -0
- package/apps/studio/equal-spacing-detector.ts +205 -0
- package/apps/studio/export-dialog.tsx +1 -1
- package/apps/studio/exporters/_browser-bundles.ts +117 -0
- package/apps/studio/exporters/_runtime.ts +69 -0
- package/apps/studio/exporters/html.ts +4 -3
- package/apps/studio/exporters/index.ts +28 -2
- package/apps/studio/exporters/pdf.ts +4 -3
- package/apps/studio/exporters/png.ts +5 -3
- package/apps/studio/exporters/pptx.ts +6 -4
- package/apps/studio/exporters/svg.ts +9 -5
- package/apps/studio/exporters/video-encode-lib.ts +200 -0
- package/apps/studio/exporters/video-render-lib.ts +108 -0
- package/apps/studio/exporters/video.ts +184 -0
- package/apps/studio/history.ts +47 -1
- package/apps/studio/http.ts +758 -27
- package/apps/studio/input-router.tsx +19 -1
- package/apps/studio/marquee-overlay.tsx +1 -1
- package/apps/studio/measure-overlay.tsx +241 -0
- package/apps/studio/participants-chrome.tsx +3 -3
- package/apps/studio/runtime-bundle.ts +30 -0
- package/apps/studio/server.ts +34 -9
- package/apps/studio/sizing-mode.ts +117 -0
- package/apps/studio/spacing-handles.ts +166 -0
- package/apps/studio/test/annotate-write.test.ts +890 -0
- package/apps/studio/test/annotations-roundtrip.test.ts +47 -0
- package/apps/studio/test/camera-reveal.test.tsx +173 -0
- package/apps/studio/test/canvas-create-api.test.ts +76 -0
- package/apps/studio/test/canvas-edit.test.ts +140 -0
- package/apps/studio/test/canvas-list-watch.test.ts +49 -0
- package/apps/studio/test/canvas-media-drop.test.ts +30 -1
- package/apps/studio/test/canvas-origin-gate.test.ts +54 -0
- package/apps/studio/test/canvas-rects.test.ts +198 -0
- package/apps/studio/test/clip-addressing.test.ts +732 -0
- package/apps/studio/test/comments-overlay.test.ts +117 -0
- package/apps/studio/test/config-reload.test.ts +230 -0
- package/apps/studio/test/dns-rebinding-guard.test.ts +74 -0
- package/apps/studio/test/dom-selection.test.ts +130 -0
- package/apps/studio/test/edit-css-occurrence.test.ts +81 -0
- package/apps/studio/test/edit-persistence.test.ts +91 -0
- package/apps/studio/test/edit-scope-api.test.ts +115 -0
- package/apps/studio/test/element-resize.test.ts +136 -0
- package/apps/studio/test/element-structural-api.test.ts +360 -0
- package/apps/studio/test/element-structural-edit.test.ts +233 -0
- package/apps/studio/test/equal-spacing-detector.test.ts +165 -1
- package/apps/studio/test/exporters/runtime.test.ts +59 -0
- package/apps/studio/test/file-lock.test.ts +84 -0
- package/apps/studio/test/fixtures/video-comp-fixture.tsx +82 -0
- package/apps/studio/test/history-rollback.test.ts +26 -0
- package/apps/studio/test/knob-props-authored.test.ts +87 -0
- package/apps/studio/test/read-annotations.test.ts +154 -0
- package/apps/studio/test/sizing-mode.test.ts +102 -0
- package/apps/studio/test/spacing-handles.test.ts +138 -0
- package/apps/studio/test/specimen-select.test.ts +88 -0
- package/apps/studio/test/timeline-parse.test.ts +127 -0
- package/apps/studio/test/timeline-snap.test.ts +85 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +84 -0
- package/apps/studio/test/undo-sequence-byte-compare.test.ts +211 -0
- package/apps/studio/test/video-asset.test.ts +163 -0
- package/apps/studio/test/video-comp-fixture.test.ts +50 -0
- package/apps/studio/test/video-comp.test.ts +168 -0
- package/apps/studio/test/video-render-bridge.test.ts +149 -0
- package/apps/studio/tool-palette.tsx +122 -2
- package/apps/studio/undo-hud.tsx +2 -2
- package/apps/studio/use-annotation-resize.tsx +6 -3
- package/apps/studio/use-canvas-media-drop.tsx +66 -4
- package/apps/studio/use-element-resize.tsx +732 -0
- package/apps/studio/use-keyboard-discipline.tsx +205 -15
- package/apps/studio/use-selection-set.tsx +14 -0
- package/apps/studio/use-spacing-handles.tsx +388 -0
- package/apps/studio/video-comp.tsx +444 -0
- package/apps/studio/whats-new.json +55 -0
- package/apps/studio/ws.ts +5 -0
- package/cli/commands/design.mjs +6 -1
- package/cli/commands/design.test.mjs +49 -1
- package/cli/lib/fetch-asset.test.mjs +213 -0
- package/package.json +8 -8
- package/plugins/design/dependencies.json +10 -2
- package/plugins/design/templates/_shell.html +25 -2
- package/plugins/design/templates/design-system-inspiration/_MAPPING.md +1 -1
|
@@ -146,6 +146,19 @@ export function selectorIndex(doc: Document, selector: string, el: Element | nul
|
|
|
146
146
|
return 0;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* GLOBAL occurrence index of `el` among EVERY node in the document that shares
|
|
151
|
+
* its `data-cd-id` — the DOM instance index the server-side reused-component
|
|
152
|
+
* usage resolver (`resolveUsageId`) expects, in source order. Distinct from a
|
|
153
|
+
* `Selection.index` (which counts within an artboard-SCOPED selector). Used to
|
|
154
|
+
* route a whole-instance move/resize per-occurrence (Stage H3) so it stays local
|
|
155
|
+
* to the dragged instance. Matches the reorder drag's own snapshot occurrence.
|
|
156
|
+
*/
|
|
157
|
+
export function globalCdOccurrence(doc: Document, cdId: string, el: Element | null): number {
|
|
158
|
+
if (!cdId || !el) return 0;
|
|
159
|
+
return selectorIndex(doc, `[data-cd-id="${cssEscape(cdId)}"]`, el);
|
|
160
|
+
}
|
|
161
|
+
|
|
149
162
|
/**
|
|
150
163
|
* Resolve a stored Selection to its live element, artboard-scoped. Prefers the
|
|
151
164
|
* id+artboardId scoped selector (the robust path), then the stored `selector`
|
|
@@ -175,6 +188,63 @@ export function resolveSelectionEl(
|
|
|
175
188
|
return null;
|
|
176
189
|
}
|
|
177
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Length of the trailing run `a` and `b` share, walked from the end. `domPath()`
|
|
193
|
+
* hops carry no positional (nth-child) info, so a shared suffix survives a
|
|
194
|
+
* sibling insertion/removal anywhere earlier in the tree — exactly the DDR-019
|
|
195
|
+
* `data-cd-id` renumbering case this fallback exists for.
|
|
196
|
+
*/
|
|
197
|
+
function matchingSuffixLength(a: string[], b: string[]): number {
|
|
198
|
+
let n = 0;
|
|
199
|
+
const max = Math.min(a.length, b.length);
|
|
200
|
+
for (let i = 1; i <= max; i++) {
|
|
201
|
+
if (a[a.length - i] !== b[b.length - i]) break;
|
|
202
|
+
n++;
|
|
203
|
+
}
|
|
204
|
+
return n;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Best-effort structural fallback for when a stored `data-cd-id` selector no
|
|
209
|
+
* longer identifies the intended element. A canvas rewrite (`/design:edit`
|
|
210
|
+
* regenerating JSX) renumbers `data-cd-id` — it's an AST-position fingerprint,
|
|
211
|
+
* not a stable identity (DDR-019) — so the id can end up on an unrelated
|
|
212
|
+
* element, or on none at all. This scores every stamped element in the
|
|
213
|
+
* artboard by how much of its live `domPath()` matches the STORED path
|
|
214
|
+
* (as a trailing run) plus authored-class overlap, and requires the leaf tag
|
|
215
|
+
* to match. Returns null when nothing scores above zero (no plausible match —
|
|
216
|
+
* the caller should treat the target as gone).
|
|
217
|
+
*/
|
|
218
|
+
export function resolveByDomPath(
|
|
219
|
+
doc: Document,
|
|
220
|
+
opts: { artboardId?: string | null; tag?: string; classes?: string; dom_path?: string[] }
|
|
221
|
+
): Element | null {
|
|
222
|
+
const storedPath = opts.dom_path;
|
|
223
|
+
const wantTag = (opts.tag || '').toLowerCase();
|
|
224
|
+
if (!storedPath || storedPath.length === 0 || !wantTag) return null;
|
|
225
|
+
let scope: ParentNode = doc;
|
|
226
|
+
if (opts.artboardId) {
|
|
227
|
+
const artboard = doc.querySelector(`[data-dc-screen="${opts.artboardId}"]`);
|
|
228
|
+
if (artboard) scope = artboard;
|
|
229
|
+
}
|
|
230
|
+
const wantClasses = (opts.classes || '').split(/\s+/).filter(Boolean);
|
|
231
|
+
let best: Element | null = null;
|
|
232
|
+
let bestScore = 0;
|
|
233
|
+
for (const el of Array.from(scope.querySelectorAll('[data-cd-id]'))) {
|
|
234
|
+
if (el.tagName.toLowerCase() !== wantTag) continue;
|
|
235
|
+
const suffix = matchingSuffixLength(domPath(el), storedPath);
|
|
236
|
+
if (suffix === 0) continue;
|
|
237
|
+
const liveClasses = realClasses(el).split(/\s+/).filter(Boolean);
|
|
238
|
+
const overlap = wantClasses.filter((c) => liveClasses.includes(c)).length;
|
|
239
|
+
const score = suffix * 10 + overlap;
|
|
240
|
+
if (score > bestScore) {
|
|
241
|
+
bestScore = score;
|
|
242
|
+
best = el;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return best;
|
|
246
|
+
}
|
|
247
|
+
|
|
178
248
|
/**
|
|
179
249
|
* Phase 12.2 — style maps for the CSS-knob properties. `authored` is what the
|
|
180
250
|
* element sets INLINE (React renders `style={{padding:8}}` → `style="padding:8px"`),
|
|
@@ -193,9 +263,18 @@ const KNOB_PROPS = [
|
|
|
193
263
|
// Layout
|
|
194
264
|
'display',
|
|
195
265
|
'flex-direction',
|
|
266
|
+
'flex-wrap',
|
|
196
267
|
'align-items',
|
|
197
268
|
'justify-content',
|
|
198
269
|
'gap',
|
|
270
|
+
// Stage M — flex-CHILD props (sizing mode Fill + the Auto-layout child rows).
|
|
271
|
+
// Shown only when the PARENT is flex (Selection.parentDisplay); captured here so
|
|
272
|
+
// they round-trip. `flex` shorthand is listed for customStyles-exclusion only.
|
|
273
|
+
'flex',
|
|
274
|
+
'flex-grow',
|
|
275
|
+
'flex-shrink',
|
|
276
|
+
'flex-basis',
|
|
277
|
+
'align-self',
|
|
199
278
|
// Typography
|
|
200
279
|
'font-family',
|
|
201
280
|
'color',
|
|
@@ -219,7 +298,10 @@ const KNOB_PROPS = [
|
|
|
219
298
|
// Size
|
|
220
299
|
'width',
|
|
221
300
|
'height',
|
|
301
|
+
'min-width',
|
|
302
|
+
'min-height',
|
|
222
303
|
'max-width',
|
|
304
|
+
'max-height',
|
|
223
305
|
// Appearance
|
|
224
306
|
'background-color',
|
|
225
307
|
'border-radius',
|
|
@@ -232,6 +314,32 @@ const KNOB_PROPS = [
|
|
|
232
314
|
'border-color',
|
|
233
315
|
'box-shadow',
|
|
234
316
|
'opacity',
|
|
317
|
+
// feature-element-editing-robustness Stage B — promotes DDR-104 §3's OUT-list
|
|
318
|
+
// into curated rows (superseded by the Stage-G DDR). Adding them here captures
|
|
319
|
+
// their authored/computed values for the new panel controls AND moves them out
|
|
320
|
+
// of the Advanced "customStyles" hatch (a canvas that carried one as a raw
|
|
321
|
+
// custom prop now surfaces it in its curated row instead).
|
|
322
|
+
// Position + stacking
|
|
323
|
+
'position',
|
|
324
|
+
'top',
|
|
325
|
+
'right',
|
|
326
|
+
'bottom',
|
|
327
|
+
'left',
|
|
328
|
+
'z-index',
|
|
329
|
+
// Transform
|
|
330
|
+
'transform',
|
|
331
|
+
'transform-origin',
|
|
332
|
+
// Typography (extra)
|
|
333
|
+
'font-style',
|
|
334
|
+
'text-transform',
|
|
335
|
+
'text-decoration',
|
|
336
|
+
'white-space',
|
|
337
|
+
// Overflow
|
|
338
|
+
'overflow',
|
|
339
|
+
// Media framing
|
|
340
|
+
'object-fit',
|
|
341
|
+
'aspect-ratio',
|
|
342
|
+
'object-position',
|
|
235
343
|
] as const;
|
|
236
344
|
|
|
237
345
|
// Phase 12.3 — HTML attributes the custom-attribute hatch may have written, so a
|
|
@@ -245,6 +353,8 @@ function styleMapsFor(el: Element | null): {
|
|
|
245
353
|
computed: Record<string, string>;
|
|
246
354
|
customStyles: Record<string, string>;
|
|
247
355
|
attrs: Record<string, string>;
|
|
356
|
+
parentDisplay?: string;
|
|
357
|
+
parentFlexDirection?: string;
|
|
248
358
|
} {
|
|
249
359
|
if (!el || typeof window === 'undefined' || !window.getComputedStyle) {
|
|
250
360
|
return { authored: {}, computed: {}, customStyles: {}, attrs: {} };
|
|
@@ -283,7 +393,16 @@ function styleMapsFor(el: Element | null): {
|
|
|
283
393
|
if (ATTR_SKIP.test(a.name)) continue;
|
|
284
394
|
attrs[a.name] = a.value;
|
|
285
395
|
}
|
|
286
|
-
|
|
396
|
+
// Stage M — parent's layout context (for the Fixed/Hug/Fill sizing control +
|
|
397
|
+
// flex-child row gating). Read here because the shell can't reach the
|
|
398
|
+
// cross-origin iframe to compute it after selection.
|
|
399
|
+
const parent = (el as HTMLElement).parentElement;
|
|
400
|
+
const parentDisplay = parent ? window.getComputedStyle(parent).display : '';
|
|
401
|
+
const parentFlexDirection =
|
|
402
|
+
parent && (parentDisplay === 'flex' || parentDisplay === 'inline-flex')
|
|
403
|
+
? window.getComputedStyle(parent).flexDirection
|
|
404
|
+
: '';
|
|
405
|
+
return { authored, computed, customStyles, attrs, parentDisplay, parentFlexDirection };
|
|
287
406
|
} catch {
|
|
288
407
|
return { authored: {}, computed: {}, customStyles: {}, attrs: {} };
|
|
289
408
|
}
|
|
@@ -336,6 +455,13 @@ export function hoverTargetToSelection(target: HoverTarget, file?: string): Sele
|
|
|
336
455
|
h: Math.round(rect.height),
|
|
337
456
|
}
|
|
338
457
|
: null,
|
|
458
|
+
// WORLD-unit size — `offsetWidth`/`offsetHeight` are the element's own local
|
|
459
|
+
// pixel box, unaffected by an ancestor's `.dc-world` zoom transform (unlike
|
|
460
|
+
// `bounds`, which is the SCREEN rect and lies at any zoom other than 100%).
|
|
461
|
+
// The Inspector's artboard-resize fields (Stage D4 tail) need the true
|
|
462
|
+
// JSX-authored width/height to pre-fill correctly regardless of zoom.
|
|
463
|
+
worldW: el instanceof HTMLElement ? Math.round(el.offsetWidth) : undefined,
|
|
464
|
+
worldH: el instanceof HTMLElement ? Math.round(el.offsetHeight) : undefined,
|
|
339
465
|
html: el ? (el.outerHTML ?? '').slice(0, 4000) : '',
|
|
340
466
|
...styleMapsFor(el),
|
|
341
467
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file drag-state.ts
|
|
3
|
+
* @purpose Tiny shared module-scope flag: is an in-canvas ELEMENT drag
|
|
4
|
+
* (`ReorderDrag` — reorder or out-of-flow reposition) currently
|
|
5
|
+
* active? Consulted by per-frame rAF overlays (`ElementResizeOverlay`,
|
|
6
|
+
* `SpacingHandlesOverlay`) so they can skip their own
|
|
7
|
+
* getBoundingClientRect/getComputedStyle work while the drag's OWN
|
|
8
|
+
* live-preview chrome is what matters — avoids compounding rAF cost
|
|
9
|
+
* during the single most DOM-churn-heavy gesture the canvas has
|
|
10
|
+
* (dogfood 2026-07-07: "hrozně zavaší ten toolbar" during reorder).
|
|
11
|
+
* A standalone module (not exported from canvas-shell.tsx) so the
|
|
12
|
+
* overlay files can import it without a circular import back into
|
|
13
|
+
* canvas-shell.tsx (which imports THEM).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
let active = false;
|
|
17
|
+
|
|
18
|
+
export function setElementDragActive(v: boolean): void {
|
|
19
|
+
active = v;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isElementDragActive(): boolean {
|
|
23
|
+
return active;
|
|
24
|
+
}
|
|
@@ -96,3 +96,208 @@ export function detectEqualSpacing(
|
|
|
96
96
|
|
|
97
97
|
return { axis, gapPx: median, midpoints };
|
|
98
98
|
}
|
|
99
|
+
|
|
100
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
101
|
+
// Task L7 — Alt-hover distance measurement between exactly TWO rects (the
|
|
102
|
+
// selected element + whatever's hovered). Same pairwise-gap formula as
|
|
103
|
+
// `detectEqualSpacing`'s loop body, but without its 3-rect / equal-tolerance
|
|
104
|
+
// preconditions, and shaped for line-painting rather than midpoint dots.
|
|
105
|
+
|
|
106
|
+
export interface PairGap {
|
|
107
|
+
axis: SpacingAxis;
|
|
108
|
+
/** The gap between the two rects' facing edges, in the caller's coord space. */
|
|
109
|
+
gap: number;
|
|
110
|
+
/** Leading edge of the gap span — `left` for an x-axis line, `top` for y. */
|
|
111
|
+
from: number;
|
|
112
|
+
/** Perpendicular-axis center of the pair — `top` for an x-axis line
|
|
113
|
+
* (vertically centers it between the two rects), `left` for y. */
|
|
114
|
+
cross: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Distance between two rects along one axis. Returns `null` when they
|
|
119
|
+
* overlap on that axis (no meaningful gap — e.g. two rects stacked only on
|
|
120
|
+
* y have no x-axis gap to show).
|
|
121
|
+
*/
|
|
122
|
+
export function computePairGap(a: Rect, b: Rect, axis: SpacingAxis): PairGap | null {
|
|
123
|
+
const [first, second] =
|
|
124
|
+
axis === 'x' ? (a.x <= b.x ? [a, b] : [b, a]) : a.y <= b.y ? [a, b] : [b, a];
|
|
125
|
+
if (axis === 'x') {
|
|
126
|
+
const gap = second.x - (first.x + first.w);
|
|
127
|
+
if (gap <= 0) return null;
|
|
128
|
+
return {
|
|
129
|
+
axis,
|
|
130
|
+
gap,
|
|
131
|
+
from: first.x + first.w,
|
|
132
|
+
cross: (first.y + first.h / 2 + second.y + second.h / 2) / 2,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const gap = second.y - (first.y + first.h);
|
|
136
|
+
if (gap <= 0) return null;
|
|
137
|
+
return {
|
|
138
|
+
axis,
|
|
139
|
+
gap,
|
|
140
|
+
from: first.y + first.h,
|
|
141
|
+
cross: (first.x + first.w / 2 + second.x + second.w / 2) / 2,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
146
|
+
// Task L5 — distribute / align / tidy-up for a multi-selection of ELEMENTS.
|
|
147
|
+
//
|
|
148
|
+
// These mirror the artboard `distributeArtboards`/`alignArtboards` algorithms
|
|
149
|
+
// in canvas-shell.tsx verbatim, generalized from `ArtboardRect` to any rect
|
|
150
|
+
// set. Kept pure/DOM-free like `detectEqualSpacing` above — the caller
|
|
151
|
+
// resolves DOM rects, calls these, then posts one `reposition-request` per
|
|
152
|
+
// moved rect through the existing element-drag write lane (no new endpoint).
|
|
153
|
+
//
|
|
154
|
+
// `key` is an OPAQUE caller-supplied correlation token (not a `data-cd-id`) —
|
|
155
|
+
// a shared-component instance's cd-id can repeat across selections
|
|
156
|
+
// (disambiguated only by occurrence index), so matching results back to the
|
|
157
|
+
// caller's own bookkeeping by `id` would collide. Callers pass e.g. the
|
|
158
|
+
// selection's array index as `key` and zip the result back themselves.
|
|
159
|
+
|
|
160
|
+
export interface KeyedRect extends Rect {
|
|
161
|
+
key: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export type AlignAxisMode = 'left' | 'right' | 'center-x' | 'top' | 'bottom' | 'center-y';
|
|
165
|
+
|
|
166
|
+
export interface MovedRect {
|
|
167
|
+
key: string;
|
|
168
|
+
x: number;
|
|
169
|
+
y: number;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Align 2+ rects to a common edge/midline of their union bbox. Returns only
|
|
174
|
+
* the rects whose position actually changes (rounded-equal = no-op, skipped).
|
|
175
|
+
*/
|
|
176
|
+
export function computeAlign(rects: KeyedRect[], mode: AlignAxisMode): MovedRect[] {
|
|
177
|
+
if (rects.length < 2) return [];
|
|
178
|
+
let xMin = Number.POSITIVE_INFINITY;
|
|
179
|
+
let yMin = Number.POSITIVE_INFINITY;
|
|
180
|
+
let xMax = Number.NEGATIVE_INFINITY;
|
|
181
|
+
let yMax = Number.NEGATIVE_INFINITY;
|
|
182
|
+
for (const r of rects) {
|
|
183
|
+
if (r.x < xMin) xMin = r.x;
|
|
184
|
+
if (r.y < yMin) yMin = r.y;
|
|
185
|
+
if (r.x + r.w > xMax) xMax = r.x + r.w;
|
|
186
|
+
if (r.y + r.h > yMax) yMax = r.y + r.h;
|
|
187
|
+
}
|
|
188
|
+
const cx = (xMin + xMax) / 2;
|
|
189
|
+
const cy = (yMin + yMax) / 2;
|
|
190
|
+
const moved: MovedRect[] = [];
|
|
191
|
+
for (const r of rects) {
|
|
192
|
+
let nx = r.x;
|
|
193
|
+
let ny = r.y;
|
|
194
|
+
switch (mode) {
|
|
195
|
+
case 'left':
|
|
196
|
+
nx = xMin;
|
|
197
|
+
break;
|
|
198
|
+
case 'right':
|
|
199
|
+
nx = xMax - r.w;
|
|
200
|
+
break;
|
|
201
|
+
case 'center-x':
|
|
202
|
+
nx = cx - r.w / 2;
|
|
203
|
+
break;
|
|
204
|
+
case 'top':
|
|
205
|
+
ny = yMin;
|
|
206
|
+
break;
|
|
207
|
+
case 'bottom':
|
|
208
|
+
ny = yMax - r.h;
|
|
209
|
+
break;
|
|
210
|
+
case 'center-y':
|
|
211
|
+
ny = cy - r.h / 2;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
if (Math.round(nx) === Math.round(r.x) && Math.round(ny) === Math.round(r.y)) continue;
|
|
215
|
+
moved.push({ key: r.key, x: Math.round(nx), y: Math.round(ny) });
|
|
216
|
+
}
|
|
217
|
+
return moved;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Distribute 3+ rects with equal gaps on one axis. Sorts by leading edge,
|
|
222
|
+
* holds the first + last fixed, redistributes the middle ones so the trailing
|
|
223
|
+
* → next-leading gap is equal everywhere.
|
|
224
|
+
*/
|
|
225
|
+
export function computeDistribute(rects: KeyedRect[], axis: SpacingAxis): MovedRect[] {
|
|
226
|
+
if (rects.length < 3) return [];
|
|
227
|
+
const sorted = [...rects].sort((a, b) => (axis === 'x' ? a.x - b.x : a.y - b.y));
|
|
228
|
+
const first = sorted[0];
|
|
229
|
+
const last = sorted[sorted.length - 1];
|
|
230
|
+
if (!first || !last) return [];
|
|
231
|
+
const sideLen = (r: KeyedRect) => (axis === 'x' ? r.w : r.h);
|
|
232
|
+
const totalSides = sorted.reduce((acc, r) => acc + sideLen(r), 0);
|
|
233
|
+
const span = (axis === 'x' ? last.x + last.w - first.x : last.y + last.h - first.y) - totalSides;
|
|
234
|
+
const gap = span / (sorted.length - 1);
|
|
235
|
+
const moved: MovedRect[] = [];
|
|
236
|
+
let cursor = axis === 'x' ? first.x + first.w + gap : first.y + first.h + gap;
|
|
237
|
+
for (let i = 1; i < sorted.length - 1; i++) {
|
|
238
|
+
const r = sorted[i];
|
|
239
|
+
if (!r) continue;
|
|
240
|
+
if (axis === 'x') {
|
|
241
|
+
moved.push({ key: r.key, x: Math.round(cursor), y: Math.round(r.y) });
|
|
242
|
+
cursor += r.w + gap;
|
|
243
|
+
} else {
|
|
244
|
+
moved.push({ key: r.key, x: Math.round(r.x), y: Math.round(cursor) });
|
|
245
|
+
cursor += r.h + gap;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return moved;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface TidyOptions {
|
|
252
|
+
/** Pixel gap between grid cells, both axes. Default 16. */
|
|
253
|
+
gap?: number;
|
|
254
|
+
/** Column count. Default: near-square (`round(sqrt(n))`). */
|
|
255
|
+
columns?: number;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Snap 2+ rects into a clean grid: reading-order sort (top-to-bottom, then
|
|
260
|
+
* left-to-right), laid out at a fixed gap from the union bbox's top-left,
|
|
261
|
+
* each column/row sized to its widest/tallest member so nothing overlaps.
|
|
262
|
+
*/
|
|
263
|
+
export function computeTidyGrid(rects: KeyedRect[], opts: TidyOptions = {}): MovedRect[] {
|
|
264
|
+
const n = rects.length;
|
|
265
|
+
if (n < 2) return [];
|
|
266
|
+
const gap = opts.gap ?? 16;
|
|
267
|
+
const columns = Math.max(1, Math.min(n, opts.columns ?? Math.round(Math.sqrt(n))));
|
|
268
|
+
const sorted = [...rects].sort((a, b) => a.y - b.y || a.x - b.x);
|
|
269
|
+
const originX = Math.min(...rects.map((r) => r.x));
|
|
270
|
+
const originY = Math.min(...rects.map((r) => r.y));
|
|
271
|
+
const rows = Math.ceil(n / columns);
|
|
272
|
+
const colWidths: number[] = new Array(columns).fill(0);
|
|
273
|
+
const rowHeights: number[] = new Array(rows).fill(0);
|
|
274
|
+
sorted.forEach((r, i) => {
|
|
275
|
+
const row = Math.floor(i / columns);
|
|
276
|
+
const col = i % columns;
|
|
277
|
+
colWidths[col] = Math.max(colWidths[col] ?? 0, r.w);
|
|
278
|
+
rowHeights[row] = Math.max(rowHeights[row] ?? 0, r.h);
|
|
279
|
+
});
|
|
280
|
+
const colX: number[] = [];
|
|
281
|
+
{
|
|
282
|
+
let cursor = originX;
|
|
283
|
+
for (let c = 0; c < columns; c++) {
|
|
284
|
+
colX.push(cursor);
|
|
285
|
+
cursor += (colWidths[c] ?? 0) + gap;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const rowY: number[] = [];
|
|
289
|
+
{
|
|
290
|
+
let cursor = originY;
|
|
291
|
+
for (let r = 0; r < rows; r++) {
|
|
292
|
+
rowY.push(cursor);
|
|
293
|
+
cursor += (rowHeights[r] ?? 0) + gap;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return sorted.map((r, i) => {
|
|
297
|
+
const row = Math.floor(i / columns);
|
|
298
|
+
const col = i % columns;
|
|
299
|
+
const nx = colX[col] ?? r.x;
|
|
300
|
+
const ny = rowY[row] ?? r.y;
|
|
301
|
+
return { key: r.key, x: Math.round(nx), y: Math.round(ny) };
|
|
302
|
+
});
|
|
303
|
+
}
|
|
@@ -271,7 +271,7 @@ const DIALOG_CSS = `
|
|
|
271
271
|
.dc-export-dialog footer button.dc-ed-primary { background: var(--maude-hud-accent, #1a1a1a); color: var(--maude-hud-accent-fg, #fff); border-color: transparent; }
|
|
272
272
|
.dc-export-dialog footer button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
273
273
|
.dc-export-dialog .dc-ed-status { padding: 8px 20px; font-size: 12px; color: var(--maude-chrome-fg-1, rgba(40,30,20,0.65)); border-top: 1px solid var(--maude-chrome-border, rgba(0,0,0,0.08)); }
|
|
274
|
-
.dc-export-dialog .dc-ed-status.is-error { color:
|
|
274
|
+
.dc-export-dialog .dc-ed-status.is-error { color: #c0392b; }
|
|
275
275
|
`;
|
|
276
276
|
|
|
277
277
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -9,6 +9,8 @@ import { existsSync } from 'node:fs';
|
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
|
|
12
|
+
import { DEV_SERVER_ROOT } from '../paths.ts';
|
|
13
|
+
|
|
12
14
|
interface CachedBundle {
|
|
13
15
|
path: string;
|
|
14
16
|
ready: Promise<string>;
|
|
@@ -68,6 +70,121 @@ async function buildIife(entry: string, globalName: string, cachePath: string):
|
|
|
68
70
|
return cachePath;
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
/**
|
|
74
|
+
* DDR-148 — build the in-page video encoder (`video-encode-lib.ts`) to a
|
|
75
|
+
* self-contained browser ESM module cached under the OS temp dir. It imports
|
|
76
|
+
* mediabunny + gifenc (both INLINED — no externals) and assigns
|
|
77
|
+
* `window.__maudeEnc`; the capture shim injects it via
|
|
78
|
+
* `page.addScriptTag({ content, type: 'module' })`. Returns the cache path.
|
|
79
|
+
*
|
|
80
|
+
* Unlike getBrowserBundle this bundles a LOCAL source entry (not an npm
|
|
81
|
+
* package) and keeps ESM (the module's top-level `window.__maudeEnc = …` side
|
|
82
|
+
* effect runs on load — no IIFE export-hoisting needed).
|
|
83
|
+
*/
|
|
84
|
+
let encodeLibReady: Promise<string> | null = null;
|
|
85
|
+
export function getEncodeLibBundle(): Promise<string> {
|
|
86
|
+
if (encodeLibReady) return encodeLibReady;
|
|
87
|
+
// DDR-045: derive from DEV_SERVER_ROOT, never `import.meta.dir`. In a compiled
|
|
88
|
+
// binary the latter is the virtual `/$bunfs/root`, so `Bun.build` fails with
|
|
89
|
+
// "failed to open root directory: /$bunfs/root" (the mp4/gif export bug).
|
|
90
|
+
const entry = path.join(DEV_SERVER_ROOT, 'exporters', 'video-encode-lib.ts');
|
|
91
|
+
const cachePath = path.join(tmpdir(), 'maude-video-encode-lib.mjs');
|
|
92
|
+
encodeLibReady = (async () => {
|
|
93
|
+
const built = await Bun.build({
|
|
94
|
+
entrypoints: [entry],
|
|
95
|
+
target: 'browser',
|
|
96
|
+
format: 'esm',
|
|
97
|
+
minify: true,
|
|
98
|
+
conditions: ['browser', 'import'],
|
|
99
|
+
});
|
|
100
|
+
if (!built.success) {
|
|
101
|
+
throw new Error(`encode-lib bundle failed: ${built.logs.map((l) => l.message).join('; ')}`);
|
|
102
|
+
}
|
|
103
|
+
const first = built.outputs[0];
|
|
104
|
+
if (!first) throw new Error('encode-lib bundle produced no outputs');
|
|
105
|
+
await Bun.write(cachePath, await first.text());
|
|
106
|
+
return cachePath;
|
|
107
|
+
})();
|
|
108
|
+
return encodeLibReady;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* DDR-148 addendum (audio export) — build the in-page whole-comp renderer
|
|
113
|
+
* (`video-render-lib.ts`, wraps @remotion/web-renderer's renderMediaOnWeb) to a
|
|
114
|
+
* self-contained browser ESM module cached under the OS temp dir. Assigns
|
|
115
|
+
* `window.__maudeRenderVideo__`; the capture shim injects it via addScriptTag
|
|
116
|
+
* before calling `window.__maude_render_video__` (video-comp.tsx bridge).
|
|
117
|
+
*
|
|
118
|
+
* UNLIKE getEncodeLibBundle, this externalizes 'remotion' + '@remotion/media' —
|
|
119
|
+
* they must resolve to the SAME module instances the canvas's importmap already
|
|
120
|
+
* loaded (dual-package hazard: a second bundled copy would give
|
|
121
|
+
* renderMediaOnWeb's internal Composition/context providers a different
|
|
122
|
+
* `TimelineContext` than the one the passed `component`'s hooks read from,
|
|
123
|
+
* freezing every frame at 0 / silencing audio). The browser's importmap
|
|
124
|
+
* (declared in _shell.html) resolves these bare specifiers even for a
|
|
125
|
+
* dynamically injected `<script type=module>` tag.
|
|
126
|
+
*/
|
|
127
|
+
let webRendererLibReady: Promise<string> | null = null;
|
|
128
|
+
export function getWebRendererBundle(): Promise<string> {
|
|
129
|
+
if (webRendererLibReady) return webRendererLibReady;
|
|
130
|
+
const entry = path.join(DEV_SERVER_ROOT, 'exporters', 'video-render-lib.ts');
|
|
131
|
+
const cachePath = path.join(tmpdir(), 'maude-video-render-lib.mjs');
|
|
132
|
+
webRendererLibReady = (async () => {
|
|
133
|
+
// Bun's `external` field is package-name PREFIX matched (runtime-bundle.ts's
|
|
134
|
+
// comment on the same gotcha) — listing 'remotion' also externalizes deep
|
|
135
|
+
// subpaths like 'remotion/version' (a real import inside web-renderer's own
|
|
136
|
+
// dependency tree). The importmap only maps the bare 'remotion' specifier, so
|
|
137
|
+
// an externalized 'remotion/version' 404s in the browser with "Failed to
|
|
138
|
+
// resolve module specifier". Use an exact-match onResolve plugin (mirrors
|
|
139
|
+
// canvas-build.ts's `exact-externals`) so ONLY the specifiers the importmap
|
|
140
|
+
// actually covers are externalized; every subpath (version, no-react, etc.)
|
|
141
|
+
// gets bundled inline like any other dependency.
|
|
142
|
+
//
|
|
143
|
+
// react/react-dom MUST also be externalized here (not just remotion) — a
|
|
144
|
+
// second bundled React copy inside this module renders the SAME `component`
|
|
145
|
+
// reference the page's importmap-loaded React created, and two React
|
|
146
|
+
// instances in one page is the textbook "Invalid Hook Call" (minified React
|
|
147
|
+
// error #321): the component's hooks resolve against instance A's dispatcher
|
|
148
|
+
// while web-renderer's internal renderer/reconciler is instance B. Discovered
|
|
149
|
+
// empirically — the first working build threw exactly this error.
|
|
150
|
+
const EXTERNAL_EXACT = new Set([
|
|
151
|
+
'remotion',
|
|
152
|
+
'@remotion/media',
|
|
153
|
+
'react',
|
|
154
|
+
'react-dom',
|
|
155
|
+
'react-dom/client',
|
|
156
|
+
'react/jsx-runtime',
|
|
157
|
+
'react/jsx-dev-runtime',
|
|
158
|
+
]);
|
|
159
|
+
const built = await Bun.build({
|
|
160
|
+
entrypoints: [entry],
|
|
161
|
+
target: 'browser',
|
|
162
|
+
format: 'esm',
|
|
163
|
+
minify: true,
|
|
164
|
+
conditions: ['browser', 'import'],
|
|
165
|
+
plugins: [
|
|
166
|
+
{
|
|
167
|
+
name: 'exact-externals',
|
|
168
|
+
setup(builder) {
|
|
169
|
+
builder.onResolve({ filter: /.*/ }, (args) => {
|
|
170
|
+
if (EXTERNAL_EXACT.has(args.path)) return { path: args.path, external: true };
|
|
171
|
+
return null;
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
});
|
|
177
|
+
if (!built.success) {
|
|
178
|
+
throw new Error(`web-renderer bundle failed: ${built.logs.map((l) => l.message).join('; ')}`);
|
|
179
|
+
}
|
|
180
|
+
const first = built.outputs[0];
|
|
181
|
+
if (!first) throw new Error('web-renderer bundle produced no outputs');
|
|
182
|
+
await Bun.write(cachePath, await first.text());
|
|
183
|
+
return cachePath;
|
|
184
|
+
})();
|
|
185
|
+
return webRendererLibReady;
|
|
186
|
+
}
|
|
187
|
+
|
|
71
188
|
/**
|
|
72
189
|
* Returns the path to an IIFE bundle for the given npm package, attaching its
|
|
73
190
|
* exports under `window[globalName]`. Caches under the OS temp dir so a long-
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Shared runtime + path resolution for the external playwright render shims
|
|
2
|
+
// (`bin/_{png,pdf,svg,html,pptx,video}-playwright.mjs`).
|
|
3
|
+
//
|
|
4
|
+
// RCA: issue-desktop-export-failures. The render exporters were written for the
|
|
5
|
+
// dev-server runtime and never migrated for the compiled-Bun sidecar packaging,
|
|
6
|
+
// so they broke in two ways in every `bun --compile` standalone binary (the
|
|
7
|
+
// desktop app AND `npm i -g @1agh/maude`):
|
|
8
|
+
//
|
|
9
|
+
// 1. Shim paths computed from `import.meta.dir` → inside a compiled binary that
|
|
10
|
+
// is the virtual `/$bunfs/root`, not a real disk path, so `Bun.spawn` /
|
|
11
|
+
// `Bun.build` fail with "failed to open root directory: /$bunfs/root". This
|
|
12
|
+
// is the DDR-045 bug: filesystem-relative paths MUST derive from
|
|
13
|
+
// `paths.ts`, never a local `import.meta.dir`.
|
|
14
|
+
// 2. Spawning a hardcoded `'node'` → the desktop sidecar is a compiled Bun
|
|
15
|
+
// binary with no bundled `node`; a Finder-launched `.app` can't rely on one
|
|
16
|
+
// being on PATH, so the spawn fails with `posix_spawn 'node'` ENOENT.
|
|
17
|
+
//
|
|
18
|
+
// This module centralizes both concerns so all six adapters resolve identically.
|
|
19
|
+
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
|
|
22
|
+
import { DEV_SERVER_ROOT } from '../paths.ts';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Real-disk dir holding the render shims (`<DEV_SERVER_ROOT>/bin`).
|
|
26
|
+
*
|
|
27
|
+
* Derived from `DEV_SERVER_ROOT` (paths.ts) — NEVER `import.meta.dir`, which is
|
|
28
|
+
* the virtual `/$bunfs/root` inside a compiled binary (DDR-045). In dev this is
|
|
29
|
+
* the same `apps/studio/bin` the old `path.join(import.meta.dir, '..', 'bin')`
|
|
30
|
+
* resolved to; in the desktop bundle it resolves to `Resources/apps/studio/bin`
|
|
31
|
+
* (the shims ARE staged there — stage-resources ships the whole source tree).
|
|
32
|
+
*/
|
|
33
|
+
export const EXPORT_SHIM_DIR: string = path.join(DEV_SERVER_ROOT, 'bin');
|
|
34
|
+
|
|
35
|
+
/** Absolute path to a render shim by basename (e.g. `_png-playwright.mjs`). */
|
|
36
|
+
export function exportShimPath(basename: string): string {
|
|
37
|
+
return path.join(EXPORT_SHIM_DIR, basename);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a JS runtime able to execute the external `.mjs` render shims.
|
|
42
|
+
*
|
|
43
|
+
* Ladder (mirrors `acp/probe.ts` resolveAgentRuntime): explicit override → a
|
|
44
|
+
* real `node` on PATH → a standalone `bun` on PATH. The compiled maude binary
|
|
45
|
+
* (`process.execPath`) is deliberately NOT a fallback — a `bun --compile`
|
|
46
|
+
* standalone ignores a script argument and re-runs its own embedded entrypoint,
|
|
47
|
+
* so it can't run a shim. When nothing resolves (a packaged desktop app whose
|
|
48
|
+
* sidecar has no node/bun on PATH) we throw an ACTIONABLE error instead of the
|
|
49
|
+
* cryptic `posix_spawn 'node'` ENOENT the hardcoded `'node'` produced.
|
|
50
|
+
*/
|
|
51
|
+
export function resolveExportRuntime(): string {
|
|
52
|
+
// Look up against the LIVE process PATH (the desktop sidecar sets it via env at
|
|
53
|
+
// spawn — DDR-128), not Bun's startup env snapshot, so a corrected PATH is
|
|
54
|
+
// honored and the lookup is deterministic/testable.
|
|
55
|
+
const pathEnv = process.env.PATH;
|
|
56
|
+
const runtime =
|
|
57
|
+
process.env.MAUDE_EXPORT_RUNTIME ||
|
|
58
|
+
Bun.which('node', { PATH: pathEnv }) ||
|
|
59
|
+
Bun.which('bun', { PATH: pathEnv });
|
|
60
|
+
if (!runtime) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
'render export needs a JS runtime (node or bun) on PATH, but this build has none. ' +
|
|
63
|
+
'Run the export from the browser dev-server (`maude design serve`), install Node, ' +
|
|
64
|
+
'or set MAUDE_EXPORT_RUNTIME to a node/bun binary. ' +
|
|
65
|
+
'(Native desktop render-export backend is pending — see RCA issue-desktop-export-failures.)'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return runtime;
|
|
69
|
+
}
|
|
@@ -11,7 +11,7 @@ import { tmpdir } from 'node:os';
|
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
|
|
13
13
|
import JSZip from 'jszip';
|
|
14
|
-
|
|
14
|
+
import { exportShimPath, resolveExportRuntime } from './_runtime.ts';
|
|
15
15
|
import {
|
|
16
16
|
canvasShellUrl,
|
|
17
17
|
type ExportContext,
|
|
@@ -20,7 +20,8 @@ import {
|
|
|
20
20
|
} from './index.ts';
|
|
21
21
|
import type { Target } from './scope.ts';
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// DDR-045: resolve via DEV_SERVER_ROOT, never `import.meta.dir`. See _runtime.ts.
|
|
24
|
+
const HTML_PLAYWRIGHT = exportShimPath('_html-playwright.mjs');
|
|
24
25
|
|
|
25
26
|
async function captureHtml(
|
|
26
27
|
target: Extract<Target, { kind: 'element' }>,
|
|
@@ -45,7 +46,7 @@ async function captureHtml(
|
|
|
45
46
|
if (target.widen) args.push('--widen-to-artboard', '1');
|
|
46
47
|
args.push('--out', path.join(outDir, `${target.canvasSlug}.html`));
|
|
47
48
|
}
|
|
48
|
-
const proc = Bun.spawn([
|
|
49
|
+
const proc = Bun.spawn([resolveExportRuntime(), ...args], {
|
|
49
50
|
cwd: path.dirname(HTML_PLAYWRIGHT),
|
|
50
51
|
stdout: 'pipe',
|
|
51
52
|
stderr: 'pipe',
|