@1agh/maude 0.58.2 → 0.59.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.
Files changed (140) hide show
  1. package/apps/studio/annotations-bindings.ts +83 -4
  2. package/apps/studio/annotations-layer.tsx +49 -15
  3. package/apps/studio/api.ts +6 -1
  4. package/apps/studio/bin/_fetch-asset.mjs +169 -5
  5. package/apps/studio/bin/_import-asset.mjs +90 -0
  6. package/apps/studio/bin/_import-figma.mjs +1775 -0
  7. package/apps/studio/bin/_perf-probe-safari.mjs +332 -0
  8. package/apps/studio/bin/_perf-probe.mjs +228 -0
  9. package/apps/studio/bin/_perf-shared.mjs +345 -0
  10. package/apps/studio/bin/_video-playwright.mjs +103 -7
  11. package/apps/studio/bin/import-figma.sh +47 -0
  12. package/apps/studio/bin/perf.sh +228 -0
  13. package/apps/studio/bin/read-annotations.mjs +11 -1
  14. package/apps/studio/bin/smoke.sh +49 -5
  15. package/apps/studio/bun.lock +16 -22
  16. package/apps/studio/canvas-edit.ts +29 -5
  17. package/apps/studio/canvas-lib.tsx +148 -6
  18. package/apps/studio/client/app.jsx +196 -38
  19. package/apps/studio/client/export-center.jsx +42 -4
  20. package/apps/studio/client/panels/CloudBar.jsx +92 -1
  21. package/apps/studio/client/panels/FigmaImportPanel.jsx +264 -0
  22. package/apps/studio/client/panels/GitPanel.jsx +26 -6
  23. package/apps/studio/client/panels/SettingsPanel.jsx +181 -0
  24. package/apps/studio/client/panels/SetupChecklist.jsx +26 -2
  25. package/apps/studio/client/panels/SyncPanel.jsx +229 -0
  26. package/apps/studio/client/panels/TimelinePanel.jsx +31 -3
  27. package/apps/studio/client/panels/timeline-comp-target.js +101 -0
  28. package/apps/studio/client/panels/timeline-parse.js +3 -3
  29. package/apps/studio/client/styles/3-shell-maude.css +37 -0
  30. package/apps/studio/client/styles/4-components.css +134 -0
  31. package/apps/studio/clip-ops.ts +93 -17
  32. package/apps/studio/cloud/endpoints.ts +78 -10
  33. package/apps/studio/cloud/renew.ts +183 -0
  34. package/apps/studio/context.ts +2 -1
  35. package/apps/studio/dist/client.bundle.js +1231 -1231
  36. package/apps/studio/dist/runtime/@remotion_media.js +56 -136
  37. package/apps/studio/dist/runtime/@remotion_player.js +18 -18
  38. package/apps/studio/dist/runtime/@remotion_transitions.js +9 -9
  39. package/apps/studio/dist/runtime/@remotion_transitions_clock-wipe.js +1 -1
  40. package/apps/studio/dist/runtime/remotion.js +12 -12
  41. package/apps/studio/dist/styles.css +1 -1
  42. package/apps/studio/exporters/_browser-bundles.ts +20 -6
  43. package/apps/studio/exporters/_runtime.ts +19 -0
  44. package/apps/studio/exporters/degraded.ts +92 -0
  45. package/apps/studio/exporters/index.ts +5 -0
  46. package/apps/studio/exporters/jobs.ts +19 -0
  47. package/apps/studio/exporters/unsupported-media.ts +170 -0
  48. package/apps/studio/exporters/video-encode-lib.ts +35 -6
  49. package/apps/studio/exporters/video-render-lib.ts +6 -0
  50. package/apps/studio/exporters/video.ts +72 -1
  51. package/apps/studio/figma/assets.test.ts +464 -0
  52. package/apps/studio/figma/assets.ts +452 -0
  53. package/apps/studio/figma/client.test.ts +395 -0
  54. package/apps/studio/figma/client.ts +513 -0
  55. package/apps/studio/figma/codegen-client.test.ts +276 -0
  56. package/apps/studio/figma/codegen-client.ts +509 -0
  57. package/apps/studio/figma/codegen-fonts.test.ts +103 -0
  58. package/apps/studio/figma/codegen-fonts.ts +195 -0
  59. package/apps/studio/figma/codegen-values.test.ts +179 -0
  60. package/apps/studio/figma/codegen-values.ts +270 -0
  61. package/apps/studio/figma/comments-to-strokes.test.ts +194 -0
  62. package/apps/studio/figma/comments-to-strokes.ts +173 -0
  63. package/apps/studio/figma/endpoints.ts +273 -0
  64. package/apps/studio/figma/fig-decode.test.ts +702 -0
  65. package/apps/studio/figma/fig-decode.ts +617 -0
  66. package/apps/studio/figma/fig-kiwi.ts +410 -0
  67. package/apps/studio/figma/fig-zip.ts +270 -0
  68. package/apps/studio/figma/from-codegen.test.ts +408 -0
  69. package/apps/studio/figma/from-codegen.ts +1103 -0
  70. package/apps/studio/figma/sanitize.test.ts +325 -0
  71. package/apps/studio/figma/sanitize.ts +407 -0
  72. package/apps/studio/figma/style-map.ts +352 -0
  73. package/apps/studio/figma/tailwind-map.test.ts +142 -0
  74. package/apps/studio/figma/tailwind-map.ts +545 -0
  75. package/apps/studio/figma/to-artboard.test.ts +808 -0
  76. package/apps/studio/figma/to-artboard.ts +701 -0
  77. package/apps/studio/figma/to-render.test.ts +180 -0
  78. package/apps/studio/figma/to-render.ts +328 -0
  79. package/apps/studio/figma/to-strokes-roundtrip.test.ts +152 -0
  80. package/apps/studio/figma/to-strokes.test.ts +705 -0
  81. package/apps/studio/figma/to-strokes.ts +749 -0
  82. package/apps/studio/figma/to-tokens.test.ts +321 -0
  83. package/apps/studio/figma/to-tokens.ts +305 -0
  84. package/apps/studio/figma/types.ts +544 -0
  85. package/apps/studio/figma/url.test.ts +167 -0
  86. package/apps/studio/figma/url.ts +160 -0
  87. package/apps/studio/http.ts +176 -0
  88. package/apps/studio/sync/asset-push.ts +432 -0
  89. package/apps/studio/sync/connection-state.ts +82 -3
  90. package/apps/studio/sync/hub-link.ts +63 -7
  91. package/apps/studio/sync/hubs-config.ts +31 -3
  92. package/apps/studio/sync/index.ts +286 -27
  93. package/apps/studio/sync/migrate-flat-fallback.ts +121 -0
  94. package/apps/studio/sync/presentation.ts +45 -1
  95. package/apps/studio/sync/status.ts +18 -0
  96. package/apps/studio/sync/supervisor.ts +5 -1
  97. package/apps/studio/sync/workspace-signin.ts +7 -3
  98. package/apps/studio/test/annotations-bindings.test.ts +150 -12
  99. package/apps/studio/test/canvas-create-api.test.ts +4 -1
  100. package/apps/studio/test/canvas-origin-gate.test.ts +17 -0
  101. package/apps/studio/test/capture-determinism-shape.test.ts +135 -0
  102. package/apps/studio/test/clip-addressing.test.ts +6 -1
  103. package/apps/studio/test/clip-ops.test.ts +5 -1
  104. package/apps/studio/test/cloud-endpoints.test.ts +96 -0
  105. package/apps/studio/test/cloud-renew.test.ts +205 -0
  106. package/apps/studio/test/cloud-shell-surfaces.test.ts +11 -2
  107. package/apps/studio/test/exporters/degraded-propagation.test.ts +123 -0
  108. package/apps/studio/test/exporters/unsupported-media.test.ts +123 -0
  109. package/apps/studio/test/fetch-asset-gate.test.ts +189 -0
  110. package/apps/studio/test/figma-explode.test.ts +438 -0
  111. package/apps/studio/test/figma-provenance.test.ts +108 -0
  112. package/apps/studio/test/figma-routes.test.ts +294 -0
  113. package/apps/studio/test/fixtures/perf-canvas.mjs +201 -0
  114. package/apps/studio/test/git-cloud-posture.test.ts +50 -0
  115. package/apps/studio/test/hub-link.test.ts +11 -0
  116. package/apps/studio/test/import-figma.test.ts +667 -0
  117. package/apps/studio/test/sync-asset-push.test.ts +567 -0
  118. package/apps/studio/test/sync-connection-state.test.ts +79 -0
  119. package/apps/studio/test/sync-hubs-config.test.ts +5 -0
  120. package/apps/studio/test/sync-migrate-flat-fallback.test.ts +98 -0
  121. package/apps/studio/test/sync-panel-surface.test.ts +90 -0
  122. package/apps/studio/test/sync-path-pull.test.ts +63 -0
  123. package/apps/studio/test/sync-presentation.test.ts +77 -0
  124. package/apps/studio/test/sync-runtime.test.ts +316 -1
  125. package/apps/studio/test/sync-status.test.ts +28 -0
  126. package/apps/studio/test/timeline-comp-target.test.ts +139 -0
  127. package/apps/studio/test/video-comp.test.ts +104 -2
  128. package/apps/studio/test/video-encode-lib.test.ts +63 -0
  129. package/apps/studio/test/workspace-containment.test.ts +1 -0
  130. package/apps/studio/use-artboard-drag.tsx +37 -3
  131. package/apps/studio/video-comp.tsx +121 -6
  132. package/apps/studio/whats-new.json +98 -0
  133. package/apps/studio/workspace-mode.ts +4 -0
  134. package/cli/commands/design.mjs +15 -0
  135. package/cli/commands/kg.mjs +8 -1
  136. package/cli/commands/kg.test.mjs +24 -0
  137. package/cli/lib/figma-codegen-reachability.test.mjs +104 -0
  138. package/cli/lib/figma-import-controls.test.mjs +70 -0
  139. package/package.json +8 -8
  140. package/plugins/flow/.claude-plugin/config.schema.json +3 -3
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env bash
2
+ # perf.sh — render-performance benchmark for a canvas (`maude design perf`).
3
+ #
4
+ # Drives a scripted pan + zoom against a live canvas and reports frame-time
5
+ # percentiles, long-task count and the React render count during the gesture,
6
+ # with a DELTA against the previous run of the same (canvas, engine) pair. The
7
+ # history file makes "before and after" a property of the tool rather than
8
+ # something the operator has to remember to write down.
9
+ #
10
+ # Deliberately NOT a CI gate. Headless frame timings swing with GPU load and
11
+ # machine state, so an absolute threshold would go red for reasons unrelated to
12
+ # the code and train everyone to ignore it. The gate is the delta, read by a
13
+ # human, on one machine. (Plan: .ai/plans/feature-canvas-render-performance.md)
14
+ #
15
+ # Usage:
16
+ # perf.sh [--root <repo>] [--canvas <rel-path>] [--fixture]
17
+ # [--boards N] [--strokes N]
18
+ # [--pan N] [--zoom N] [--timeout S] [--repeat N]
19
+ # [--engine chromium|safari]
20
+ # [--history <path>] [--json]
21
+ #
22
+ # --engine safari measures WebKit through safaridriver (one-time
23
+ # `safaridriver --enable`, needs admin auth). Use it for anything the Tauri
24
+ # desktop shell has to be fast at: the desktop is WKWebView, and Chromium does
25
+ # NOT reproduce WebKit's compositing / re-raster behaviour — a canvas can sit
26
+ # at a warm 60fps in headless Chromium while being unusable in the .app.
27
+ #
28
+ # --canvas Canvas to measure, relative to <designRoot> (default: _active.json).
29
+ # --fixture Generate + measure the synthetic fixture instead (--boards/--strokes).
30
+ # --json Raw JSON (current + previous run) instead of the human report.
31
+ #
32
+ # Reads: $DESIGN_ROOT/_server.json (must exist — run `maude design server-up` first)
33
+ # Writes: $DESIGN_ROOT/_smoke/perf/history.jsonl (append-only, per-machine runtime state)
34
+ # $DESIGN_ROOT/ui/perf-fixture.tsx (only with --fixture)
35
+ #
36
+ # Exit: 0 measured / 1 missing dep or capture failure / 2 bad args.
37
+
38
+ REPO=""
39
+ CANVAS=""
40
+ FIXTURE=0
41
+ BOARDS=128
42
+ STROKES=150
43
+ PAN=60
44
+ ZOOM=40
45
+ REPEAT=3
46
+ PROBE_ENGINE="chromium"
47
+ FIT_ALL=0
48
+ STUDIO=0
49
+ TIMEOUT=30
50
+ HISTORY=""
51
+ JSON=0
52
+
53
+ while [ $# -gt 0 ]; do
54
+ case "$1" in
55
+ --root) REPO="$2"; shift 2 ;;
56
+ --canvas) CANVAS="$2"; shift 2 ;;
57
+ --fixture) FIXTURE=1; shift ;;
58
+ --boards) BOARDS="$2"; shift 2 ;;
59
+ --strokes) STROKES="$2"; shift 2 ;;
60
+ --pan) PAN="$2"; shift 2 ;;
61
+ --zoom) ZOOM="$2"; shift 2 ;;
62
+ --timeout) TIMEOUT="$2"; shift 2 ;;
63
+ --repeat) REPEAT="$2"; shift 2 ;;
64
+ --engine) PROBE_ENGINE="$2"; shift 2 ;;
65
+ --fit-all) FIT_ALL=1; shift ;;
66
+ --studio) STUDIO=1; shift ;;
67
+ --history) HISTORY="$2"; shift 2 ;;
68
+ --json) JSON=1; shift ;;
69
+ --help|-h)
70
+ sed -n '2,30p' "$0" | sed 's/^# \?//'
71
+ exit 0
72
+ ;;
73
+ *)
74
+ echo "perf.sh: unknown arg '$1' (try --help)" >&2
75
+ exit 2
76
+ ;;
77
+ esac
78
+ done
79
+
80
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
81
+
82
+ # JS runtime. The packaged desktop app ships no user `node` (DDR-177) but always
83
+ # has a `bun` on PATH (real, or the compiled-sidecar shim `maude` stages), so
84
+ # prefer bun and keep node as the developer-machine fallback.
85
+ if command -v bun >/dev/null 2>&1; then JS_RUNTIME="bun"; else JS_RUNTIME="node"; fi
86
+
87
+ # ---------- resolve repo + design root (mirrors smoke.sh) ----------
88
+ if [ -z "$REPO" ]; then
89
+ REPO="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
90
+ fi
91
+ DESIGN_ROOT="$REPO/.design"
92
+ [ -d "$DESIGN_ROOT" ] || { echo "perf.sh: no .design/ under $REPO" >&2; exit 1; }
93
+
94
+ STATE="$DESIGN_ROOT/_server.json"
95
+ [ -f "$STATE" ] || { echo "perf.sh: $STATE missing — run 'maude design server-up' first" >&2; exit 1; }
96
+
97
+ if command -v jq >/dev/null 2>&1; then
98
+ PORT=$(jq -r .port "$STATE" 2>/dev/null)
99
+ else
100
+ PORT=$(sed -nE 's/.*"port"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p' "$STATE" | head -n1)
101
+ fi
102
+ [ -n "$PORT" ] || { echo "perf.sh: no port in $STATE" >&2; exit 1; }
103
+
104
+ # ---------- fixture generation ----------
105
+ if [ "$FIXTURE" = "1" ]; then
106
+ # Counts are validated as plain integers and paths travel via argv, never
107
+ # interpolated into the script body: `--boards "1); <js>; ("` or a design root
108
+ # containing a quote would otherwise execute inside this node process, which
109
+ # runs with the user's full privileges. `maude design perf` is reachable from
110
+ # an agent session, so its arguments are not automatically trustworthy.
111
+ case "$BOARDS" in ''|*[!0-9]*) echo "perf.sh: --boards must be a positive integer" >&2; exit 2 ;; esac
112
+ case "$STROKES" in ''|*[!0-9]*) echo "perf.sh: --strokes must be a non-negative integer" >&2; exit 2 ;; esac
113
+ # Upper bound too: the guards above accept any magnitude, and `--boards 99999999`
114
+ # is a cheap way to make an auto-approved verb fill a disk.
115
+ [ "$BOARDS" -le 2000 ] || { echo "perf.sh: --boards must be <= 2000" >&2; exit 2; }
116
+ [ "$STROKES" -le 5000 ] || { echo "perf.sh: --strokes must be <= 5000" >&2; exit 2; }
117
+ "$JS_RUNTIME" -e '
118
+ const [modPath, designRoot, boards, strokes] = process.argv.slice(1);
119
+ import(modPath).then((m) => {
120
+ const r = m.writePerfCanvas({
121
+ designRoot,
122
+ boards: Number(boards),
123
+ strokes: Number(strokes),
124
+ });
125
+ console.error("→ fixture written: " + r.canvasPath);
126
+ }).catch((e) => {
127
+ console.error("perf.sh: fixture generation failed: " + e.message);
128
+ process.exit(1);
129
+ });
130
+ ' "$SCRIPT_DIR/../test/fixtures/perf-canvas.mjs" "$DESIGN_ROOT" "$BOARDS" "$STROKES" || exit 1
131
+ CANVAS="ui/perf-fixture.tsx"
132
+ fi
133
+
134
+ # ---------- resolve canvas ----------
135
+ if [ -z "$CANVAS" ]; then
136
+ ACTIVE_FILE="$DESIGN_ROOT/_active.json"
137
+ if [ -f "$ACTIVE_FILE" ] && command -v jq >/dev/null 2>&1; then
138
+ CANVAS=$(jq -r '.active // empty' "$ACTIVE_FILE" 2>/dev/null)
139
+ fi
140
+ fi
141
+ [ -n "$CANVAS" ] || { echo "perf.sh: no --canvas and no active canvas in _active.json" >&2; exit 2; }
142
+
143
+ # `_active.json` stores the path relative to the REPO (".design/ui/x.tsx"); the
144
+ # canvas-shell query param wants it the same way, so normalize either input
145
+ # shape to the repo-relative form rather than guessing at call sites.
146
+ DESIGN_REL="${DESIGN_ROOT#"$REPO"/}"
147
+ case "$CANVAS" in
148
+ "$DESIGN_REL"/*) REL_FULL="$CANVAS" ;;
149
+ *) REL_FULL="$DESIGN_REL/$CANVAS" ;;
150
+ esac
151
+ # Containment: an existence check alone would accept `--canvas ../../../x.tsx`
152
+ # and leave the boundary entirely to the dev-server route. A benchmark has no
153
+ # business probing that gate.
154
+ case "$REL_FULL" in
155
+ *..*) echo "perf.sh: --canvas must not contain '..'" >&2; exit 2 ;;
156
+ esac
157
+ [ -f "$REPO/$REL_FULL" ] || { echo "perf.sh: canvas not found: $REPO/$REL_FULL" >&2; exit 2; }
158
+
159
+ # Percent-encode the whole value, not just spaces: a canvas named with `#`, `&`
160
+ # or `%` would otherwise truncate or rewrite the query string.
161
+ REL_ENC=$("$JS_RUNTIME" -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$REL_FULL")
162
+ URL="http://localhost:${PORT}/_canvas-shell.html?canvas=${REL_ENC}"
163
+
164
+ # ---------- history path ----------
165
+ # `_smoke/` is already IGNORED runtime state (DDR-115), so the benchmark log
166
+ # inherits the right gitignore posture without touching any of the three lists.
167
+ if [ -z "$HISTORY" ]; then
168
+ HISTORY="$DESIGN_ROOT/_smoke/perf/history.jsonl"
169
+ fi
170
+ # Containment: `perf` is auto-approved in ACP sessions (`Bash(maude:*)`), so an
171
+ # unconstrained --history would be an arbitrary mkdir + append outside every
172
+ # write-scope gate. The default already lives under the design root; require it.
173
+ HISTORY_ABS=$("$JS_RUNTIME" -e 'const p=require("path");process.stdout.write(p.resolve(process.argv[1]))' "$HISTORY")
174
+ DESIGN_ABS=$("$JS_RUNTIME" -e 'const p=require("path");process.stdout.write(p.resolve(process.argv[1]))' "$DESIGN_ROOT")
175
+ case "$HISTORY_ABS" in
176
+ "$DESIGN_ABS"/*) ;;
177
+ *) echo "perf.sh: --history must resolve under $DESIGN_ABS (got $HISTORY_ABS)" >&2; exit 2 ;;
178
+ esac
179
+ HISTORY="$HISTORY_ABS"
180
+ mkdir -p "$(dirname "$HISTORY")"
181
+
182
+ # ---------- engine tag ----------
183
+ # The tag is part of the history KEY, not decoration: a WKWebView number and a
184
+ # headless-Chromium number are not comparable, and silently averaging them would
185
+ # make every delta meaningless.
186
+ ENGINE_TAG="${MAUDE_PERF_ENGINE_TAG:-}"
187
+ if [ -z "$ENGINE_TAG" ]; then
188
+ if [ -n "$MAUDE_DESKTOP" ]; then ENGINE_TAG="webkit-desktop"; else ENGINE_TAG="blink-headless"; fi
189
+ fi
190
+
191
+ if [ "$PROBE_ENGINE" = "safari" ]; then
192
+ command -v safaridriver >/dev/null 2>&1 || {
193
+ echo "perf.sh: safaridriver not found (macOS only)" >&2
194
+ exit 1
195
+ }
196
+ PROBE_URL="$URL"
197
+ if [ "$STUDIO" = "1" ]; then
198
+ # Studio mode drives the real app shell (root URL + a file-tree click),
199
+ # not the bare canvas page — see _perf-probe-safari.mjs for why that
200
+ # difference changes the numbers.
201
+ PROBE_URL="http://localhost:${PORT}/"
202
+ SLUG=$(printf '%s' "${REL_FULL#"$DESIGN_REL"/}" \
203
+ | sed -E 's/\.[A-Za-z]+$//' | tr '[:upper:]' '[:lower:]' \
204
+ | sed -E 's/[^a-z0-9]+/-/g; s/^-+|-+$//g')
205
+ fi
206
+ echo "→ perf: $REL_FULL | port: $PORT | engine: webkit-safari${STUDIO:+ | studio}" >&2
207
+ ARGS=(--url "$PROBE_URL" --label "$REL_FULL$([ "$STUDIO" = "1" ] && echo " (studio)")" --history "$HISTORY"
208
+ --timeout "$TIMEOUT" --pan "$PAN" --zoom "$ZOOM" --repeat "$REPEAT")
209
+ [ "$STUDIO" = "1" ] && ARGS+=(--studio "$SLUG")
210
+ [ "$FIT_ALL" = "1" ] && ARGS+=(--fit-all)
211
+ [ "$JSON" = "1" ] && ARGS+=(--json)
212
+ exec "$JS_RUNTIME" "$SCRIPT_DIR/_perf-probe-safari.mjs" "${ARGS[@]}"
213
+ fi
214
+
215
+ AB="${MAUDE_AGENT_BROWSER:-agent-browser}"
216
+ command -v "$AB" >/dev/null 2>&1 || {
217
+ echo "perf.sh: agent-browser not on PATH (required — the probe needs scripted gestures + rAF timing)" >&2
218
+ exit 1
219
+ }
220
+
221
+ echo "→ perf: $REL_FULL | port: $PORT | engine: $ENGINE_TAG" >&2
222
+
223
+ ARGS=(--url "$URL" --label "$REL_FULL" --history "$HISTORY" --engine-tag "$ENGINE_TAG"
224
+ --timeout "$TIMEOUT" --pan "$PAN" --zoom "$ZOOM" --repeat "$REPEAT")
225
+ [ "$FIT_ALL" = "1" ] && ARGS+=(--fit-all)
226
+ [ "$JSON" = "1" ] && ARGS+=(--json)
227
+
228
+ exec "$JS_RUNTIME" "$SCRIPT_DIR/_perf-probe.mjs" "${ARGS[@]}"
@@ -106,6 +106,9 @@ Args:
106
106
 
107
107
  FigJam v3 additive fields per annotation: z (render order), groupIds (deepest →
108
108
  shallowest), author ("ai" for annotate-verb strokes), authorName/authorId
109
+ // (an IMPORTED stroke carries authorName "imported-figma" — DDR-216 D7, so a
110
+ // consuming skill can tell third-party content from what the user drew), the
111
+ // rendered fontSize for text/sticky (so 8 px is distinguishable from a heading)
109
112
  (human-drawn stickies), and from/to host ids on bound arrows.
110
113
 
111
114
  Section annotations additionally carry members: an array of every OTHER
@@ -210,6 +213,10 @@ function readText(attrs, inner) {
210
213
  h: null,
211
214
  text: decodeEntities(inner).trim(),
212
215
  color: attr(attrs, 'fill') || attr(attrs, 'stroke') || null,
216
+ // DDR-216 D6b / review F5 — a consuming skill must be able to see HOW BIG
217
+ // this renders. Without it, text at the 8 px floor is indistinguishable
218
+ // from a heading in the JSON an agent is handed.
219
+ fontSize: num(attr(attrs, 'font-size')),
213
220
  };
214
221
  if (anchorId) o.anchorId = anchorId;
215
222
  return o;
@@ -235,7 +242,8 @@ function readSticky(attrs, inner) {
235
242
  // Geometry off the inner <rect>; paper tint off the group fill; body off the
236
243
  // inner <text>. (annotations-layer.tsx strokeToSvgEl 'sticky' branch.)
237
244
  const rectAttrs = inner.match(/<rect\b([^>]*?)\/?>/)?.[1] ?? '';
238
- const body = inner.match(/<text\b[^>]*>([\s\S]*?)<\/text>/)?.[1] ?? '';
245
+ const textEl = inner.match(/<text\b([^>]*)>([\s\S]*?)<\/text>/);
246
+ const body = textEl?.[2] ?? '';
239
247
  return {
240
248
  tool: 'sticky',
241
249
  id: attr(attrs, 'data-id') || '',
@@ -245,6 +253,8 @@ function readSticky(attrs, inner) {
245
253
  h: num(attr(rectAttrs, 'height')),
246
254
  text: decodeEntities(body).trim(),
247
255
  color: attr(attrs, 'fill') || null,
256
+ // See readText — a consuming skill needs the rendered size.
257
+ fontSize: num(attr(textEl?.[1] ?? '', 'font-size')),
248
258
  };
249
259
  }
250
260
 
@@ -26,10 +26,14 @@
26
26
  # [--changed-only] (default off — screenshot only canvases changed
27
27
  # since the last smoke run; escalates to the full
28
28
  # set when dev-server / canvas-lib / templates changed)
29
+ # [--perf] (also benchmark the heaviest canvas after the sweep
30
+ # and append the frame-time / render-count delta to
31
+ # the report — see perf.sh)
29
32
  #
30
33
  # Reads:
31
34
  # $DESIGN_ROOT/_server.json (must exist — caller runs server-up.sh first)
32
- # $DESIGN_ROOT/ui/*.tsx (canvases)
35
+ # $DESIGN_ROOT/ui/**/*.tsx (canvases, RECURSIVELY — `--pages` imports
36
+ # land in ui/<folder>/ and were being missed)
33
37
  # $DESIGN_ROOT/system/*/preview/*.tsx (specimens, when --include-system 1)
34
38
  #
35
39
  # Writes:
@@ -52,6 +56,7 @@ INCLUDE_SYSTEM=1
52
56
  TIMEOUT=8
53
57
  ENGINE="auto"
54
58
  CHANGED_ONLY=0
59
+ PERF=0
55
60
 
56
61
  while [ $# -gt 0 ]; do
57
62
  case "$1" in
@@ -61,6 +66,7 @@ while [ $# -gt 0 ]; do
61
66
  --timeout) TIMEOUT="$2"; shift 2 ;;
62
67
  --engine) ENGINE="$2"; shift 2 ;;
63
68
  --changed-only) CHANGED_ONLY=1; shift ;;
