hyperframes 0.6.108 → 0.6.110

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/cli.js CHANGED
@@ -50,7 +50,7 @@ var VERSION;
50
50
  var init_version = __esm({
51
51
  "src/version.ts"() {
52
52
  "use strict";
53
- VERSION = true ? "0.6.108" : "0.0.0-dev";
53
+ VERSION = true ? "0.6.110" : "0.0.0-dev";
54
54
  }
55
55
  });
56
56
 
@@ -48396,7 +48396,10 @@ var init_core = __esm({
48396
48396
  return findings;
48397
48397
  },
48398
48398
  // missing_timeline_registry + timeline_registry_missing_init
48399
- ({ source }) => {
48399
+ ({ source, rawSource, options }) => {
48400
+ if (options.isSubComposition || rawSource.trimStart().toLowerCase().startsWith("<template")) {
48401
+ return [];
48402
+ }
48400
48403
  const findings = [];
48401
48404
  if (!TIMELINE_REGISTRY_INIT_PATTERN.test(source) && !TIMELINE_REGISTRY_ASSIGN_PATTERN.test(source)) {
48402
48405
  findings.push({
@@ -48607,6 +48610,7 @@ var init_core = __esm({
48607
48610
  return findings;
48608
48611
  },
48609
48612
  // pointer_events_none
48613
+ // fallow-ignore-next-line complexity
48610
48614
  ({ tags, styles }) => {
48611
48615
  const findings = [];
48612
48616
  const reported = /* @__PURE__ */ new Set();
@@ -56637,7 +56641,7 @@ ${right.raw}`)
56637
56641
  if (hasHardKill) continue;
56638
56642
  findings.push({
56639
56643
  code: "gsap_exit_missing_hard_kill",
56640
- severity: "warning",
56644
+ severity: "error",
56641
56645
  message: `GSAP exit on "${win.targetSelector}" ends at the ${boundary.toFixed(2)}s clip start boundary without a matching tl.set hard kill. Non-linear seeking can land after the fade and leave stale visibility state.`,
56642
56646
  selector: win.targetSelector,
56643
56647
  fixHint: `Add \`tl.set("${win.targetSelector}", ${hiddenStateLiteral(win.propertyValues)}, ${boundary.toFixed(2)})\` after the exit tween.`,
@@ -56671,7 +56675,7 @@ ${right.raw}`)
56671
56675
  if (className && (classUsage.get(className) || 0) < 2) continue;
56672
56676
  findings.push({
56673
56677
  code: "unscoped_gsap_selector",
56674
- severity: "warning",
56678
+ severity: "error",
56675
56679
  message: `Timeline "${localTimelineCompId}" uses unscoped selector "${win.targetSelector}" that will target elements in ALL compositions when bundled, causing data loss (opacity, transforms, etc.).`,
56676
56680
  selector: win.targetSelector,
56677
56681
  fixHint: `Scope the selector: \`[data-composition-id="${localTimelineCompId}"] ${win.targetSelector}\` or use a unique id.`,
@@ -56725,7 +56729,7 @@ ${right.raw}`)
56725
56729
  const windows = await cachedExtractGsapWindows(script.content);
56726
56730
  const conflicts = /* @__PURE__ */ new Map();
56727
56731
  for (const win of windows) {
56728
- if (win.method === "fromTo") continue;
56732
+ if (win.method === "fromTo" || win.method === "from") continue;
56729
56733
  const sel = win.targetSelector;
56730
56734
  const cssKey = sel.startsWith("#") || sel.startsWith(".") ? sel : `#${sel}`;
56731
56735
  const translateProps = win.properties.filter(
@@ -56749,7 +56753,7 @@ ${right.raw}`)
56749
56753
  const fixHint = gsapEquivalent ? `Remove \`transform: ${cssTransform}\` from CSS and replace with GSAP properties: ${gsapEquivalent}. Example: tl.fromTo('${sel}', { ${gsapEquivalent} }, { ${gsapEquivalent}, ...yourAnimation }). tl.fromTo is exempt from this rule.` : `Remove the transform from CSS and use tl.fromTo('${sel}', { xPercent: -50, x: -1000 }, { xPercent: -50, x: 0 }) so GSAP owns the full transform state. tl.fromTo is exempt from this rule.`;
56750
56754
  findings.push({
56751
56755
  code: "gsap_css_transform_conflict",
56752
- severity: "warning",
56756
+ severity: "error",
56753
56757
  message: `"${sel}" has CSS \`transform: ${cssTransform}\` and a GSAP tween animates ${propList}. GSAP will overwrite the full CSS transform, discarding any translateX(-50%) centering or CSS scale value.`,
56754
56758
  selector: sel,
56755
56759
  fixHint,
@@ -56861,7 +56865,7 @@ ${right.raw}`)
56861
56865
  });
56862
56866
  if (sceneElements.length < 2) return findings;
56863
56867
  for (const script of scripts) {
56864
- const content = script.content;
56868
+ const content = stripJsComments(script.content);
56865
56869
  for (const tag of sceneElements) {
56866
56870
  const id = readAttr(tag.raw, "id") || "";
56867
56871
  const exitPattern = new RegExp(`["']#${id}["'][^)]*opacity\\s*:\\s*0`);
@@ -56872,7 +56876,7 @@ ${right.raw}`)
56872
56876
  if (!hasKill) {
56873
56877
  findings.push({
56874
56878
  code: "scene_layer_missing_visibility_kill",
56875
- severity: "warning",
56879
+ severity: "error",
56876
56880
  elementId: id,
56877
56881
  message: `Scene layer "#${id}" exits via opacity tween but has no visibility: hidden hard kill. When scrubbing or when tweens conflict, the scene may remain partially visible and overlap the next scene.`,
56878
56882
  fixHint: `Add \`tl.set("#${id}", { visibility: "hidden" }, <exit-end-time>)\` after the scene's exit tweens.`
@@ -56893,7 +56897,7 @@ ${right.raw}`)
56893
56897
  if (hasRegistration || canInheritFromHost) continue;
56894
56898
  findings.push({
56895
56899
  code: "gsap_timeline_not_registered",
56896
- severity: "warning",
56900
+ severity: "error",
56897
56901
  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.",
56898
56902
  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).'
56899
56903
  });
@@ -56929,6 +56933,7 @@ ${right.raw}`)
56929
56933
  for (const win of windows) {
56930
56934
  if (win.method !== "from") continue;
56931
56935
  if (!win.properties.includes("opacity")) continue;
56936
+ if (win.propertyValues["opacity"] !== 0) continue;
56932
56937
  const sel = win.targetSelector;
56933
56938
  const cssKey = sel.startsWith("#") || sel.startsWith(".") ? sel : `#${sel}`;
56934
56939
  if (!cssOpacityZeroSelectors.has(cssKey)) continue;
@@ -57000,6 +57005,31 @@ ${right.raw}`)
57000
57005
  });
57001
57006
 
57002
57007
  // ../core/src/lint/rules/captions.ts
57008
+ function extractArrayLiteral(src, varMatch) {
57009
+ const openIdx = varMatch.index + varMatch[0].length - 1;
57010
+ let depth = 0;
57011
+ let inStr = false;
57012
+ let strChar = "";
57013
+ for (let i2 = openIdx; i2 < src.length; i2++) {
57014
+ const c3 = src[i2];
57015
+ if (inStr) {
57016
+ if (c3 === "\\") {
57017
+ i2++;
57018
+ continue;
57019
+ }
57020
+ if (c3 === strChar) inStr = false;
57021
+ } else if (c3 === '"' || c3 === "'") {
57022
+ inStr = true;
57023
+ strChar = c3;
57024
+ } else if (c3 === "[") {
57025
+ depth++;
57026
+ } else if (c3 === "]") {
57027
+ depth--;
57028
+ if (depth === 0) return src.slice(openIdx, i2 + 1);
57029
+ }
57030
+ }
57031
+ return null;
57032
+ }
57003
57033
  var captionRules;
57004
57034
  var init_captions = __esm({
57005
57035
  "../core/src/lint/rules/captions.ts"() {
@@ -57018,7 +57048,7 @@ var init_captions = __esm({
57018
57048
  if (hasCaptionLoop && hasExitTween && !hasHardKill) {
57019
57049
  findings.push({
57020
57050
  code: "caption_exit_missing_hard_kill",
57021
- severity: "warning",
57051
+ severity: "error",
57022
57052
  message: "Caption exit animations (tl.to with opacity: 0) detected without a hard tl.set kill. Exit tweens can fail when karaoke word-level tweens conflict, leaving captions stuck on screen.",
57023
57053
  fixHint: 'Add `tl.set(groupEl, { opacity: 0, visibility: "hidden" }, group.end)` after every exit tl.to animation as a deterministic kill.'
57024
57054
  });
@@ -57051,6 +57081,7 @@ var init_captions = __esm({
57051
57081
  return findings;
57052
57082
  },
57053
57083
  // caption_transcript_not_inline
57084
+ // fallow-ignore-next-line complexity
57054
57085
  ({ scripts, styles, options }) => {
57055
57086
  const findings = [];
57056
57087
  const isCaptionFile = options.filePath && /caption/i.test(options.filePath) || styles.some((s2) => /\.caption[-_]?(?:group|word)/i.test(s2.content));
@@ -57063,21 +57094,21 @@ var init_captions = __esm({
57063
57094
  if (!hasInlineTranscript && hasFetchTranscript) {
57064
57095
  findings.push({
57065
57096
  code: "caption_transcript_not_inline",
57066
- severity: "warning",
57097
+ severity: "error",
57067
57098
  message: "Captions composition loads transcript via fetch(). The studio caption editor requires an inline `var TRANSCRIPT = [...]` array to detect and edit captions.",
57068
57099
  fixHint: 'Embed the transcript as `var TRANSCRIPT = [{ "text": "...", "start": 0, "end": 1 }, ...]` with JSON-quoted property keys. See the captions skill for details.'
57069
57100
  });
57070
57101
  }
57071
57102
  if (hasInlineTranscript) {
57072
- const varPattern = /(?:const|let|var)\s+(?:TRANSCRIPT|script)\s*=\s*(\[[\s\S]*?\]);/;
57073
- const match = allScript.match(varPattern);
57074
- if (match?.[1]) {
57103
+ const varStart = /(?:const|let|var)\s+(?:TRANSCRIPT|script)\s*=\s*\[/.exec(allScript);
57104
+ const transcriptJson = varStart ? extractArrayLiteral(allScript, varStart) : null;
57105
+ if (transcriptJson) {
57075
57106
  try {
57076
- JSON.parse(match[1]);
57107
+ JSON.parse(transcriptJson);
57077
57108
  } catch {
57078
57109
  findings.push({
57079
57110
  code: "caption_transcript_parse_error",
57080
- severity: "warning",
57111
+ severity: "error",
57081
57112
  message: "Inline TRANSCRIPT array is not valid JSON. The studio caption editor may fail to parse it. Common cause: unquoted property keys with apostrophes in text.",
57082
57113
  fixHint: `Use JSON-quoted keys: { "text": "don't", "start": 0, "end": 1 } instead of { text: "don't", start: 0, end: 1 }.`
57083
57114
  });
@@ -57098,7 +57129,7 @@ var init_captions = __esm({
57098
57129
  if (/position\s*:\s*relative/i.test(body)) {
57099
57130
  findings.push({
57100
57131
  code: "caption_container_relative_position",
57101
- severity: "warning",
57132
+ severity: "error",
57102
57133
  selector: (selector ?? "").trim(),
57103
57134
  message: `Caption selector "${(selector ?? "").trim()}" uses position: relative which causes overflow and breaks caption stacking.`,
57104
57135
  fixHint: "Use position: absolute for all caption elements."
@@ -57124,7 +57155,7 @@ var init_captions = __esm({
57124
57155
  if (/overflow\s*:\s*hidden/i.test(body)) {
57125
57156
  findings.push({
57126
57157
  code: "caption_overflow_clips_scaled_words",
57127
- severity: "warning",
57158
+ severity: "error",
57128
57159
  selector: (selector ?? "").trim(),
57129
57160
  message: `"${(selector ?? "").trim()}" has overflow: hidden but GSAP scales caption words above 1.0x. Scaled emphasis words and their glow effects will be clipped.`,
57130
57161
  fixHint: "Use overflow: visible on caption containers. Rely on fitTextFontSize with reduced maxWidth to prevent overflow instead."
@@ -57154,6 +57185,7 @@ var init_captions = __esm({
57154
57185
  return findings;
57155
57186
  },
57156
57187
  // caption_fittext_scale_mismatch
57188
+ // fallow-ignore-next-line complexity
57157
57189
  ({ scripts }) => {
57158
57190
  const findings = [];
57159
57191
  for (const script of scripts) {
@@ -57252,6 +57284,7 @@ var init_composition = __esm({
57252
57284
  ];
57253
57285
  },
57254
57286
  // timeline_track_too_dense
57287
+ // fallow-ignore-next-line complexity
57255
57288
  ({ tags, options }) => {
57256
57289
  const trackCounts = /* @__PURE__ */ new Map();
57257
57290
  for (const tag of tags) {
@@ -57276,6 +57309,7 @@ var init_composition = __esm({
57276
57309
  return findings;
57277
57310
  },
57278
57311
  // timed_element_missing_visibility_hidden
57312
+ // fallow-ignore-next-line complexity
57279
57313
  ({ tags }) => {
57280
57314
  const findings = [];
57281
57315
  for (const tag of tags) {
@@ -57302,6 +57336,7 @@ var init_composition = __esm({
57302
57336
  return findings;
57303
57337
  },
57304
57338
  // deprecated_data_layer + deprecated_data_end
57339
+ // fallow-ignore-next-line complexity
57305
57340
  ({ tags }) => {
57306
57341
  const findings = [];
57307
57342
  for (const tag of tags) {
@@ -57309,7 +57344,7 @@ var init_composition = __esm({
57309
57344
  const elementId = readAttr(tag.raw, "id") || void 0;
57310
57345
  findings.push({
57311
57346
  code: "deprecated_data_layer",
57312
- severity: "warning",
57347
+ severity: "error",
57313
57348
  message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> uses data-layer instead of data-track-index.`,
57314
57349
  elementId,
57315
57350
  fixHint: "Replace data-layer with data-track-index. The runtime reads data-track-index.",
@@ -57320,7 +57355,7 @@ var init_composition = __esm({
57320
57355
  const elementId = readAttr(tag.raw, "id") || void 0;
57321
57356
  findings.push({
57322
57357
  code: "deprecated_data_end",
57323
- severity: "warning",
57358
+ severity: "error",
57324
57359
  message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> uses data-end without data-duration. Use data-duration in source HTML.`,
57325
57360
  elementId,
57326
57361
  fixHint: "Replace data-end with data-duration. The compiler generates data-end from data-duration automatically.",
@@ -57373,27 +57408,8 @@ var init_composition = __esm({
57373
57408
  }
57374
57409
  return findings;
57375
57410
  },
57376
- // external_script_dependency
57377
- ({ source }) => {
57378
- const findings = [];
57379
- const externalScriptRe = /<script\b[^>]*\bsrc=["'](https?:\/\/[^"']+)["'][^>]*>/gi;
57380
- let match;
57381
- const seen = /* @__PURE__ */ new Set();
57382
- while ((match = externalScriptRe.exec(source)) !== null) {
57383
- const src = match[1] ?? "";
57384
- if (seen.has(src)) continue;
57385
- seen.add(src);
57386
- findings.push({
57387
- code: "external_script_dependency",
57388
- severity: "info",
57389
- message: `This composition loads an external script from \`${src}\`. The HyperFrames bundler automatically hoists CDN scripts from sub-compositions into the parent document. In unbundled runtime mode, \`loadExternalCompositions\` re-injects them. If you're using a custom pipeline that bypasses both, you'll need to include this script manually.`,
57390
- fixHint: "No action needed when using `hyperframes preview` or `hyperframes render`. If using a custom pipeline, add this script tag to your root composition or HTML page.",
57391
- snippet: truncateSnippet(match[0] ?? "")
57392
- });
57393
- }
57394
- return findings;
57395
- },
57396
57411
  // timed_element_missing_clip_class
57412
+ // fallow-ignore-next-line complexity
57397
57413
  ({ tags }) => {
57398
57414
  const findings = [];
57399
57415
  const skipTags = /* @__PURE__ */ new Set(["audio", "video", "script", "style", "template"]);
@@ -57403,15 +57419,14 @@ var init_composition = __esm({
57403
57419
  if (readAttr(tag.raw, "data-composition-src")) continue;
57404
57420
  const hasStart = readAttr(tag.raw, "data-start") !== null;
57405
57421
  const hasDuration = readAttr(tag.raw, "data-duration") !== null;
57406
- const hasTrackIndex = readAttr(tag.raw, "data-track-index") !== null;
57407
- if (!hasStart && !hasDuration && !hasTrackIndex) continue;
57422
+ if (!hasStart && !hasDuration) continue;
57408
57423
  const classAttr = readAttr(tag.raw, "class") || "";
57409
57424
  const hasClip = classAttr.split(/\s+/).includes("clip");
57410
57425
  if (hasClip) continue;
57411
57426
  const elementId = readAttr(tag.raw, "id") || void 0;
57412
57427
  findings.push({
57413
57428
  code: "timed_element_missing_clip_class",
57414
- severity: "warning",
57429
+ severity: "error",
57415
57430
  message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> has timing attributes but no class="clip". The element will be visible for the entire composition instead of only during its scheduled time range.`,
57416
57431
  elementId,
57417
57432
  fixHint: 'Add class="clip" to the element. The HyperFrames runtime uses .clip to control visibility based on data-start/data-duration.',
@@ -57421,6 +57436,7 @@ var init_composition = __esm({
57421
57436
  return findings;
57422
57437
  },
57423
57438
  // overlapping_clips_same_track
57439
+ // fallow-ignore-next-line complexity
57424
57440
  ({ tags }) => {
57425
57441
  const findings = [];
57426
57442
  const trackMap = /* @__PURE__ */ new Map();
@@ -57471,7 +57487,7 @@ var init_composition = __esm({
57471
57487
  if (!hasStart) {
57472
57488
  findings.push({
57473
57489
  code: "root_composition_missing_data_start",
57474
- severity: "warning",
57490
+ severity: "error",
57475
57491
  message: `Root composition "${compId}" is missing data-start. The runtime needs data-start="0" on the root element to begin playback.`,
57476
57492
  fixHint: 'Add data-start="0" to the root composition element.',
57477
57493
  snippet: truncateSnippet(rootTag.raw)
@@ -57487,7 +57503,7 @@ var init_composition = __esm({
57487
57503
  if (trimmed.startsWith("<template")) {
57488
57504
  findings.push({
57489
57505
  code: "standalone_composition_wrapped_in_template",
57490
- severity: "warning",
57506
+ severity: "error",
57491
57507
  message: "Root index.html is wrapped in a <template> tag. Only sub-compositions loaded via data-composition-src should use <template> wrappers. The runtime cannot play a standalone composition inside a template.",
57492
57508
  fixHint: "Remove the <template> wrapper. Use <!DOCTYPE html><html>...<div data-composition-id>...</div>...</html> instead."
57493
57509
  });
@@ -57514,14 +57530,15 @@ var init_composition = __esm({
57514
57530
  return findings;
57515
57531
  },
57516
57532
  // requestanimationframe_in_composition
57517
- ({ scripts }) => {
57533
+ ({ scripts, rawSource, options }) => {
57534
+ if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return [];
57518
57535
  const findings = [];
57519
57536
  for (const script of scripts) {
57520
57537
  const stripped = stripJsComments(script.content);
57521
57538
  if (/requestAnimationFrame\s*\(/.test(stripped)) {
57522
57539
  findings.push({
57523
57540
  code: "requestanimationframe_in_composition",
57524
- severity: "warning",
57541
+ severity: "error",
57525
57542
  message: "`requestAnimationFrame` runs on wall-clock time, not the GSAP timeline. It will not sync with frame capture and may cause flickering or missed frames during rendering.",
57526
57543
  fixHint: "Use GSAP tweens or onUpdate callbacks instead of requestAnimationFrame for animation logic.",
57527
57544
  snippet: truncateSnippet(script.content)
@@ -57535,6 +57552,7 @@ var init_composition = __esm({
57535
57552
  // `data-variable-values`. The runtime swallows JSON errors silently and
57536
57553
  // falls back to declared defaults, which masks typos. This rule surfaces
57537
57554
  // the parse failure so authors notice before render time.
57555
+ // fallow-ignore-next-line complexity
57538
57556
  ({ tags }) => {
57539
57557
  const findings = [];
57540
57558
  for (const tag of tags) {
@@ -57547,7 +57565,7 @@ var init_composition = __esm({
57547
57565
  const reason = err instanceof Error ? err.message : "unknown";
57548
57566
  findings.push({
57549
57567
  code: "invalid_variable_values_json",
57550
- severity: "warning",
57568
+ severity: "error",
57551
57569
  message: `data-variable-values is not valid JSON (${reason}).`,
57552
57570
  fixHint: `Wrap the attribute value in single quotes and the JSON keys/values in double quotes, e.g. data-variable-values='{"title":"Hello"}'.`,
57553
57571
  elementId: readAttr(tag.raw, "id") || void 0,
@@ -57558,7 +57576,7 @@ var init_composition = __esm({
57558
57576
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
57559
57577
  findings.push({
57560
57578
  code: "invalid_variable_values_json",
57561
- severity: "warning",
57579
+ severity: "error",
57562
57580
  message: 'data-variable-values must be a JSON object keyed by variable id (e.g. {"title":"Hello"}).',
57563
57581
  fixHint: "Replace the value with a JSON object whose keys are variable ids declared in the sub-composition's data-composition-variables.",
57564
57582
  elementId: readAttr(tag.raw, "id") || void 0,
@@ -57573,6 +57591,7 @@ var init_composition = __esm({
57573
57591
  // on any structural problem. Surface JSON / shape failures so authors
57574
57592
  // catch them at lint time rather than wondering why their `getVariables()`
57575
57593
  // defaults aren't applied.
57594
+ // fallow-ignore-next-line complexity
57576
57595
  ({ source }) => {
57577
57596
  const htmlTag = findHtmlTag(source);
57578
57597
  if (!htmlTag) return [];
@@ -57586,7 +57605,7 @@ var init_composition = __esm({
57586
57605
  return [
57587
57606
  {
57588
57607
  code: "invalid_composition_variables_declaration",
57589
- severity: "warning",
57608
+ severity: "error",
57590
57609
  message: `data-composition-variables is not valid JSON (${reason}).`,
57591
57610
  fixHint: `Provide a JSON array of variable declarations: data-composition-variables='[{"id":"title","type":"string","label":"Title","default":"Hello"}]'.`,
57592
57611
  snippet: truncateSnippet(htmlTag.raw)
@@ -57597,7 +57616,7 @@ var init_composition = __esm({
57597
57616
  return [
57598
57617
  {
57599
57618
  code: "invalid_composition_variables_declaration",
57600
- severity: "warning",
57619
+ severity: "error",
57601
57620
  message: "data-composition-variables must be a JSON array of variable declarations.",
57602
57621
  fixHint: `Wrap declarations in [] and give each an id, type, label, and default: '[{"id":"title","type":"string","label":"Title","default":"Hello"}]'.`,
57603
57622
  snippet: truncateSnippet(htmlTag.raw)
@@ -57611,7 +57630,7 @@ var init_composition = __esm({
57611
57630
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
57612
57631
  findings.push({
57613
57632
  code: "invalid_composition_variables_declaration",
57614
- severity: "warning",
57633
+ severity: "error",
57615
57634
  message: `data-composition-variables entry [${i2}] must be an object with id, type, label, and default.`,
57616
57635
  snippet: truncateSnippet(htmlTag.raw)
57617
57636
  });
@@ -57626,7 +57645,7 @@ var init_composition = __esm({
57626
57645
  if (missing.length > 0) {
57627
57646
  findings.push({
57628
57647
  code: "invalid_composition_variables_declaration",
57629
- severity: "warning",
57648
+ severity: "error",
57630
57649
  message: `data-composition-variables entry [${i2}] is missing or has invalid: ${missing.join(", ")}. Type must be one of string, number, color, boolean, enum.`,
57631
57650
  snippet: truncateSnippet(htmlTag.raw)
57632
57651
  });
@@ -57833,7 +57852,7 @@ var init_textures = __esm({
57833
57852
  if (definedTextureClasses.has(textureClass)) continue;
57834
57853
  findings.push({
57835
57854
  code: "texture_class_unknown",
57836
- severity: "warning",
57855
+ severity: "error",
57837
57856
  message: `Texture material class \`${textureClass}\` is not defined by local CSS.`,
57838
57857
  elementId: readAttr(tag.raw, "id") || void 0,
57839
57858
  fixHint: "Paste the Texture Mask Text component `<style>...</style>` block into the composition, or fix the texture class typo.",
@@ -58036,6 +58055,7 @@ var init_fonts = __esm({
58036
58055
  "../core/src/lint/rules/fonts.ts"() {
58037
58056
  "use strict";
58038
58057
  init_aliases2();
58058
+ init_composition();
58039
58059
  GENERIC_FAMILIES = /* @__PURE__ */ new Set([
58040
58060
  "serif",
58041
58061
  "sans-serif",
@@ -58057,7 +58077,8 @@ var init_fonts = __esm({
58057
58077
  ]);
58058
58078
  fontRules = [
58059
58079
  // google_fonts_import
58060
- ({ styles, source }) => {
58080
+ ({ styles, source, rawSource, options }) => {
58081
+ if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return [];
58061
58082
  const findings = [];
58062
58083
  const googleFontsInLink = /<link\b[^>]*fonts\.googleapis\.com[^>]*>/i.test(source);
58063
58084
  const googleFontsInImport = styles.some(
@@ -58066,7 +58087,7 @@ var init_fonts = __esm({
58066
58087
  if (googleFontsInLink || googleFontsInImport) {
58067
58088
  findings.push({
58068
58089
  code: "google_fonts_import",
58069
- severity: "warning",
58090
+ severity: "error",
58070
58091
  message: "Composition loads fonts from fonts.googleapis.com. External font requests fail in sandboxed/offline renders and add latency. Use local @font-face declarations with captured .woff2 files instead.",
58071
58092
  fixHint: "Replace the Google Fonts <link> or @import with @font-face { font-family: '...'; src: url('capture/assets/fonts/Font.woff2'); } pointing to captured font files."
58072
58093
  });
@@ -58089,7 +58110,8 @@ var init_fonts = __esm({
58089
58110
  ];
58090
58111
  },
58091
58112
  // font_family_without_font_face
58092
- ({ styles }) => {
58113
+ ({ styles, rawSource, options }) => {
58114
+ if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return [];
58093
58115
  const findings = [];
58094
58116
  const declared = extractFontFaceFamilies(styles);
58095
58117
  const used = extractUsedFontFamilies(styles);
@@ -58097,7 +58119,7 @@ var init_fonts = __esm({
58097
58119
  if (undeclared.length === 0) return findings;
58098
58120
  findings.push({
58099
58121
  code: "font_family_without_font_face",
58100
- severity: "warning",
58122
+ severity: "error",
58101
58123
  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.`,
58102
58124
  fixHint: "Add @font-face { font-family: '...'; src: url('capture/assets/fonts/...woff2'); } for each font family, pointing to the captured .woff2 files."
58103
58125
  });
@@ -74869,10 +74891,17 @@ function findOnPath2(name) {
74869
74891
  return void 0;
74870
74892
  }
74871
74893
  }
74894
+ function findInCommonDirs(name) {
74895
+ for (const dir of COMMON_BIN_DIRS) {
74896
+ const candidate = `${dir}/${name}`;
74897
+ if (existsSync19(candidate)) return candidate;
74898
+ }
74899
+ return void 0;
74900
+ }
74872
74901
  function findConfiguredBinary(envName, binaryName) {
74873
74902
  const configured = process.env[envName]?.trim();
74874
74903
  if (configured) return existsSync19(configured) ? resolve10(configured) : void 0;
74875
- return findOnPath2(binaryName);
74904
+ return findOnPath2(binaryName) ?? findInCommonDirs(binaryName);
74876
74905
  }
74877
74906
  function findFFmpeg() {
74878
74907
  return findConfiguredBinary(FFMPEG_PATH_ENV2, "ffmpeg");
@@ -74892,12 +74921,13 @@ function getFFmpegInstallHint() {
74892
74921
  return "https://ffmpeg.org/download.html";
74893
74922
  }
74894
74923
  }
74895
- var FFMPEG_PATH_ENV2, FFPROBE_PATH_ENV2;
74924
+ var FFMPEG_PATH_ENV2, FFPROBE_PATH_ENV2, COMMON_BIN_DIRS;
74896
74925
  var init_ffmpeg = __esm({
74897
74926
  "src/browser/ffmpeg.ts"() {
74898
74927
  "use strict";
74899
74928
  FFMPEG_PATH_ENV2 = "HYPERFRAMES_FFMPEG_PATH";
74900
74929
  FFPROBE_PATH_ENV2 = "HYPERFRAMES_FFPROBE_PATH";
74930
+ COMMON_BIN_DIRS = process.platform === "win32" ? [] : ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/snap/bin"];
74901
74931
  }
74902
74932
  });
74903
74933
 
@@ -100563,7 +100593,12 @@ function resolveAssetDir(devSegments, builtSegments) {
100563
100593
  return existsSync54(devPath) ? devPath : builtPath;
100564
100594
  }
100565
100595
  function getStaticTemplateDir(templateId) {
100566
- return resolveAssetDir(["..", "templates", templateId], ["templates", templateId]);
100596
+ const base2 = dirname23(fileURLToPath7(import.meta.url));
100597
+ const devPath = resolve31(base2, "..", "templates", templateId);
100598
+ if (existsSync54(devPath)) return devPath;
100599
+ const registryPath = resolve31(base2, "..", "..", "..", "..", "registry", "examples", templateId);
100600
+ if (existsSync54(registryPath)) return registryPath;
100601
+ return resolve31(base2, "templates", templateId);
100567
100602
  }
100568
100603
  function getSharedTemplateDir() {
100569
100604
  return resolveAssetDir(["..", "templates", "_shared"], ["templates", "_shared"]);
@@ -101002,20 +101037,25 @@ var init_init = __esm({
101002
101037
  console.error(c.error(`Directory already exists and is not empty: ${name2}`));
101003
101038
  process.exit(1);
101004
101039
  }
101005
- mkdirSync33(destDir2, { recursive: true });
101006
- let localVideoName2;
101007
- let videoDuration2;
101008
- let sourceFilePath2;
101009
101040
  if (videoFlag && audioFlag) {
101010
101041
  console.error(c.error("Cannot use --video and --audio together"));
101011
101042
  process.exit(1);
101012
101043
  }
101013
- if (videoFlag) {
101014
- const videoPath = resolve31(videoFlag);
101015
- if (!existsSync54(videoPath)) {
101016
- console.error(c.error(`Video file not found: ${videoFlag}`));
101017
- process.exit(1);
101018
- }
101044
+ const videoPath = videoFlag ? resolve31(videoFlag) : void 0;
101045
+ if (videoPath && !existsSync54(videoPath)) {
101046
+ console.error(c.error(`Video file not found: ${videoFlag}`));
101047
+ process.exit(1);
101048
+ }
101049
+ const audioPath = audioFlag ? resolve31(audioFlag) : void 0;
101050
+ if (audioPath && !existsSync54(audioPath)) {
101051
+ console.error(c.error(`Audio file not found: ${audioFlag}`));
101052
+ process.exit(1);
101053
+ }
101054
+ mkdirSync33(destDir2, { recursive: true });
101055
+ let localVideoName2;
101056
+ let videoDuration2;
101057
+ let sourceFilePath2;
101058
+ if (videoPath) {
101019
101059
  sourceFilePath2 = videoPath;
101020
101060
  const result = await handleVideoFile(videoPath, destDir2, false);
101021
101061
  localVideoName2 = result.localVideoName;
@@ -101024,12 +101064,7 @@ var init_init = __esm({
101024
101064
  `Video: ${result.meta.width}x${result.meta.height}, ${result.meta.durationSeconds.toFixed(1)}s`
101025
101065
  );
101026
101066
  }
101027
- if (audioFlag) {
101028
- const audioPath = resolve31(audioFlag);
101029
- if (!existsSync54(audioPath)) {
101030
- console.error(c.error(`Audio file not found: ${audioFlag}`));
101031
- process.exit(1);
101032
- }
101067
+ if (audioPath) {
101033
101068
  sourceFilePath2 = audioPath;
101034
101069
  copyFileSync6(audioPath, resolve31(destDir2, basename10(audioPath)));
101035
101070
  console.log(`Audio: ${basename10(audioPath)}`);
@@ -149232,8 +149267,8 @@ var init_snapshot = __esm({
149232
149267
  init_ffmpeg();
149233
149268
  FFMPEG_EXTRACT_TIMEOUT_MS = 3e4;
149234
149269
  examples24 = [
149235
- ["Capture 5 key frames from a composition", "snapshot captures/stripe"],
149236
- ["Capture 10 evenly-spaced frames", "snapshot captures/stripe --frames 10"]
149270
+ ["Capture 5 key frames from a composition", "snapshot capture"],
149271
+ ["Capture 10 evenly-spaced frames", "snapshot capture --frames 10"]
149237
149272
  ];
149238
149273
  snapshot_default = defineCommand({
149239
149274
  meta: {
@@ -153088,8 +153123,8 @@ var init_capture2 = __esm({
153088
153123
  "use strict";
153089
153124
  init_dist();
153090
153125
  examples25 = [
153091
- ["Capture a website", "hyperframes capture https://stripe.com"],
153092
- ["Capture to a specific directory", "hyperframes capture https://linear.app -o linear-video"],
153126
+ ["Capture a website into ./capture/", "hyperframes capture https://stripe.com"],
153127
+ ["Capture to a different directory", "hyperframes capture https://linear.app -o linear-video"],
153093
153128
  ["JSON output for AI agents", "hyperframes capture https://example.com --json"],
153094
153129
  [
153095
153130
  "Pull a video from the captured manifest by index",
@@ -153113,7 +153148,7 @@ var init_capture2 = __esm({
153113
153148
  },
153114
153149
  output: {
153115
153150
  type: "string",
153116
- description: "Output directory name",
153151
+ description: "Output directory name (default: ./capture, then ./capture-2/, ./capture-3/, \u2026)",
153117
153152
  alias: "o"
153118
153153
  },
153119
153154
  "skip-assets": {
@@ -153152,6 +153187,7 @@ var init_capture2 = __esm({
153152
153187
  default: false
153153
153188
  }
153154
153189
  },
153190
+ // fallow-ignore-next-line complexity
153155
153191
  async run({ args }) {
153156
153192
  if (args.video) {
153157
153193
  const { runVideoMode: runVideoMode2 } = await Promise.resolve().then(() => (init_video(), video_exports));
@@ -153176,17 +153212,30 @@ var init_capture2 = __esm({
153176
153212
  console.error(`Invalid URL: ${url}`);
153177
153213
  process.exit(1);
153178
153214
  }
153179
- let outputName = args.output;
153180
- if (!outputName) {
153181
- const hostname = new URL(url).hostname.replace(/^www\./, "");
153182
- outputName = `captures/${hostname.replace(/\./g, "-")}`;
153215
+ const isDefaultOutput = !args.output;
153216
+ let outputName = args.output ?? "capture";
153217
+ let outputDir = resolve52(outputName);
153218
+ if (isDefaultOutput) {
153219
+ const { existsSync: existsSync92 } = await import("fs");
153220
+ let n2 = 2;
153221
+ while (existsSync92(outputDir) && n2 < 100) {
153222
+ outputName = `capture-${n2}`;
153223
+ outputDir = resolve52(outputName);
153224
+ n2++;
153225
+ }
153226
+ if (existsSync92(outputDir)) {
153227
+ console.error(`./capture-{2..99} are all taken. Pass -o <name> to pick a directory.`);
153228
+ process.exit(1);
153229
+ }
153183
153230
  }
153184
- const outputDir = resolve52(outputName);
153185
153231
  const isJson = args.json;
153186
153232
  if (!isJson) {
153187
153233
  const { c: c3 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
153188
153234
  console.log();
153189
153235
  console.log(c3.dim("\u25C6") + " Capturing " + c3.bold(url));
153236
+ if (isDefaultOutput && outputName !== "capture") {
153237
+ console.log(` ${c3.dim(`(./capture/ exists; writing to ./${outputName}/)`)}`);
153238
+ }
153190
153239
  console.log();
153191
153240
  }
153192
153241
  const { captureWebsite: captureWebsite2 } = await Promise.resolve().then(() => (init_capture(), capture_exports));