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 +136 -87
- package/dist/studio/assets/hyperframes-player-BRjQ5a5_.js +418 -0
- package/dist/studio/assets/{index-BVqybwMG.css → index-1C8oFiPi.css} +1 -1
- package/dist/studio/assets/{index-Btg-jqxS.js → index-CXy_mWnP.js} +1 -1
- package/dist/studio/assets/{index-tn9M43-U.js → index-CfiyaR7G.js} +1 -1
- package/dist/studio/assets/{index-BgbVhDYd.js → index-D-3sGz65.js} +88 -88
- package/dist/studio/index.html +2 -2
- package/dist/templates/warm-grain/compositions/captions.html +154 -0
- package/dist/templates/warm-grain/compositions/graphics.html +165 -0
- package/dist/templates/warm-grain/compositions/intro.html +84 -0
- package/dist/templates/warm-grain/index.html +271 -0
- package/dist/templates/warm-grain/registry-item.json +34 -0
- package/package.json +1 -1
- package/dist/studio/assets/hyperframes-player-67pq7USK.js +0 -418
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.
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
57073
|
-
const
|
|
57074
|
-
if (
|
|
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(
|
|
57107
|
+
JSON.parse(transcriptJson);
|
|
57077
57108
|
} catch {
|
|
57078
57109
|
findings.push({
|
|
57079
57110
|
code: "caption_transcript_parse_error",
|
|
57080
|
-
severity: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
101014
|
-
|
|
101015
|
-
|
|
101016
|
-
|
|
101017
|
-
|
|
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 (
|
|
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
|
|
149236
|
-
["Capture 10 evenly-spaced frames", "snapshot
|
|
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
|
|
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
|
-
|
|
153180
|
-
|
|
153181
|
-
|
|
153182
|
-
|
|
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));
|