69
+ --perf) PERF=1; shift ;;
64
70
  --help|-h)
65
71
  sed -n '2,30p' "$0" | sed 's/^# \?//'
66
72
  exit 0
@@ -111,13 +117,24 @@ fi
111
117
  echo "→ smoke engine: $ENGINE | port: $PORT | out: $OUT_DIR" >&2
112
118
 
113
119
  # ---------- collect canvases ----------
120
+ #
121
+ # RECURSIVE, and that is a fix, not a preference. This was `-maxdepth 1` and the
122
+ # consequence was measured on the first live Figma migration: `--pages` always
123
+ # writes to `ui/<folder>/`, so EVERY canvas the primary import route produces was
124
+ # invisible here. The run reported "52/52 canvases rendered styled" having never
125
+ # looked at the 6 imported pages — green, true, and worthless. The import-graph
126
+ # lint below already walks `ui/` recursively, which is exactly why the mismatch
127
+ # went unnoticed: the lint covered the imports, the screenshots did not.
128
+ #
129
+ # A `_`-prefixed DIRECTORY is skipped as well as a `_`-prefixed file, so runtime
130
+ # state (`_history/`, `_draw/`, `_smoke/` output itself) never enters the sweep —
131
+ # without that, recursing would screenshot our own previous reports.
114
132
  CANVASES=""
