@hyperframes/lint 0.8.144 → 0.8.146

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/dist/browser.js CHANGED
@@ -1077,7 +1077,7 @@ function describeRootDimensionsDrift(source, dataWidth, dataHeight) {
1077
1077
  const declared = `Root composition declares data-width="${dataWidth}" data-height="${dataHeight}"`;
1078
1078
  if (!bodyCssMismatch) {
1079
1079
  return {
1080
- message: `${declared}, but ${viewportMismatch}. The viewport meta has no effect on capture \u2014 the renderer sizes the viewport from the root's own data-width/data-height \u2014 so this is stale metadata, not a clipping risk.`,
1080
+ message: `${declared}, but ${viewportMismatch}. The viewport meta has no effect on capture (the renderer sizes the viewport from the root's own data-width/data-height), so this is stale metadata, not a clipping risk.`,
1081
1081
  fixHint: "update the meta viewport to match, or scaffold with `hyperframes init --resolution portrait`"
1082
1082
  };
1083
1083
  }
@@ -1349,7 +1349,7 @@ var coreRules = [
1349
1349
  findings.push({
1350
1350
  code: "unclosed_tag_swallowed_element",
1351
1351
  severity: "error",
1352
- message: `<${tag.name}> is missing its closing \`>\` before the next \`<\` \u2014 the following element is swallowed as bogus attribute text and never becomes a real node.`,
1352
+ message: `<${tag.name}> is missing its closing \`>\` before the next \`<\`, so the following element is swallowed as bogus attribute text and never becomes a real node.`,
1353
1353
  fixHint: "Close the previous tag's `>` before opening the next element.",
1354
1354
  snippet: truncateSnippet(tag.raw)
1355
1355
  });
@@ -2064,7 +2064,7 @@ var mediaRules = [
2064
2064
  severity: "error",
2065
2065
  message: `<video${elementId ? ` id="${elementId}"` : ""}> has data-start but declares neither muted nor data-has-audio. If the file has sound, add data-has-audio="true" (the sound stays on this clip). If it is silent, add muted.`,
2066
2066
  elementId,
2067
- fixHint: 'Add data-has-audio="true" when the file has sound (recommended \u2014 keeps picture and sound on one clip), or add `muted` for silent footage.',
2067
+ fixHint: 'Add data-has-audio="true" when the file has sound (recommended: it keeps picture and sound on one clip), or add `muted` for silent footage.',
2068
2068
  snippet: truncateSnippet(tag.raw)
2069
2069
  });
2070
2070
  }
@@ -2149,7 +2149,7 @@ var mediaRules = [
2149
2149
  severity: "error",
2150
2150
  message: `Self-closing <${tagName}/> is invalid HTML. The browser will leave the tag open, swallowing all subsequent elements as invisible fallback content. This makes compositions INVISIBLE.`,
2151
2151
  elementId,
2152
- fixHint: `Change <${tagName} .../> to <${tagName} ...></${tagName}> \u2014 media elements MUST have explicit closing tags.`,
2152
+ fixHint: `Change <${tagName} .../> to <${tagName} ...></${tagName}>. Media elements MUST have explicit closing tags.`,
2153
2153
  snippet: truncateSnippet(scMatch[0])
2154
2154
  });
2155
2155
  }
