@hyperframes/lint 0.8.111 → 0.8.113
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 +209 -33
- package/dist/browser.js.map +1 -1
- package/dist/index.js +209 -33
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/browser.js
CHANGED
|
@@ -10,7 +10,7 @@ var TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN = /window\.__timelines\s*=\s*\{\s*(
|
|
|
10
10
|
var TIMELINE_REGISTRY_ASSIGN_PATTERN = /window\.__timelines(?:\[[^\]]+\]|\.[A-Za-z_$][\w$]*)\s*=/i;
|
|
11
11
|
var WINDOW_TIMELINE_ASSIGN_PATTERN = /window\.__timelines(?:\[\s*(?:["']([^"']+)["']|[A-Za-z_$][\w$.]*)\s*\]|\.\s*([A-Za-z_$][\w$]*))\s*=\s*([A-Za-z_$][\w$]*)/i;
|
|
12
12
|
var INVALID_SCRIPT_CLOSE_PATTERN = /<script[^>]*>[\s\S]*?<\s*\/\s*script(?!>)/i;
|
|
13
|
-
var TIMELINE_REGISTRY_KEY_PATTERN = /window\.__timelines(?:\[\s*["']([^"']+)["']\s*\]|\.\s*([A-Za-z_$][\w$]*))\s
|
|
13
|
+
var TIMELINE_REGISTRY_KEY_PATTERN = /window\.__timelines(?:\[\s*["']([^"']+)["']\s*\]|\.\s*([A-Za-z_$][\w$]*))\s*(?:\?\?|\|\||&&)?=/g;
|
|
14
14
|
var TIMELINE_REGISTRY_OBJECT_OPEN_PATTERN = /window\.__timelines\s*=\s*\{/i;
|
|
15
15
|
var TIMELINE_REGISTRY_OBJECT_ENTRY_PATTERN = /(?:["']([^"']+)["']|([A-Za-z_$][\w$]*))\s*:\s*[A-Za-z_$][\w$]*/g;
|
|
16
16
|
function parseHtmlStructure(source) {
|
|
@@ -114,9 +114,7 @@ function readAttr(tagSource, attr) {
|
|
|
114
114
|
return match?.[1] || null;
|
|
115
115
|
}
|
|
116
116
|
function hasAttrName(tagSource, attr) {
|
|
117
|
-
|
|
118
|
-
const attrs = tagSource.replace(/^<\s*[a-z][\w:-]*/i, "");
|
|
119
|
-
return new RegExp(`(?:^|\\s)${escaped}(?:\\s*=|\\s|/?>)`, "i").test(attrs);
|
|
117
|
+
return readDecodedAttr(tagSource, attr) !== null;
|
|
120
118
|
}
|
|
121
119
|
function isAudibleVideoTag(tagSource) {
|
|
122
120
|
if (hasAttrName(tagSource, "muted")) return false;
|
|
@@ -796,6 +794,34 @@ function repeatedDescendantId(selector) {
|
|
|
796
794
|
}
|
|
797
795
|
return repeated;
|
|
798
796
|
}
|
|
797
|
+
function addUnescapedDigitIds(selector, ids) {
|
|
798
|
+
try {
|
|
799
|
+
selectorParser((root) => {
|
|
800
|
+
root.walkIds((node) => {
|
|
801
|
+
if (/^#\d/.test(node.toString().trim())) ids.add(node.value);
|
|
802
|
+
});
|
|
803
|
+
}).processSync(selector);
|
|
804
|
+
} catch {
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
var SELECTOR_CALL_PATTERN = /(?:\.(?:querySelector(?:All)?|closest|matches|to|from|fromTo|set|toArray)\s*\(|(?<![\w$-])(?:trigger|endTrigger|pin|scrollTrigger|scroller|pinnedContainer|pinSpacer)["']?\s*:)\s*(["'`])((?:\\.|(?!\1)[^\\])*)\1/g;
|
|
808
|
+
function digitIdsTargetedBySelectors(styles, scripts) {
|
|
809
|
+
const ids = /* @__PURE__ */ new Set();
|
|
810
|
+
for (const style of styles) {
|
|
811
|
+
try {
|
|
812
|
+
postcss.parse(style.content).walkRules((rule) => {
|
|
813
|
+
for (const selector of rule.selectors) addUnescapedDigitIds(selector, ids);
|
|
814
|
+
});
|
|
815
|
+
} catch {
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
for (const script of scripts) {
|
|
819
|
+
for (const match of stripJsComments(script.content).matchAll(SELECTOR_CALL_PATTERN)) {
|
|
820
|
+
addUnescapedDigitIds(match[2] ?? "", ids);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return ids;
|
|
824
|
+
}
|
|
799
825
|
function resolvedRuleSelectors(rule) {
|
|
800
826
|
let ancestor = rule.parent;
|
|
801
827
|
while (ancestor && ancestor.type !== "rule") ancestor = ancestor.parent;
|
|
@@ -814,16 +840,20 @@ function resolvedRuleSelectors(rule) {
|
|
|
814
840
|
})
|
|
815
841
|
);
|
|
816
842
|
}
|
|
843
|
+
function rightmostCompoundNodes(selectorNode) {
|
|
844
|
+
const subject = [];
|
|
845
|
+
selectorNode.each((node) => {
|
|
846
|
+
if (node.type === "combinator") subject.length = 0;
|
|
847
|
+
else subject.push(node);
|
|
848
|
+
});
|
|
849
|
+
return subject;
|
|
850
|
+
}
|
|
817
851
|
function selectorAliasesRuntimeHiddenStyle(selector) {
|
|
818
852
|
let unsafe = false;
|
|
819
853
|
try {
|
|
820
854
|
selectorParser((root) => {
|
|
821
855
|
root.each((selectorNode) => {
|
|
822
|
-
const subject =
|
|
823
|
-
selectorNode.each((node) => {
|
|
824
|
-
if (node.type === "combinator") subject.length = 0;
|
|
825
|
-
else subject.push(node);
|
|
826
|
-
});
|
|
856
|
+
const subject = rightmostCompoundNodes(selectorNode);
|
|
827
857
|
const hostScoped = subject.some(
|
|
828
858
|
(node) => node.type === "attribute" && ["data-composition-src", "data-composition-file"].includes(
|
|
829
859
|
node.attribute.toLowerCase()
|
|
@@ -846,6 +876,57 @@ function selectorAliasesRuntimeHiddenStyle(selector) {
|
|
|
846
876
|
}
|
|
847
877
|
return unsafe;
|
|
848
878
|
}
|
|
879
|
+
var POSITION_PROPERTIES = /* @__PURE__ */ new Set(["left", "top", "right", "bottom", "inset"]);
|
|
880
|
+
function weakIdOf(node) {
|
|
881
|
+
if (node.type === "attribute" && node.attribute.toLowerCase() === "id") {
|
|
882
|
+
return node.operator === "=" && node.value ? node.value : null;
|
|
883
|
+
}
|
|
884
|
+
if (node.type !== "pseudo" || node.value.toLowerCase() !== ":where") return null;
|
|
885
|
+
const inner = node.nodes.flatMap((option) => option.nodes).find((n) => n.type === "id");
|
|
886
|
+
return inner ? inner.value : null;
|
|
887
|
+
}
|
|
888
|
+
function reducedSpecificityId(selector) {
|
|
889
|
+
let matched = null;
|
|
890
|
+
try {
|
|
891
|
+
selectorParser((root) => {
|
|
892
|
+
root.each((selectorNode) => {
|
|
893
|
+
const subject = rightmostCompoundNodes(selectorNode);
|
|
894
|
+
if (subject.some((node) => node.type === "id")) return;
|
|
895
|
+
for (const node of subject) matched = weakIdOf(node) ?? matched;
|
|
896
|
+
});
|
|
897
|
+
}).processSync(selector);
|
|
898
|
+
} catch {
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
return matched;
|
|
902
|
+
}
|
|
903
|
+
function cssIdSelector(id) {
|
|
904
|
+
return `#${id.replace(
|
|
905
|
+
/[^a-zA-Z0-9_-]|^-?\d/g,
|
|
906
|
+
(match) => Array.from(match, (char) => `\\${char.codePointAt(0)?.toString(16)} `).join("")
|
|
907
|
+
)}`;
|
|
908
|
+
}
|
|
909
|
+
function reducedSpecificityIdFindings(rule, reported) {
|
|
910
|
+
const positionProps = rule.nodes.flatMap(
|
|
911
|
+
(node) => node.type === "decl" && !node.important && POSITION_PROPERTIES.has(node.prop.toLowerCase()) ? [node.prop] : []
|
|
912
|
+
);
|
|
913
|
+
if (positionProps.length === 0) return [];
|
|
914
|
+
const findings = [];
|
|
915
|
+
for (const selector of resolvedRuleSelectors(rule)) {
|
|
916
|
+
const id = reported.has(selector) ? null : reducedSpecificityId(selector);
|
|
917
|
+
if (id === null) continue;
|
|
918
|
+
reported.add(selector);
|
|
919
|
+
findings.push({
|
|
920
|
+
code: "id_override_reduced_specificity",
|
|
921
|
+
severity: "warning",
|
|
922
|
+
message: `Selector "${selector}" sets ${positionProps.join("/")} through [id=...] or :where(#id), which has class-level or zero specificity, so a compound class rule (e.g. ".parent .row") on the same element can silently win over this override.`,
|
|
923
|
+
selector,
|
|
924
|
+
fixHint: `Use \`${cssIdSelector(id)}\` instead; id specificity beats any selector built only from classes.`,
|
|
925
|
+
snippet: truncateSnippet(rule.toString())
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
return findings;
|
|
929
|
+
}
|
|
849
930
|
function ruleForcesOpacityZero(rule) {
|
|
850
931
|
let forcesOpacityZero = false;
|
|
851
932
|
rule.walkDecls(/^opacity$/i, (declaration) => {
|
|
@@ -853,6 +934,34 @@ function ruleForcesOpacityZero(rule) {
|
|
|
853
934
|
});
|
|
854
935
|
return forcesOpacityZero;
|
|
855
936
|
}
|
|
937
|
+
var CSS_TRANSITION_PROPERTY_PATTERN = /^(?:-webkit-)?transition(?:-[a-z][a-z-]*)?$/i;
|
|
938
|
+
var TRANSITION_NONE_PROPERTIES = /* @__PURE__ */ new Set(["transition", "transition-property"]);
|
|
939
|
+
var TRANSITION_TIME_PROPERTIES = /* @__PURE__ */ new Set([
|
|
940
|
+
"transition",
|
|
941
|
+
"transition-duration",
|
|
942
|
+
"transition-delay"
|
|
943
|
+
]);
|
|
944
|
+
function hasNonZeroTime(value) {
|
|
945
|
+
const times = value.match(/[+-]?(?:\d*\.)?\d+m?s\b/g) ?? [];
|
|
946
|
+
return value.includes("var(") || times.some((time) => Number.parseFloat(time) !== 0);
|
|
947
|
+
}
|
|
948
|
+
function isSeekUnsafeTransition(declaration) {
|
|
949
|
+
const property = declaration.prop.trim().toLowerCase();
|
|
950
|
+
if (!CSS_TRANSITION_PROPERTY_PATTERN.test(property)) return false;
|
|
951
|
+
const name = property.replace(/^-webkit-/, "");
|
|
952
|
+
const value = declaration.value.trim().toLowerCase();
|
|
953
|
+
if (value === "none" && TRANSITION_NONE_PROPERTIES.has(name)) return false;
|
|
954
|
+
return !TRANSITION_TIME_PROPERTIES.has(name) || hasNonZeroTime(value);
|
|
955
|
+
}
|
|
956
|
+
function cssTransitionFinding(declaration, details) {
|
|
957
|
+
return {
|
|
958
|
+
code: "css_transition_used",
|
|
959
|
+
severity: "warning",
|
|
960
|
+
message: `CSS declaration \`${declaration.prop}: ${declaration.value}\` runs on the browser clock and cannot be seeked deterministically across render workers.`,
|
|
961
|
+
fixHint: "Keep the class or attribute swap for state; put the visual change on the paused GSAP timeline.",
|
|
962
|
+
...details
|
|
963
|
+
};
|
|
964
|
+
}
|
|
856
965
|
function isStudioTimelineElement(tag) {
|
|
857
966
|
if (["script", "style", "link", "meta", "template", "noscript"].includes(tag.name)) {
|
|
858
967
|
return false;
|
|
@@ -971,15 +1080,18 @@ function describeRootDimensionsDrift(source, dataWidth, dataHeight) {
|
|
|
971
1080
|
}
|
|
972
1081
|
var coreRules = [
|
|
973
1082
|
// id_requires_css_escape
|
|
974
|
-
({ tags }) => {
|
|
1083
|
+
({ tags, styles, scripts }) => {
|
|
975
1084
|
const findings = [];
|
|
1085
|
+
let targeted;
|
|
976
1086
|
for (const tag of tags) {
|
|
977
1087
|
const id = readAttr(tag.raw, "id");
|
|
978
1088
|
if (!id || !/^\d/.test(id)) continue;
|
|
1089
|
+
targeted ??= digitIdsTargetedBySelectors(styles, scripts);
|
|
1090
|
+
const used = targeted.has(id);
|
|
979
1091
|
findings.push({
|
|
980
1092
|
code: "id_requires_css_escape",
|
|
981
|
-
severity: "warning",
|
|
982
|
-
message: `id="${id}" starts with a digit, so the common selector \`#${id}\` throws a SyntaxError in querySelector().`,
|
|
1093
|
+
severity: used ? "error" : "warning",
|
|
1094
|
+
message: used ? `id="${id}" starts with a digit, and the selector \`#${id}\` used in this composition is invalid: querySelector and GSAP throw a SyntaxError, and CSS drops the rule.` : `id="${id}" starts with a digit, so the common selector \`#${id}\` throws a SyntaxError in querySelector().`,
|
|
983
1095
|
elementId: id,
|
|
984
1096
|
fixHint: "Rename the id to start with a letter (recommended), or build selectors with `#${CSS.escape(id)}` at runtime.",
|
|
985
1097
|
snippet: truncateSnippet(tag.raw)
|
|
@@ -1123,11 +1235,55 @@ var coreRules = [
|
|
|
1123
1235
|
}
|
|
1124
1236
|
return findings;
|
|
1125
1237
|
},
|
|
1238
|
+
// css_transition_used
|
|
1239
|
+
({ styles, tags }) => {
|
|
1240
|
+
const findings = [];
|
|
1241
|
+
for (const style of styles) {
|
|
1242
|
+
if (!/transition/i.test(style.content)) continue;
|
|
1243
|
+
let root;
|
|
1244
|
+
try {
|
|
1245
|
+
root = postcss.parse(style.content);
|
|
1246
|
+
} catch {
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
root.walkDecls((declaration) => {
|
|
1250
|
+
if (!isSeekUnsafeTransition(declaration)) return;
|
|
1251
|
+
const selector = declaration.parent?.type === "rule" ? declaration.parent.selector : void 0;
|
|
1252
|
+
findings.push(
|
|
1253
|
+
cssTransitionFinding(declaration, {
|
|
1254
|
+
selector,
|
|
1255
|
+
snippet: truncateSnippet(declaration.toString())
|
|
1256
|
+
})
|
|
1257
|
+
);
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
for (const tag of tags) {
|
|
1261
|
+
const inlineStyle = readDecodedAttr(tag.raw, "style");
|
|
1262
|
+
if (!inlineStyle || !/transition/i.test(inlineStyle)) continue;
|
|
1263
|
+
let root;
|
|
1264
|
+
try {
|
|
1265
|
+
root = postcss.parse(inlineStyle);
|
|
1266
|
+
} catch {
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
root.walkDecls((declaration) => {
|
|
1270
|
+
if (!isSeekUnsafeTransition(declaration)) return;
|
|
1271
|
+
findings.push(
|
|
1272
|
+
cssTransitionFinding(declaration, {
|
|
1273
|
+
elementId: readDecodedAttr(tag.raw, "id") || void 0,
|
|
1274
|
+
snippet: truncateSnippet(tag.raw)
|
|
1275
|
+
})
|
|
1276
|
+
);
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
return findings;
|
|
1280
|
+
},
|
|
1126
1281
|
// CSS selector safety
|
|
1127
1282
|
({ styles, locate }) => {
|
|
1128
1283
|
const findings = [];
|
|
1129
1284
|
const reportedRepeatedIds = /* @__PURE__ */ new Set();
|
|
1130
1285
|
const reportedHiddenStyleSelectors = /* @__PURE__ */ new Set();
|
|
1286
|
+
const reportedReducedIdSelectors = /* @__PURE__ */ new Set();
|
|
1131
1287
|
for (const style of styles) {
|
|
1132
1288
|
let root;
|
|
1133
1289
|
try {
|
|
@@ -1146,6 +1302,7 @@ var coreRules = [
|
|
|
1146
1302
|
}
|
|
1147
1303
|
root.walkRules((rule) => {
|
|
1148
1304
|
const forcesOpacityZero = ruleForcesOpacityZero(rule);
|
|
1305
|
+
findings.push(...reducedSpecificityIdFindings(rule, reportedReducedIdSelectors));
|
|
1149
1306
|
for (const selector of resolvedRuleSelectors(rule)) {
|
|
1150
1307
|
const repeatedId = repeatedDescendantId(selector);
|
|
1151
1308
|
if (repeatedId && !reportedRepeatedIds.has(repeatedId)) {
|
|
@@ -2383,6 +2540,7 @@ var HEAVY_OVERLAY_EXEMPT_TAGS = /* @__PURE__ */ new Set([
|
|
|
2383
2540
|
]);
|
|
2384
2541
|
var HEAVY_OVERLAY_CSS_PATTERN = /(?:filter\s*:[^;}]*\bblur\s*\()|(?:clip-path\s*:(?!\s*(?:none|inherit|initial|unset)\b)\s*[^;}]+)|(?:radial-gradient\s*\()/i;
|
|
2385
2542
|
var INLINE_STYLE_DISPLAY_NONE_PATTERN = /(?:^|;)\s*display\s*:\s*none\b/i;
|
|
2543
|
+
var COMPUTED_TIMELINE_REGISTRATION_PATTERN = /window\.__timelines\s*\[(?!\s*["'])(?:[^\r\n[\]]|\[[^\r\n\]]*\])+\]\s*(?:\?\?|\|\||&&)?=/;
|
|
2386
2544
|
function readTagTiming(rawTag) {
|
|
2387
2545
|
return readClipTiming({ getAttribute: (name) => readAttr(rawTag, name) });
|
|
2388
2546
|
}
|
|
@@ -2691,6 +2849,26 @@ function negativeZIndexFinding(match, selector) {
|
|
|
2691
2849
|
snippet: truncateSnippet(match[0])
|
|
2692
2850
|
};
|
|
2693
2851
|
}
|
|
2852
|
+
function hasComputedTimelineRegistration(scripts) {
|
|
2853
|
+
return scripts.some(
|
|
2854
|
+
(script) => COMPUTED_TIMELINE_REGISTRATION_PATTERN.test(stripJsCode(script.content))
|
|
2855
|
+
);
|
|
2856
|
+
}
|
|
2857
|
+
function mountsSource(tag) {
|
|
2858
|
+
return Boolean(
|
|
2859
|
+
readAttr(tag.raw, "data-composition-src") || readAttr(tag.raw, "data-composition-file")
|
|
2860
|
+
);
|
|
2861
|
+
}
|
|
2862
|
+
function missingTimelineFinding(tag, compositionId, isRoot) {
|
|
2863
|
+
return {
|
|
2864
|
+
code: "missing_data_no_timeline",
|
|
2865
|
+
severity: "warning",
|
|
2866
|
+
message: isRoot ? "This composition has no `window.__timelines` registration but is missing `data-no-timeline`. The producer polls for timeline registration for up to 45 seconds before timing out, adding 45 s to every render." : `Composition host "${compositionId}" has neither a matching \`window.__timelines\` registration nor \`data-no-timeline\`. The producer waits up to 45 seconds for every \`data-composition-id\` before rendering.`,
|
|
2867
|
+
elementId: readAttr(tag.raw, "id") || void 0,
|
|
2868
|
+
fixHint: isRoot ? 'Add `data-no-timeline` to the root element to skip the poll: `<div data-composition-id="..." data-no-timeline ...>`.' : "If this is a static section, use a plain `id` instead of `data-composition-id`, or add `data-no-timeline`. Otherwise, register its timeline or mount it with `data-composition-src`.",
|
|
2869
|
+
snippet: truncateSnippet(tag.raw)
|
|
2870
|
+
};
|
|
2871
|
+
}
|
|
2694
2872
|
var compositionRules = [
|
|
2695
2873
|
// duplicate_composition_id catches meta-tag/root collisions that create duplicate composition entries.
|
|
2696
2874
|
({ tags }) => {
|
|
@@ -2959,27 +3137,25 @@ var compositionRules = [
|
|
|
2959
3137
|
return findings;
|
|
2960
3138
|
},
|
|
2961
3139
|
// missing_data_no_timeline
|
|
2962
|
-
// The producer polls window.__timelines[id]
|
|
2963
|
-
//
|
|
2964
|
-
//
|
|
2965
|
-
// data-no-timeline
|
|
2966
|
-
({ rootTag, rootCompositionId, scripts, rawSource, options }) => {
|
|
2967
|
-
if (options.isSubComposition) return [];
|
|
3140
|
+
// The producer polls window.__timelines[id] for every composition id in the
|
|
3141
|
+
// rendered document. A timeline-free root or bare nested composition host
|
|
3142
|
+
// therefore spends the full 45-second readiness budget unless it explicitly
|
|
3143
|
+
// opts out with data-no-timeline.
|
|
3144
|
+
({ rootTag, rootCompositionId, tags, scripts, rawSource, options }) => {
|
|
2968
3145
|
if (!rootCompositionId || !rootTag) return [];
|
|
2969
|
-
const tagNoValues = rootTag.raw.replace(/"[^"]*"|'[^']*'/g, '""');
|
|
2970
|
-
if (/(?:^|\s)data-no-timeline(?=[\s>=/]|$)/i.test(tagNoValues)) return [];
|
|
2971
3146
|
if (/<script\b[^>]*\bsrc\s*=/i.test(rawSource)) return [];
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
3147
|
+
if (hasComputedTimelineRegistration(scripts)) return [];
|
|
3148
|
+
const registeredIds = new Set(
|
|
3149
|
+
scripts.flatMap((script) => extractTimelineRegistryKeys(stripJsComments(script.content)))
|
|
3150
|
+
);
|
|
3151
|
+
return tags.flatMap((tag) => {
|
|
3152
|
+
const compositionId = readDecodedAttr(tag.raw, "data-composition-id");
|
|
3153
|
+
if (!compositionId || registeredIds.has(compositionId)) return [];
|
|
3154
|
+
const isRoot = tag.index === rootTag.index;
|
|
3155
|
+
if (isRoot ? options.isSubComposition : isInsideInertTemplate(tag, tags)) return [];
|
|
3156
|
+
if (readDecodedAttr(tag.raw, "data-no-timeline") !== null || mountsSource(tag)) return [];
|
|
3157
|
+
return [missingTimelineFinding(tag, compositionId, isRoot)];
|
|
3158
|
+
});
|
|
2983
3159
|
},
|
|
2984
3160
|
// negative_z_index
|
|
2985
3161
|
// An element at a negative z-index is silently absent from both `snapshot`
|
|
@@ -4282,7 +4458,7 @@ ${right.raw}`)
|
|
|
4282
4458
|
severity: "warning",
|
|
4283
4459
|
message: `${fromToWindows.length} tl.fromTo() calls target "${selector}" with no stable baseline. The last-authored fromTo "from" values become the element's resting state for any seek before the first tween actually runs, because GSAP applies fromTo from-values at authoring time (immediateRender), not at tween position.`,
|
|
4284
4460
|
selector,
|
|
4285
|
-
fixHint: `Add \`immediateRender: false\` to
|
|
4461
|
+
fixHint: `Add \`immediateRender: false\` to every fromTo except the earliest-positioned one (at ${firstFromToPosition}s), so pre-first-tween seeks keep its "from" values. A hiding \`tl.set(..., 0)\` baseline trips gsap_timeline_set_initial_hide, and a \`gsap.set\` authored before the fromTo calls is overwritten by them.`,
|
|
4286
4462
|
snippet: truncateSnippet(fromToWindows.map((win) => win.raw).join("\n"))
|
|
4287
4463
|
});
|
|
4288
4464
|
}
|
|
@@ -4415,7 +4591,7 @@ ${right.raw}`)
|
|
|
4415
4591
|
const cssFromScale = scaleProps.length > 0 ? matchCssTransform(sel, cssScaleSelectors) : void 0;
|
|
4416
4592
|
if (!cssFromTranslate && !cssFromScale) continue;
|
|
4417
4593
|
const existing = conflicts.get(sel) ?? {
|
|
4418
|
-
cssTransform: [cssFromTranslate, cssFromScale].filter(Boolean).join(" "),
|
|
4594
|
+
cssTransform: [...new Set([cssFromTranslate, cssFromScale].filter(Boolean))].join(" "),
|
|
4419
4595
|
props: /* @__PURE__ */ new Set(),
|
|
4420
4596
|
raw: call.raw
|
|
4421
4597
|
};
|