115
133
  if [ -d "$DESIGN_ROOT/ui" ]; then
116
134
  while IFS= read -r f; do
117
- base=$(basename "$f")
118
- case "$base" in _*) continue ;; esac
135
+ case "$f" in *'/_'*) continue ;; esac
119
136
  CANVASES="$CANVASES$f"$'\n'
120
- done < <(find "$DESIGN_ROOT/ui" -maxdepth 1 -type f -name '*.tsx' 2>/dev/null | sort)
137
+ done < <(find "$DESIGN_ROOT/ui" -type f -name '*.tsx' 2>/dev/null | sort)
121
138
  fi
122
139
 
123
140
  if [ "$INCLUDE_SYSTEM" = "1" ] && [ -d "$DESIGN_ROOT/system" ]; then
@@ -130,7 +147,7 @@ fi
130
147
 
131
148
  CANVASES=$(printf '%s' "$CANVASES" | sed '/^$/d')
132
149
  COUNT=$(printf '%s' "$CANVASES" | grep -c .)
133
- [ "$COUNT" -gt 0 ] || { echo "smoke.sh: no canvases found (ui/*.tsx or system/*/preview/*.tsx)" >&2; exit 1; }
150
+ [ "$COUNT" -gt 0 ] || { echo "smoke.sh: no canvases found (ui/**/*.tsx or system/*/preview/*.tsx)" >&2; exit 1; }
134
151
  echo "→ found $COUNT canvases" >&2