@@ -2192,7 +2192,7 @@ var mediaRules = [
2192
2192
  findings.push({
2193
2193
  code: "base64_media_prohibited",
2194
2194
  severity: "error",
2195
- message: `Inline base64 audio/video detected (${(dataSize / 1024).toFixed(0)} KB)${isSuspicious ? " \u2014 likely fabricated data" : ""}. Base64 media is prohibited \u2014 it bloats file size and breaks rendering.`,
2195
+ message: `Inline base64 audio/video detected (${(dataSize / 1024).toFixed(0)} KB)${isSuspicious ? ", likely fabricated data" : ""}. Base64 media is prohibited: it bloats file size and breaks rendering.`,
2196
2196
  fixHint: "Use a relative path (assets/music.mp3) or HTTPS URL for the audio/video src. Never embed media as base64.",
2197
2197
  snippet: truncateSnippet((b64Match[1] ?? "").slice(0, 80) + "...")
2198
2198
  });
@@ -2221,7 +2221,7 @@ var mediaRules = [
2221
2221
  findings.push({
2222
2222
  code: "media_missing_id",
2223
2223
  severity: "error",
2224
- message: `<${tag.name}> has data-start but no id attribute. The renderer requires id to discover media elements \u2014 this ${tag.name === "audio" ? "audio will be SILENT" : "video will be FROZEN"} in renders.`,
2224
+ message: `<${tag.name}> has data-start but no id attribute. The renderer requires id to discover media elements, so this ${tag.name === "audio" ? "audio will be SILENT" : "video will be FROZEN"} in renders.`,
2225
2225
  fixHint: `Add a unique id attribute: <${tag.name} id="my-${tag.name}" ...>`,
2226
2226
  snippet: truncateSnippet(tag.raw)
2227
2227
  });
@@ -2278,7 +2278,7 @@ var mediaRules = [
2278
2278
  findings.push({
2279
2279
  code: "media_crossorigin_breaks_preview",
2280
2280
  severity: "error",
2281
- message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> has crossorigin, which forces a CORS-checked fetch. If the media host omits Access-Control-Allow-Origin, the load silently fails in Studio preview (media shows BLANK/black) while server-side renders still work \u2014 hiding the bug.`,
2281
+ message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> has crossorigin, which forces a CORS-checked fetch. If the media host omits Access-Control-Allow-Origin, the load silently fails in Studio preview (media shows BLANK/black) while server-side renders still work, which hides the bug.`,
2282
2282
  elementId,
2283
2283
  fixHint: "Remove the crossorigin attribute unless you read the media back via canvas/WebGL/WebAudio AND the host is known to send CORS headers. Plain displayed media never needs it.",
2284
2284
  snippet: truncateSnippet(tag.raw)
@@ -2312,7 +2312,7 @@ var mediaRules = [
2312
2312
  findings.push({
2313
2313
  code: "video_audio_double_source",
2314
2314
  severity: "error",
2315
- message: `<audio${audioInfo.id ? ` id="${audioInfo.id}"` : ""}> and <video${videoInfo.id ? ` id="${videoInfo.id}"` : ""}> both point to the same source at the same time. The unmuted video already provides audio \u2014 the duplicate <audio> will cause double playback and echo.`,
2315
+ message: `<audio${audioInfo.id ? ` id="${audioInfo.id}"` : ""}> and <video${videoInfo.id ? ` id="${videoInfo.id}"` : ""}> both point to the same source at the same time. The unmuted video already provides audio, so the duplicate <audio> will cause double playback and echo.`,
2316
2316
  elementId: audioInfo.id,
2317
2317
  fixHint: "Remove the <audio> element and let the video carry its own sound (recommended), or mute the video (add `muted`) and keep the separate <audio> when picture and sound must be cut independently.",
2318
2318
  snippet: truncateSnippet(audioInfo.raw)
@@ -2348,7 +2348,7 @@ function findVolumeTweenOverridesGainFindings(ctx) {
2348
2348
  findings.push({
2349
2349
  code: "audio_volume_tween_overrides_gain",
2350
2350
  severity: "warning",
2351
- message: `#${id} has data-volume="${volume}" (${db}) and a GSAP tween on \`volume\`. Tween values are absolute \u2014 they REPLACE this gain rather than scale it \u2014 so wherever the tween names a value the clip plays at that value, not at ${db}.`,
2351
+ message: `#${id} has data-volume="${volume}" (${db}) and a GSAP tween on \`volume\`. Tween values are absolute: they REPLACE this gain rather than scale it, so wherever the tween names a value the clip plays at that value, not at ${db}.`,
2352
2352
  elementId: id,
2353
2353
  fixHint: "Write the tween's targets in the same absolute gain (e.g. `volume: 1.95`, not `volume: 1`), or reset data-volume to 1 and let the tween carry the level on its own.",
2354
2354
  snippet: truncateSnippet(tag.raw)
@@ -2367,7 +2367,7 @@ function findVolumeDoubleAutomationFindings(ctx) {
2367
2367
  findings.push({
2368
2368
  code: "audio_volume_double_automation",
2369
2369
  severity: "warning",
2370
- message: `#${id} has both a volume automation lane and a GSAP tween on \`volume\`. The lane wins \u2014 the tween is ignored in preview and in the render.`,
2370
+ message: `#${id} has both a volume automation lane and a GSAP tween on \`volume\`. The lane wins, and the tween is ignored in preview and in the render.`,
2371
2371
  elementId: id,
2372
2372
  fixHint: "Keep one of them: delete the volume lane to go back to tweening, or drop the tween and shape the level in the automation lane.",
2373
2373
  snippet: truncateSnippet(tag.raw)
@@ -2415,7 +2415,7 @@ function findCarveUngroupedSourcesFindings(ctx) {
2415
2415
  severity: "warning",
2416
2416
  message: `${elementId ? `#${elementId}'s` : "This"} carve names ${clipIds.length} voice clips directly (${clipIds.join(", ")}) instead of a group.`,
2417
2417
  elementId,
2418
- fixHint: "Group the voice clips and carve against the group \u2014 a hand-rolled clip list silently rots when a clip is added.",
2418
+ fixHint: "Group the voice clips and carve against the group. A hand-rolled clip list silently rots when a clip is added.",
2419
2419
  snippet: truncateSnippet(tag.raw)
2420
2420
  });
2421
2421
  }
@@ -2464,7 +2464,7 @@ function findAudioGroupTimingAttrFindings(ctx) {
2464
2464
  findings.push({
2465
2465
  code: "audio_group_timing_attrs",
2466
2466
  severity: "warning",
2467
- message: `${elementId ? `#${elementId}` : "This audio group"} carries ${present.map((attr) => `\`${attr}\``).join(", ")}, which a bus has no use for \u2014 its members carry the timing and its automation clock is composition time.`,
2467
+ message: `${elementId ? `#${elementId}` : "This audio group"} carries ${present.map((attr) => `\`${attr}\``).join(", ")}, which a bus has no use for: its members carry the timing and its automation clock is composition time.`,
2468
2468
  elementId,
2469
2469
  fixHint: `Remove ${present.map((attr) => `\`${attr}\``).join(", ")} from the group element.`,
2470
2470
  snippet: truncateSnippet(tag.raw)
@@ -2481,7 +2481,7 @@ function findAudioGroupCarveAttrFindings(ctx) {
2481
2481
  findings.push({
2482
2482
  code: "audio_group_carve_attr",
2483
2483
  severity: "warning",
2484
- message: `${elementId ? `#${elementId}` : "This audio group"} carries \`data-fx-carve\`, which belongs on the clip being carved \u2014 a bus has no audio of its own to level-match against, and a carve here stacks with any its members already have.`,
2484
+ message: `${elementId ? `#${elementId}` : "This audio group"} carries \`data-fx-carve\`, which belongs on the clip being carved. A bus has no audio of its own to level-match against, and a carve here stacks with any its members already have.`,
2485
2485
  elementId,
2486
2486
  fixHint: "Remove `data-fx-carve` and the `fromCarve` nodes it wrote into this bus's `data-fx-chain`, and carve the bed clip instead.",
2487
2487
  snippet: truncateSnippet(tag.raw)
@@ -3159,7 +3159,7 @@ var compositionRules = [
3159
3159
  findings.push({
3160
3160
  code: "root_composition_missing_html_wrapper",
3161
3161
  severity: "error",
3162
- message: "Composition starts with a bare element instead of a proper HTML document. An index.html that contains data-composition-id but no <!DOCTYPE html>, <html>, or <body> is a fragment \u2014 browsers quirks-mode it, the preview server cannot load it, and the bundler will fail to inject runtime scripts.",
3162
+ message: "Composition starts with a bare element instead of a proper HTML document. An index.html that contains data-composition-id but no <!DOCTYPE html>, <html>, or <body> is a fragment: browsers quirks-mode it, the preview server cannot load it, and the bundler will fail to inject runtime scripts.",
3163
3163
  fixHint: 'Wrap the composition in <!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>...</body></html>.',
3164
3164
  snippet: rootTag ? truncateSnippet(rootTag.raw) : void 0
3165
3165
  });
@@ -3314,7 +3314,7 @@ var compositionRules = [
3314
3314
  findings.push({
3315
3315
  code: "unknown_variable_binding",
3316
3316
  severity: "warning",
3317
- message: `<${tag.name}> binds ${attr}="${id}" but no variable "${id}" is declared in data-composition-variables \u2014 the binding will silently keep the authored fallback.`,
3317
+ message: `<${tag.name}> binds ${attr}="${id}" but no variable "${id}" is declared in data-composition-variables, so the binding will silently keep the authored fallback.`,
3318
3318
  fixHint: `Declare the variable on the composition root (<html>, or the [data-composition-id] root element for a template/fragment comp): data-composition-variables='[{"id":"${id}","type":"${attr === "data-var-src" ? "image" : "string"}","label":"${id}","default":"..."}]', or fix the binding id.`,
3319
3319
  elementId: readAttr(tag.raw, "id") || void 0,
3320
3320
  snippet: truncateSnippet(tag.raw)
@@ -3355,8 +3355,8 @@ var compositionRules = [
3355
3355
  {
3356
3356
  code: "html_dir_attribute_breaks_render",
3357
3357
  severity: "error",
3358
- message: `<html dir="${dir}"> renders correctly in preview/snapshot but produces a fully blank/black video from render \u2014 a confirmed, silent failure.`,
3359
- fixHint: `Remove dir="${dir}" from <html>. Keep lang, and scope ${scopedDirection} to individual text-containing elements instead \u2014 text still shapes correctly via the browser's own bidi algorithm.`,
3358
+ message: `<html dir="${dir}"> renders correctly in preview/snapshot but produces a fully blank/black video from render. This is a confirmed, silent failure.`,
3359
+ fixHint: `Remove dir="${dir}" from <html>. Keep lang, and scope ${scopedDirection} to individual text-containing elements instead. Text still shapes correctly via the browser's own bidi algorithm.`,
3360
3360
  snippet: truncateSnippet(htmlTag.raw)
3361
3361
  }
3362
3362
  ];
@@ -3394,7 +3394,7 @@ var compositionRules = [
3394
3394
  findings.push({
3395
3395
  code: "subcomposition_blanks_before_host",
3396
3396
  severity: "warning",
3397
- message: `<${t.tag.name}${elementId ? ` id="${elementId}"` : ""}> sub-composition ends at ${round3(t.end)}s but the composition runs to ${round3(rootDuration)}s \u2014 its slot will be blank for ~${gap}s.`,
3397
+ message: `<${t.tag.name}${elementId ? ` id="${elementId}"` : ""}> sub-composition ends at ${round3(t.end)}s but the composition runs to ${round3(rootDuration)}s, so its slot will be blank for ~${gap}s.`,
3398
3398
  elementId,
3399
3399
  fixHint: `data-duration is the slot's visible window. Set this sub-composition's data-duration to ${round3(rootDuration - t.start)} to fill the host window, or add another clip to cover the remaining ~${gap}s.`,
3400
3400
  snippet: truncateSnippet(t.tag.raw)
@@ -3433,7 +3433,7 @@ var compositionRules = [
3433
3433
  severity: "error",
3434
3434
  message: `Root element has class="${rootClasses.join(" ")}" and is styled by ${offenders.length} rule(s) keyed off that class (e.g. ${example}). At render, every sub-composition rule is scoped to [data-composition-id="${rootCompositionId}"] <selector>, so a selector whose leftmost part is the ROOT's own class becomes a descendant selector that cannot match the scoped element itself. Since #1886 the producer preserves the authored root as an inner wrapper, so this no longer renders the scene unstyled, but #root is the shape the scoper special-cases and the registry blocks model. Use it so preview, render, and Studio agree.`,
3435
3435
  selector: example,
3436
- fixHint: `Give the root id="root" and style it with \`#root { ... }\` plus plain descendant selectors (\`.kicker\`, \`#hero\`) \u2014 the runtime already scopes each sub-composition by data-composition-id, so a class namespace on the root is redundant.`,
3436
+ fixHint: `Give the root id="root" and style it with \`#root { ... }\` plus plain descendant selectors (\`.kicker\`, \`#hero\`). The runtime already scopes each sub-composition by data-composition-id, so a class namespace on the root is redundant.`,
3437
3437
  snippet: truncateSnippet(rootTag.raw)
3438
3438
  }
3439
3439
  ];
@@ -3516,7 +3516,7 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
3516
3516
  {
3517
3517
  code: "root_composition_missing_duration_source",
3518
3518
  severity: "error",
3519
- message: `Root composition uses Three.js with no data-duration. The runtime cannot discover a Three.js scene's duration automatically (no AnimationClip/AnimationMixer inspection) \u2014 render will fail with "Composition has zero duration".`,
3519
+ message: `Root composition uses Three.js with no data-duration. The runtime cannot discover a Three.js scene's duration automatically (no AnimationClip/AnimationMixer inspection), so render will fail with "Composition has zero duration".`,
3520
3520
  fixHint: 'Add data-duration="<seconds>" to the root element.',
3521
3521
  snippet: truncateSnippet(rootTag.raw)
3522
3522
  }
@@ -3579,8 +3579,8 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
3579
3579
  {
3580
3580
  code: "composition_heavy_overlay_count_high",
3581
3581
  severity: "warning",
3582
- message: `This composition has ${heavyCount} elements carrying "heavy overlay" CSS (filter:blur, radial-gradient, or clip-path). Field signal: a composition with ~40 such elements \u2014 including opacity:0 / visibility:hidden ones \u2014 captures solid-black for the first ~half of the render, recovering near the end. Reproduces identically via drawElement, forced screenshot capture, and snapshot, so the capture layer itself is the offender (not encoder/mux). Independent of duration. Presence alone matters; only display:none elements are excluded here.`,
3583
- fixHint: `${splitTarget} and concat the pieces (FFmpeg or the runtime's slideshow) so each capture only sees a small subset of heavy overlays at once. Even hidden overlays (opacity:0 / visibility:hidden) contribute \u2014 either remove truly unused ones from the source or scope them into their own per-transition sub-composition. If an overlay is genuinely inert for the whole clip, use display:none so it never enters the render tree. Field ref ts=1784040753 (#hyperframes-cli-feedback).`
3582
+ message: `This composition has ${heavyCount} elements carrying "heavy overlay" CSS (filter:blur, radial-gradient, or clip-path). Field signal: a composition with ~40 such elements (including opacity:0 / visibility:hidden ones) captures solid-black for the first ~half of the render, recovering near the end. Reproduces identically via drawElement, forced screenshot capture, and snapshot, so the capture layer itself is the offender (not encoder/mux). Independent of duration. Presence alone matters; only display:none elements are excluded here.`,
3583
+ fixHint: `${splitTarget} and concat the pieces (FFmpeg or the runtime's slideshow) so each capture only sees a small subset of heavy overlays at once. Even hidden overlays (opacity:0 / visibility:hidden) contribute: either remove truly unused ones from the source or scope them into their own per-transition sub-composition. If an overlay is genuinely inert for the whole clip, use display:none so it never enters the render tree. Field ref ts=1784040753 (#hyperframes-cli-feedback).`
3584
3584
  }
3585
3585
  ];
3586
3586
  },
@@ -3623,8 +3623,8 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
3623
3623
  {
3624
3624
  code: "root_zoom_rescales_a_fixed_canvas",
3625
3625
  severity: "error",
3626
- message: `\`zoom: ${hit.value}\` on ${hit.where} rescales the painted composition inside a frame that does not rescale with it. The capture frame is sized from the root's data-width/data-height in LAYOUT pixels, and \`zoom\` changes only what is painted inside it, so ` + (enlarging ? `content past the frame's edge renders zero pixels \u2014 it is inside the declared composition and absent from the output, with nothing else reporting it.` : `the composition paints into part of the frame and the remainder of every output frame is dead space.`),
3627
- fixHint: `Author the composition at its real size \u2014 set data-width/data-height (and the root's width/height) to the dimensions you want \u2014 and drop the canvas-level \`zoom\`. To scale the OUTPUT without changing layout, pass \`--output-resolution\` to render, which supersamples. \`zoom\` on descendants is fine and is not flagged: it is honoured exactly as specified.`,
3626
+ message: `\`zoom: ${hit.value}\` on ${hit.where} rescales the painted composition inside a frame that does not rescale with it. The capture frame is sized from the root's data-width/data-height in LAYOUT pixels, and \`zoom\` changes only what is painted inside it, so ` + (enlarging ? `content past the frame's edge renders zero pixels. It is inside the declared composition and absent from the output, with nothing else reporting it.` : `the composition paints into part of the frame and the remainder of every output frame is dead space.`),
3627
+ fixHint: `Author the composition at its real size: set data-width/data-height (and the root's width/height) to the dimensions you want, and drop the canvas-level \`zoom\`. To scale the OUTPUT without changing layout, pass \`--output-resolution\` to render, which supersamples. \`zoom\` on descendants is fine and is not flagged: it is honoured exactly as specified.`,
3628
3628
  snippet: hit.snippet
3629
3629
  }
3630
3630
  ];
@@ -3668,8 +3668,8 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
3668
3668
  {
3669
3669
  code: "composition_exceeds_inspection_viewport_cap",
3670
3670
  severity: "warning",
3671
- message: `${over.join(" and ")} exceeds the ${INSPECTION_VIEWPORT_CAP}px viewport cap that check, validate, snapshot, compare and layout clamp to. render is unaffected and produces the full size, so the video is correct \u2014 but every inspection command captures a ${INSPECTION_VIEWPORT_CAP}px-wide frame, and anything beyond that is missing from what they report. Measured at 0.8.71: a 5000x400 composition renders 5000x400 with all content, while snapshot returns 4096x400 with the element at left:4500 absent.`,
3672
- fixHint: `Keep the authored size if the output needs it, and verify the region past ${INSPECTION_VIEWPORT_CAP}px from a render rather than from check/snapshot/compare \u2014 a clean inspection result does not cover it. If the large canvas is only there to gain resolution, author at the layout size and pass \`--output-resolution\` to render instead, which supersamples without changing layout.`,
3671
+ message: `${over.join(" and ")} exceeds the ${INSPECTION_VIEWPORT_CAP}px viewport cap that check, validate, snapshot, compare and layout clamp to. render is unaffected and produces the full size, so the video is correct, but every inspection command captures a ${INSPECTION_VIEWPORT_CAP}px-wide frame, and anything beyond that is missing from what they report. Measured at 0.8.71: a 5000x400 composition renders 5000x400 with all content, while snapshot returns 4096x400 with the element at left:4500 absent.`,
3672
+ fixHint: `Keep the authored size if the output needs it, and verify the region past ${INSPECTION_VIEWPORT_CAP}px from a render rather than from check/snapshot/compare. A clean inspection result does not cover it. If the large canvas is only there to gain resolution, author at the layout size and pass \`--output-resolution\` to render instead, which supersamples without changing layout.`,
3673
3673
  snippet: truncateSnippet(rootTag.raw)
3674
3674
  }
3675
3675
  ];
@@ -4548,7 +4548,7 @@ ${right.raw}`)
4548
4548
  findings.push({
4549
4549
  code: "gsap_animates_clip_element",
4550
4550
  severity: "error",
4551
- message: `GSAP animation sets ${conflictingProps.join(", ")} on a clip element. Selector "${sel}" resolves to element ${elDesc}. The framework manages clip visibility, and autoAlpha writes visibility as well as opacity \u2014 do not animate these properties on clip elements.`,
4551
+ message: `GSAP animation sets ${conflictingProps.join(", ")} on a clip element. Selector "${sel}" resolves to element ${elDesc}. The framework manages clip visibility, and autoAlpha writes visibility as well as opacity. Do not animate these properties on clip elements.`,
4552
4552
  selector: sel,
4553
4553
  elementId: clipInfo.id || void 0,
4554
4554
  fixHint: "Remove the visibility/display/autoAlpha tween, or move the content into a child <div> and target that instead.",
@@ -4742,7 +4742,7 @@ ${right.raw}`)
4742
4742
  findings.push({
4743
4743
  code: "audio_reactive_single_tween_per_group",
4744
4744
  severity: "warning",
4745
- message: "Audio-reactive captions use a single tween per group based on peak values. This sets one static value at group.start \u2014 not perceptible as audio reactivity.",
4745
+ message: "Audio-reactive captions use a single tween per group based on peak values. This sets one static value at group.start, which is not perceptible as audio reactivity.",
4746
4746
  fixHint: "Sample audio data at 100-200ms intervals throughout each group's lifetime (for loop from group.start to group.end) and create a tween at each sample point for visible pulsing."
4747
4747
  });
4748
4748
  }
@@ -4827,7 +4827,7 @@ ${right.raw}`)
4827
4827
  ...locate(script, gsapCallOffset(script, "timeline")),
4828
4828
  code: "gsap_timeline_not_registered",
4829
4829
  severity: "error",
4830
- message: "GSAP timeline is created but never registered in window.__timelines. The runtime discovers timelines from this registry \u2014 without registration, animations will not play during preview or render.",
4830
+ message: "GSAP timeline is created but never registered in window.__timelines. The runtime discovers timelines from this registry. Without registration, animations will not play during preview or render.",
4831
4831
  fixHint: 'Add `window.__timelines = window.__timelines || {};` and `window.__timelines["root"] = tl;` after creating the timeline (use the composition\'s data-composition-id as the key).'
4832
4832
  });
4833
4833
  }
@@ -4895,9 +4895,9 @@ ${right.raw}`)
4895
4895
  findings.push({
4896
4896
  code: "gsap_from_opacity_noop",
4897
4897
  severity: "error",
4898
- message: `"${sel}" has CSS \`opacity: 0\` and a gsap.${win.method}() that also sets opacity to 0. gsap.from() animates FROM the specified value TO the current CSS value \u2014 since CSS is already 0, the element animates from 0\u21920 and never becomes visible.`,
4898
+ message: `"${sel}" has CSS \`opacity: 0\` and a gsap.${win.method}() that also sets opacity to 0. gsap.from() animates FROM the specified value TO the current CSS value. Since CSS is already 0, the element animates from 0\u21920 and never becomes visible.`,
4899
4899
  selector: sel,
4900
- fixHint: `Remove \`opacity: 0\` from the CSS/inline style on "${sel}". Let gsap.from({opacity: 0}) handle the initial hidden state \u2014 it will animate FROM 0 TO the CSS value (1 by default).`,
4900
+ fixHint: `Remove \`opacity: 0\` from the CSS/inline style on "${sel}". Let gsap.from({opacity: 0}) handle the initial hidden state. It will animate FROM 0 TO the CSS value (1 by default).`,
4901
4901
  snippet: truncateSnippet(win.raw)
4902
4902
  });
4903
4903
  }
@@ -4976,12 +4976,12 @@ ${right.raw}`)
4976
4976
  if (layoutProps.length > 0 && allTargetsHtmlInCanvas(call.selector)) layoutProps = [];
4977
4977
  if (layoutProps.length === 0 && reflowProps.length === 0 && !usesRoundProps) continue;
4978
4978
  const flagged = [...layoutProps, ...reflowProps, ...usesRoundProps ? ["roundProps"] : []];
4979
- const message = `GSAP tween on "${call.selector}" uses motion that snaps to integer device pixels: ${flagged.join(", ")}. Layout and text-reflow properties snap during browser layout; roundProps rounds the tween value. Slow motion or an ease-out tail then stutters under the seek-by-frame capture engine \u2014 animate transforms (x/y/scale/opacity) instead.`;
4979
+ const message = `GSAP tween on "${call.selector}" uses motion that snaps to integer device pixels: ${flagged.join(", ")}. Layout and text-reflow properties snap during browser layout; roundProps rounds the tween value. Slow motion or an ease-out tail then stutters under the seek-by-frame capture engine. Animate transforms (x/y/scale/opacity) instead.`;
4980
4980
  const fixes = [];
4981
4981
  if (layoutProps.length > 0) {
4982
4982
  const tokens = [...new Set(layoutProps.flatMap((p) => LAYOUT_FIX[p] ?? []))];
4983
4983
  fixes.push(
4984
- `replace ${layoutProps.join("/")} with the transform equivalent (${tokens.join(", ")}) \u2014 e.g. tl.fromTo("${call.selector}", { x: -1300 }, { x: 0, ...yourAnimation })`
4984
+ `replace ${layoutProps.join("/")} with the transform equivalent (${tokens.join(", ")}), e.g. tl.fromTo("${call.selector}", { x: -1300 }, { x: 0, ...yourAnimation })`
4985
4985
  );
4986
4986
  }
4987
4987
  if (reflowProps.length > 0) {
@@ -4993,7 +4993,7 @@ ${right.raw}`)
4993
4993
  }
4994
4994
  if (spacing.length > 0) {
4995
4995
  parts.push(
4996
- `for ${spacing.join("/")}, split the text into per-character elements and animate each glyph's x (the spread) \u2014 uniform scale is NOT equivalent \u2014 or hold the final value statically`
4996
+ `for ${spacing.join("/")}, split the text into per-character elements and animate each glyph's x (the spread), since uniform scale is NOT equivalent, or hold the final value statically`
4997
4997
  );
4998
4998
  }
4999
4999
  fixes.push(
@@ -5057,7 +5057,7 @@ ${right.raw}`)
5057
5057
  findings.push({
5058
5058
  code: "gsap_relative_value_second_writer",
5059
5059
  severity: "error",
5060
- message: `Relative value(s) ${values} on "${win.targetSelector}" start while another writer for the same propert${sharedProps.length > 1 ? "ies" : "y"} is active between ${formatTime(win.position)} and ${formatTime(overlapEnd)}. Relative tweens capture their base at tween init: the sequential path inits mid-flight of the other writer, a cold render worker landing later inits with its end state \u2014 the same frame renders at two different positions (snap at chunk boundaries).`,
5060
+ message: `Relative value(s) ${values} on "${win.targetSelector}" start while another writer for the same propert${sharedProps.length > 1 ? "ies" : "y"} is active between ${formatTime(win.position)} and ${formatTime(overlapEnd)}. Relative tweens capture their base at tween init: the sequential path inits mid-flight of the other writer, a cold render worker landing later inits with its end state, so the same frame renders at two different positions (snap at chunk boundaries).`,
5061
5061
  selector: win.targetSelector,
5062
5062
  fixHint: `Use absolute values for ${sharedProps.join(", ")}, or a fromTo() with explicit endpoints, so every seek path resolves the same state. Single-writer relative values are safe; the conflict is the second writer.`,
5063
5063
  snippet: truncateSnippet(`${win.raw}
@@ -5126,13 +5126,13 @@ ${other.raw}`)
5126
5126
  const readsInvariant = TRANSFORM_INVARIANT_READ.test(fn.body);
5127
5127
  const badMember = firstParamMemberAccessHazard(fn);
5128
5128
  if (!readsSensitive && !readsInvariant && !badMember) continue;
5129
- const reason = readsSensitive ? "reads transform-sensitive geometry, so its result depends on the worker's own seek order" : badMember ? `accesses .${badMember} on its first parameter \u2014 GSAP function values receive (index, target, targets), so the first parameter is a NUMBER and this throws at tween init` : "measures layout at tween init, which is deterministic across cold render workers only while the measured layout never animates";
5129
+ const reason = readsSensitive ? "reads transform-sensitive geometry, so its result depends on the worker's own seek order" : badMember ? `accesses .${badMember} on its first parameter. GSAP function values receive (index, target, targets), so the first parameter is a NUMBER and this throws at tween init` : "measures layout at tween init, which is deterministic across cold render workers only while the measured layout never animates";
5130
5130
  findings.push({
5131
5131
  code: "gsap_function_value_hazard",
5132
5132
  severity: readsSensitive || badMember ? "error" : "warning",
5133
5133
  message: `Function-valued tween var for ${prop} on "${anim.targetSelector}" ${reason}. Each render worker initializes tweens independently.`,
5134
5134
  selector: anim.targetSelector,
5135
- fixHint: badMember ? "Use the SECOND parameter for the element: (index, target) => ... \u2014 or index arithmetic like (i) => i * 20." : "Compute the value once at build time (before the timeline is registered) and pass a constant, or derive it from fixed composition coordinates.",
5135
+ fixHint: badMember ? "Use the SECOND parameter for the element: (index, target) => ..., or index arithmetic like (i) => i * 20." : "Compute the value once at build time (before the timeline is registered) and pass a constant, or derive it from fixed composition coordinates.",
5136
5136
  snippet: truncateSnippet(raw)
5137
5137
  });
5138
5138
  }
@@ -5168,9 +5168,9 @@ ${other.raw}`)
5168
5168
  findings.push({
5169
5169
  code: "gsap_callback_dom_measurement",
5170
5170
  severity: "warning",
5171
- message: "Timeline callback reaches DOM measurement (getBoundingClientRect/getTotalLength/getComputedStyle/...). The renderer seeks with suppressEvents=false, so callbacks re-fire on every seek \u2014 and a cold render worker runs them against whatever DOM state its own non-linear seek order produced. Measured geometry is seek-order-dependent, and values measured at build time (before the callback ran) are stale or zero.",
5171
+ message: "Timeline callback reaches DOM measurement (getBoundingClientRect/getTotalLength/getComputedStyle/...). The renderer seeks with suppressEvents=false, so callbacks re-fire on every seek, and a cold render worker runs them against whatever DOM state its own non-linear seek order produced. Measured geometry is seek-order-dependent, and values measured at build time (before the callback ran) are stale or zero.",
5172
5172
  selector: truncateSnippet(site, 120),
5173
- fixHint: "Do all measurement and DOM setup synchronously at build time, before registering the timeline \u2014 or derive geometry from fixed composition coordinates instead of measuring.",
5173
+ fixHint: "Do all measurement and DOM setup synchronously at build time, before registering the timeline, or derive geometry from fixed composition coordinates instead of measuring.",
5174
5174
  snippet: truncateSnippet(snippet)
5175
5175
  });
5176
5176
  };
@@ -5243,8 +5243,8 @@ ${other.raw}`)
5243
5243
  findings.push({
5244
5244
  code: "gsap_timeline_return_used_as_tween",
5245
5245
  severity: "error",
5246
- message: `\`${match[0].includes(".push") ? "push" : boundName}\` captures the return of \`.${method}()\` on timeline \`${timelineVars[0]}\`, but a timeline's \`.to()\`/\`.from()\`/\`.set()\` returns THE TIMELINE ITSELF, not the tween it created. Every captured value is the same master timeline, so a tween-scoped call on it \u2014 \`.kill()\` above all \u2014 hits the whole composition: it interrupts the timeline and detaches it from its parent, which freezes a parent-driven seek while an explicit \`tl.progress()\` keeps working. Only \`gsap.to()\` returns a tween.`,
5247
- fixHint: "To build a group of keyframes you can later discard, put them in a nested timeline: `const nested = gsap.timeline(); nested.to(...); tl.add(nested, 0);` \u2014 then `nested.kill()` replaces exactly those keyframes and cannot reach `tl`. Children keep their absolute times when the nest is added at 0. To hold a single real tween, create it with `gsap.to(...)` and place it with `tl.add(tween, at)`.",
5246
+ message: `\`${match[0].includes(".push") ? "push" : boundName}\` captures the return of \`.${method}()\` on timeline \`${timelineVars[0]}\`, but a timeline's \`.to()\`/\`.from()\`/\`.set()\` returns THE TIMELINE ITSELF, not the tween it created. Every captured value is the same master timeline, so a tween-scoped call on it (\`.kill()\` above all) hits the whole composition: it interrupts the timeline and detaches it from its parent, which freezes a parent-driven seek while an explicit \`tl.progress()\` keeps working. Only \`gsap.to()\` returns a tween.`,
5247
+ fixHint: "To build a group of keyframes you can later discard, put them in a nested timeline: `const nested = gsap.timeline(); nested.to(...); tl.add(nested, 0);`. Then `nested.kill()` replaces exactly those keyframes and cannot reach `tl`. Children keep their absolute times when the nest is added at 0. To hold a single real tween, create it with `gsap.to(...)` and place it with `tl.add(tween, at)`.",
5248
5248
  snippet: truncateSnippet(source.slice(contextStart, match.index + match[0].length + 60))
5249
5249
  });
5250
5250
  }
@@ -5339,7 +5339,7 @@ ${other.raw}`)
5339
5339
  findings.push({
5340
5340
  code: "svg_drawon_css_dasharray_conflict",
5341
5341
  severity: "error",
5342
- message: `GSAP writes strokeDasharray on "${targetLabel}", but its CSS ("${conflictToken}") declares a multi-component stroke-dasharray. GSAP merges dash lists per component, so the CSS gap survives (e.g. "641.4px, 10px") \u2014 the draw-on hide only hides one gap's worth and the line stays visible the whole scene.`,
5342
+ message: `GSAP writes strokeDasharray on "${targetLabel}", but its CSS ("${conflictToken}") declares a multi-component stroke-dasharray. GSAP merges dash lists per component, so the CSS gap survives (e.g. "641.4px, 10px"). The draw-on hide only hides one gap's worth and the line stays visible the whole scene.`,
5343
5343
  selector: quotedSelector ?? void 0,
5344
5344
  fixHint: `Remove the CSS stroke-dasharray from "${conflictToken}" (decorative dashes belong on a separate element), or set the full two-component value in GSAP: strokeDasharray: "\${len} \${len}".`,
5345
5345
  snippet: truncateSnippet(match[0] + firstVars.slice(1))
@@ -5461,7 +5461,7 @@ ${other.raw}`)
5461
5461
  findings.push({
5462
5462
  code: "svg_measure_before_path_d",
5463
5463
  severity: sameVarAssignmentExists ? "warning" : "error",
5464
- message: sameVarAssignmentExists ? `getTotalLength() is called on "${tokenLabel}", whose \`d\` is only assigned inside a function body \u2014 if the measure runs before that function (e.g. the function is a timeline callback), the length is 0 and the dash animation is dead.` : `getTotalLength() is called on "${tokenLabel}", but the path has no static \`d\` attribute and no d assignment exists anywhere \u2014 getTotalLength() returns 0 in Chrome, silently killing dash animations.`,
5464
+ message: sameVarAssignmentExists ? `getTotalLength() is called on "${tokenLabel}", whose \`d\` is only assigned inside a function body. If the measure runs before that function (e.g. the function is a timeline callback), the length is 0 and the dash animation is dead.` : `getTotalLength() is called on "${tokenLabel}", but the path has no static \`d\` attribute and no d assignment exists anywhere, so getTotalLength() returns 0 in Chrome, silently killing dash animations.`,
5465
5465
  selector: tokenLabel,
5466
5466
  fixHint: "Assign the path's `d` synchronously at build time (top level, before measuring), or author a static d attribute in the HTML.",
5467
5467
  snippet: truncateSnippet(match[0] + ")")
@@ -6009,7 +6009,7 @@ var fontRules = [
6009
6009
  {
6010
6010
  code: "system_font_will_alias",
6011
6011
  severity: "warning",
6012
- message: `Font ${aliased.length === 1 ? "family" : "families"} will be substituted at render time: ${aliased.join(", ")}. In distributed/Lambda rendering system-font capture is disabled \u2014 these fonts will fall back to OS defaults. Embed explicit @font-face declarations instead.`
6012
+ message: `Font ${aliased.length === 1 ? "family" : "families"} will be substituted at render time: ${aliased.join(", ")}. In distributed/Lambda rendering system-font capture is disabled, so these fonts will fall back to OS defaults. Embed explicit @font-face declarations instead.`
6013
6013
  }
6014
6014
  ];
6015
6015
  },
@@ -6029,7 +6029,7 @@ var fontRules = [
6029
6029
  code: "font_family_without_font_face",
6030
6030
  severity: "error",
6031
6031
  message: `Font ${undeclared.length === 1 ? "family" : "families"} used without @font-face declaration: ${undeclared.join(", ")}. These are not in the auto-resolved font list, so the renderer cannot supply them automatically. Text will fall back to a generic font, producing incorrect typography in the video.`,
6032
- fixHint: "Add @font-face { font-family: '...'; src: url('capture/assets/fonts/...woff2'); } for each font family, pointing to the captured .woff2 files. For an OS-bundled system font (e.g. Hiragino Sans, Microsoft YaHei) that has no downloadable file, use src: local('Exact Font Name') instead \u2014 the declaration alone satisfies this check without needing a font file."
6032
+ fixHint: "Add @font-face { font-family: '...'; src: url('capture/assets/fonts/...woff2'); } for each font family, pointing to the captured .woff2 files. For an OS-bundled system font (e.g. Hiragino Sans, Microsoft YaHei) that has no downloadable file, use src: local('Exact Font Name') instead. The declaration alone satisfies this check without needing a font file."
6033
6033
  });
6034
6034
  return findings;
6035
6035
  }
@@ -6457,4 +6457,3 @@ export {
6457
6457
  lintMediaUrls,
6458
6458
  shouldBlockRender
6459
6459
  };
6460
- //# sourceMappingURL=browser.js.map