diff --git a/packages/core/src/lint/hyperframeLinter.test.ts b/packages/core/src/lint/hyperframeLinter.test.ts
index a3fe608e2..1e4b6b618 100644
--- a/packages/core/src/lint/hyperframeLinter.test.ts
+++ b/packages/core/src/lint/hyperframeLinter.test.ts
@@ -217,6 +217,120 @@ describe("lintScriptUrls", () => {
vi.unstubAllGlobals();
});
+
+ // ── gsap_css_transform_conflict ──────────────────────────────────────────
+
+ it("warns when tl.to animates x on an element with CSS translateX", () => {
+ const html = `
+
+
+
+
+`;
+ const result = lintHyperframeHtml(html);
+ const finding = result.findings.find((f) => f.code === "gsap_css_transform_conflict");
+ expect(finding).toBeDefined();
+ expect(finding?.severity).toBe("warning");
+ expect(finding?.selector).toBe("#title");
+ expect(finding?.fixHint).toMatch(/fromTo/);
+ expect(finding?.fixHint).toMatch(/xPercent/);
+ });
+
+ it("warns when tl.to animates scale on an element with CSS scale transform", () => {
+ const html = `
+
+
+
+
+`;
+ const result = lintHyperframeHtml(html);
+ const finding = result.findings.find((f) => f.code === "gsap_css_transform_conflict");
+ expect(finding).toBeDefined();
+ expect(finding?.severity).toBe("warning");
+ expect(finding?.selector).toBe("#hero");
+ });
+
+ it("does NOT warn when tl.to targets element without CSS transform", () => {
+ const html = `
+
+
+
+
+`;
+ const result = lintHyperframeHtml(html);
+ const conflict = result.findings.find((f) => f.code === "gsap_css_transform_conflict");
+ expect(conflict).toBeUndefined();
+ });
+
+ it("does NOT warn when tl.fromTo targets element WITH CSS transform (author owns both ends)", () => {
+ const html = `
+
+
+
+
+`;
+ const result = lintHyperframeHtml(html);
+ const conflict = result.findings.find((f) => f.code === "gsap_css_transform_conflict");
+ expect(conflict).toBeUndefined();
+ });
+
+ it("emits one warning when a combined CSS transform conflicts with multiple GSAP properties", () => {
+ const html = `
+
+
+
+
+`;
+ const result = lintHyperframeHtml(html);
+ const conflicts = result.findings.filter((f) => f.code === "gsap_css_transform_conflict");
+ expect(conflicts).toHaveLength(1);
+ expect(conflicts[0]?.message).toMatch(/x\/scale|scale\/x/);
+ });
});
describe("template_literal_selector rule", () => {
diff --git a/packages/core/src/lint/hyperframeLinter.ts b/packages/core/src/lint/hyperframeLinter.ts
index f43deb70c..f97c79e9f 100644
--- a/packages/core/src/lint/hyperframeLinter.ts
+++ b/packages/core/src/lint/hyperframeLinter.ts
@@ -21,6 +21,7 @@ type GsapWindow = {
end: number;
properties: string[];
overwriteAuto: boolean;
+ method: string;
raw: string;
};
@@ -522,6 +523,104 @@ export function lintHyperframeHtml(
}
}
+ // ── Rule: gsap_css_transform_conflict ─────────────────────────────────────
+ // Detects elements whose CSS