135
152
 
136
153
  # ---------- --changed-only incremental filter (Phase C / DDR-061) ----------
@@ -525,6 +542,33 @@ TOTAL_FAIL=$((FAILED + LINT_FAILED))
525
542
  fi
526
543
  } >> "$MD"
527
544
 
545
+ # ---------- optional perf pass (--perf) ----------
546
+ # Smoke answers "does it still render"; this answers "is it still fast", against
547
+ # the same canvases, in the same run. The heaviest canvas (most artboards) is the
548
+ # one worth timing — a 1-artboard specimen has nothing to say about pan/zoom.
549
+ if [ "$PERF" = "1" ]; then
550
+ HEAVIEST=""
551
+ HEAVIEST_N=0
552
+ while IFS= read -r f; do
553
+ [ -n "$f" ] || continue
554
+ n=$(grep -c 'DCArtboard' "$f" 2>/dev/null || echo 0)
555
+ if [ "$n" -gt "$HEAVIEST_N" ]; then HEAVIEST_N="$n"; HEAVIEST="$f"; fi
556
+ done <<< "$CANVASES"
557
+ if [ -n "$HEAVIEST" ]; then
558
+ REL_PERF="${HEAVIEST#"$REPO"/}"
559
+ echo "→ perf: benchmarking heaviest canvas ($HEAVIEST_N artboards): $REL_PERF" >&2
560
+ {
561
+ echo ""
562
+ echo "## Perf"
563
+ echo ""
564
+ echo '```'
565
+ bash "$(dirname "${BASH_SOURCE[0]}")/perf.sh" --root "$REPO" --canvas "$REL_PERF" 2>&1 || \
566
+ echo "(perf run failed — see stderr; smoke result above is unaffected)"
567
+ echo '```'
568
+ } >> "$MD"
569
+ fi
570
+ fi
571
+
528
572
  echo "→ report: $MD" >&2
529
573
  echo "→ tsv: $TSV" >&2
530
574
 
@@ -14,10 +14,10 @@
14
14
  "@mediabunny/aac-encoder": "1.50.7",
15
15
  "@mediabunny/flac-encoder": "1.50.7",
16
16
  "@mediabunny/mp3-encoder": "1.50.7",
17
- "@remotion/media": "4.0.486",
18
- "@remotion/player": "4.0.486",
19
- "@remotion/transitions": "4.0.486",
20
- "@remotion/web-renderer": "4.0.486",
17
+ "@remotion/media": "4.0.507",
18
+ "@remotion/player": "4.0.507",
19
+ "@remotion/transitions": "4.0.507",
20
+ "@remotion/web-renderer": "4.0.507",
21
21
  "dom-to-pptx": "^1.1.10",
22
22
  "dom-to-svg": "^0.12.2",
23
23
  "gifenc": "^1.0.3",
@@ -37,7 +37,7 @@
37
37
  "pptxgenjs": "^4.0.1",
38
38
  "react": "^19.2.7",
39
39
  "react-dom": "^19.2.7",
40
- "remotion": "4.0.486",
40
+ "remotion": "4.0.507",
41
41
  "svgo": "^4.0.1",
42
42
  "y-protocols": "^1.0.7",
43
43
  "yjs": "^13.6.31",
@@ -323,19 +323,19 @@
323
323
 
324
324
  "@radix-ui/rect": ["@radix-ui/rect@1.1.2", "", {}, "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA=="],
325
325
 
326
- "@remotion/licensing": ["@remotion/licensing@4.0.486", "", {}, "sha512-9viDTCmthh0LntxGQxKaArHPVW6wAc6FmoZfBNdIHfXoE9FCfhb+VXeJVhfx2Z6awPgu7vn93wstGBMussODOw=="],
326
+ "@remotion/licensing": ["@remotion/licensing@4.0.507", "", {}, "sha512-fdods4YD9c8Ww69wHdnmXpjx7W0rvct6B7gLb92cWsd+ITSUG3hKoh+NqSWXF0GK5bk8IFRmTtBAwJObxow9wA=="],
327
327
 
328
- "@remotion/media": ["@remotion/media@4.0.486", "", { "dependencies": { "mediabunny": "1.50.3", "remotion": "4.0.486", "zod": "4.3.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-vRglyMIvLDuVkj6r1miIcWDoVOoTHsWig4dMLM+F6Z5Z9BxVC95Vtlu8HjrtAvpV6K0AZ1uK3RhXemI+HybvCQ=="],
328
+ "@remotion/media": ["@remotion/media@4.0.507", "", { "dependencies": { "mediabunny": "1.50.8", "remotion": "4.0.507", "zod": "4.4.3" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-PpSbLq0x2E/yZGHyYcGsFpUWcocy+w2uV64qcTgASSN3Vd2ZSwNAaAGrNCUD9nN3EUaHjsoqheL/o8cRTZZ06g=="],
329
329
 
330
- "@remotion/paths": ["@remotion/paths@4.0.486", "", {}, "sha512-/qhmgyQ7/xUxbxnuqtZqfAjBzkV15Tvw73r0wKS82uVE+pBzlS14aucgoJcfksDv26VwCXn6BdHpUXUjXFCjhA=="],
330
+ "@remotion/paths": ["@remotion/paths@4.0.507", "", {}, "sha512-/2vXtdZDvpicamS30CmW+qYkA/yWhTUCk/5MysLExZYll8pqCR+BTlluRwpkfqhH0H4VJ4LwDoY1ZRd3MQ1HeQ=="],
331
331
 
332
- "@remotion/player": ["@remotion/player@4.0.486", "", { "dependencies": { "remotion": "4.0.486" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-S8wh+bS695gidwXYNnCjwZzJG9oPzyYBFMjpO0rlTbrNc3naIKT8UP443J0opitP4g8VuXnoIKObF7JzAfpe5A=="],
332
+ "@remotion/player": ["@remotion/player@4.0.507", "", { "dependencies": { "remotion": "4.0.507" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-ZAunyyCfOXSYh5sZrp+vFxZlSPatg5evmh7fHVCndAOWBUPIDVjdUWEM2D4i132MsN6GXasXDByi+6H0H+IkhQ=="],
333
333
 
334
- "@remotion/shapes": ["@remotion/shapes@4.0.486", "", { "dependencies": { "@remotion/paths": "4.0.486", "remotion": "4.0.486" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-k+2EiXVd6heKVnGACfSnFF5Ox46lvsq6RFtHvPc3CunSMJa04Ui1Z4vNQDcddCSXoHnT/+vht6bjZNlSk36OrQ=="],
334
+ "@remotion/shapes": ["@remotion/shapes@4.0.507", "", { "dependencies": { "@remotion/paths": "4.0.507", "remotion": "4.0.507" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-PinB0Va4FRdv9OeaoIxikPqius9XQj5S5xruQEg/Y1CX+1Eu9KOVXqPOrMgELjkfPLLQ0rA/I4gmKoVxhaoYDQ=="],
335
335
 
336
- "@remotion/transitions": ["@remotion/transitions@4.0.486", "", { "dependencies": { "@remotion/paths": "4.0.486", "@remotion/shapes": "4.0.486", "remotion": "4.0.486" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-U7K21B2NsPie3AE4vrbX7Th46Nw1dFvjs5smYLjuYRaSWPO6vTbNbPQn+pQFXf/dzTjygTNUU9Co5s3oZZNz9A=="],
336
+ "@remotion/transitions": ["@remotion/transitions@4.0.507", "", { "dependencies": { "@remotion/paths": "4.0.507", "@remotion/shapes": "4.0.507", "remotion": "4.0.507" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-d0rtF2gVdSxR4Q35kjSSEdQ1ji0WoKFRaeoNgx/7NO13c/3gDSECAg9m2LoP+fN5pkZFdfL8z+yhM16rL7rWjg=="],
337
337
 
338
- "@remotion/web-renderer": ["@remotion/web-renderer@4.0.486", "", { "dependencies": { "@mediabunny/aac-encoder": "1.50.3", "@mediabunny/flac-encoder": "1.50.3", "@mediabunny/mp3-encoder": "1.50.3", "@remotion/licensing": "4.0.486", "mediabunny": "1.50.3", "remotion": "4.0.486" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-RbG4qFmg6paFDDc/rNL/3hooDPEWPnHu8MtDJB5axD/TiXFsaNR009L9dP610b9hPGwlykIOooSlD7INzo0Gqw=="],
338
+ "@remotion/web-renderer": ["@remotion/web-renderer@4.0.507", "", { "dependencies": { "@mediabunny/aac-encoder": "1.50.8", "@mediabunny/flac-encoder": "1.50.8", "@mediabunny/mp3-encoder": "1.50.8", "@remotion/licensing": "4.0.507", "mediabunny": "1.50.8", "remotion": "4.0.507" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-fH8iZX1e0hmeJ+hPqn3RfqrDBNxMGsZZzfdyX2QpTDZ9F2kKqOkh0dQsRqaU8o+YoE8JNV/+FyYm4yyDAh9o6Q=="],
339
339
 
340
340
  "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="],
341
341
 
@@ -757,7 +757,7 @@
757
757
 
758
758
  "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
759
759
 
760
- "remotion": ["remotion@4.0.486", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-njrfAmfr+fJCr4NXY6hlbzWpIRkfnF0lPdRGZ4DkQao3/XRUtuB166GcVuaVnUpwa8NP+W+Q6TczOPveUpr0pw=="],
760
+ "remotion": ["remotion@4.0.507", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-g/NrnRZKJyh0isE+/0sIxKlgJl6BH5v/CJ35W4TGYR1obQZf60fHkf9CGxSBHJEw27dG1PJCgKT75YGfoCZ7tg=="],
761
761
 
762
762
  "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
763
763
 
@@ -881,17 +881,11 @@
881
881
 
882
882
  "@pdf-lib/upng/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
883
883
 
884
- "@remotion/media/mediabunny": ["mediabunny@1.50.3", "", { "dependencies": { "@types/dom-mediacapture-transform": "^0.1.11", "@types/dom-webcodecs": "0.1.13" } }, "sha512-lw6NjC6++/p6MzHC3u7RbkpM0dnukEQcAAXzZwXlmmx9y8n6132DXUMer2WaNn8RPuGgUpnemi/QIVNpEnONhQ=="],
884
+ "@remotion/web-renderer/@mediabunny/aac-encoder": ["@mediabunny/aac-encoder@1.50.8", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-A5Se/LZd6RmYq/h36lBMSEsHvsyW8d0toR7FrAwpsFYbK+DVQYf90KiBT1Aw/mzLXx8/ypIOORJnd1sVZqOvJQ=="],
885
885
 
886
- "@remotion/media/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
886
+ "@remotion/web-renderer/@mediabunny/flac-encoder": ["@mediabunny/flac-encoder@1.50.8", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-4cfN03SbEoQaG+eBeYFUAb1R1ALaAHzf43dXFzQTr/oO5ueqzTgBoG3coKrIvBaAMU7Vv36mrBKLX8bvTppdAQ=="],
887
887
 
888
- "@remotion/web-renderer/@mediabunny/aac-encoder": ["@mediabunny/aac-encoder@1.50.3", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-rVPnhJbn+7BUsu09sEl/bee/DkzmAc33oErJ250RPlCqrPndx61DSb2GLvVmmSx6L2LU2WyCnvJIMUrGPJEXPA=="],
889
-
890
- "@remotion/web-renderer/@mediabunny/flac-encoder": ["@mediabunny/flac-encoder@1.50.3", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-aIEHE3eQ+hG35rRvtNNYO6ufTiGkZpzgOBjcqK8CQuRAMtUY9/DzV12jj52L0nSj2zmBKXQQ6dWBWsusigV/dQ=="],
891
-
892
- "@remotion/web-renderer/@mediabunny/mp3-encoder": ["@mediabunny/mp3-encoder@1.50.3", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-dyjWOhG27+ctlejfjY4vFy0g8wzo0NCRTNQ/GNsCaHxWkduaq0GI2BWKXm6UFCsfnUKgtDWMEu4zOPl9fKpbsA=="],
893
-
894
- "@remotion/web-renderer/mediabunny": ["mediabunny@1.50.3", "", { "dependencies": { "@types/dom-mediacapture-transform": "^0.1.11", "@types/dom-webcodecs": "0.1.13" } }, "sha512-lw6NjC6++/p6MzHC3u7RbkpM0dnukEQcAAXzZwXlmmx9y8n6132DXUMer2WaNn8RPuGgUpnemi/QIVNpEnONhQ=="],
888
+ "@remotion/web-renderer/@mediabunny/mp3-encoder": ["@mediabunny/mp3-encoder@1.50.8", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-eBT/H30tTu8AmZXqZ5RTpJ9VmwVlwednajuOrrWp0GS6cbGXsGMQ60Qvr3ZMIE0QDiadlEzb8/ozR+doP+MC1g=="],
895
889
 
896
890
  "body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
897
891
 
@@ -1641,7 +1641,21 @@ const CLIP_TAGS = new Set(['Sequence', 'Series.Sequence', 'TransitionSeries.Sequ
1641
1641
  /** Transition tags (occupy a slot between clips inside a TransitionSeries). */
1642
1642
  const TRANSITION_TAGS = new Set(['Series.Transition', 'TransitionSeries.Transition']);
1643
1643
  /** Media tags that carry a `src` (the replace-media + drop targets). */
1644
- const MEDIA_TAGS = new Set(['Video', 'OffthreadVideo', 'Audio', 'Img', 'Image']);
1644
+ // `Maude*` spellings are the collision-avoidance ALIASES clip-ops emits when a
1645
+ // canvas already declares a component of that name (e.g. `export default
1646
+ // function Video()`). They are the same elements and must tokenize the same,
1647
+ // or the Timeline silently loses the clip's media (RCA
1648
+ // issue-mp4-audio-export-html5audio-silent-degrade follow-up).
1649
+ const MEDIA_TAGS = new Set([
1650
+ 'Video',
1651
+ 'OffthreadVideo',
1652
+ 'Audio',
1653
+ 'Img',
1654
+ 'Image',
1655
+ 'MaudeVideo',
1656
+ 'MaudeAudio',
1657
+ 'MaudeImg',
1658
+ ]);
1645
1659
  /** Remotion/layout primitives that are structural, not their own timeline layer. */
1646
1660
  const LAYER_SKIP_TAGS = new Set([
1647
1661
  'AbsoluteFill',
@@ -4534,7 +4548,7 @@ export function assembleCompSource(
4534
4548
  const dur = Math.max(1, Math.round(c.durationInFrames ?? defDur));
4535
4549
  seqLines.push(
4536
4550
  ` <TransitionSeries.Sequence name="clip-${i + 1}" durationInFrames={${dur}}>`,
4537
- ` <OffthreadVideo src="${escapeAttr(c.src)}" />`,
4551
+ ` <Video src="${escapeAttr(c.src)}" />`,
4538
4552
  ` </TransitionSeries.Sequence>`
4539
4553
  );
4540
4554
  cursor += dur;
@@ -4557,16 +4571,26 @@ export function assembleCompSource(
4557
4571
  // the cursor (with the historical defDur floor for degenerate inputs).
4558
4572
  const total = videos.length || audios.length ? Math.max(cursor, defDur) : fps * 5;
4559
4573
 
4574
+ // Media elements come from @remotion/media, NEVER from 'remotion'. The export's
4575
+ // only audio-capable path (renderMediaOnWeb) rejects `remotion`'s `Audio`
4576
+ // (= Html5Audio) and `OffthreadVideo` outright, so a reel assembled with those
4577
+ // exports MUTED — and ~40x slower, on the frame-step fallback. Every reel this
4578
+ // function emits has a music bed by default, which made it the widest carrier
4579
+ // of the bug. RCA issue-mp4-audio-export-html5audio-silent-degrade.
4560
4580
  const remotionImports = ['AbsoluteFill'];
4561
- if (videos.length) remotionImports.push('OffthreadVideo');
4562
- if (audios.length) remotionImports.push('Audio');
4581
+ const mediaImports: string[] = [];
4582
+ if (videos.length) mediaImports.push('Video');
4583
+ if (audios.length) mediaImports.push('Audio');
4563
4584
  const transitionsImport = videos.length
4564
4585
  ? `import { TransitionSeries } from '@remotion/transitions';\n`
4565
4586
  : '';
4587
+ const mediaImport = mediaImports.length
4588
+ ? `import { ${mediaImports.join(', ')} } from '@remotion/media';\n`
4589
+ : '';
4566
4590
 
4567
4591
  return [
4568
4592
  `import { DesignCanvas, DCSection, DCArtboard, VideoComp } from '@maude/canvas-lib';`,
4569
- `${transitionsImport}import { ${remotionImports.join(', ')} } from 'remotion';`,
4593
+ `${transitionsImport}${mediaImport}import { ${remotionImports.join(', ')} } from 'remotion';`,
4570
4594
  ``,
4571
4595
  `const Comp = () => (`,
4572
4596
  ` <AbsoluteFill style={{ background: 'var(--bg-0)' }}>`,