diff --git a/packages/lint/src/rules/gsap.test.ts b/packages/lint/src/rules/gsap.test.ts
index 2715ff5a9..cfefe769c 100644
--- a/packages/lint/src/rules/gsap.test.ts
+++ b/packages/lint/src/rules/gsap.test.ts
@@ -1194,6 +1194,63 @@ describe("GSAP rules", () => {
expect(finding).toBeDefined();
});
+ it("errors when a style block's LAST declaration is opacity:0 without a semicolon", async () => {
+ const html = `
+
+
+
+
+`;
+ const result = await lintHyperframeHtml(html);
+ const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
+ expect(finding).toBeDefined();
+ });
+
+ it("does NOT error for inline opacity: 0.98 + gsap.from({opacity:0}) — fractional is not zero", async () => {
+ const html = `
+
+
+

+
+
+`;
+ const result = await lintHyperframeHtml(html);
+ const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
+ expect(finding).toBeUndefined();
+ });
+
+ it("still errors for inline opacity: 0 without a trailing semicolon", async () => {
+ const html = `
+
+
+
+`;
+ const result = await lintHyperframeHtml(html);
+ const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
+ expect(finding).toBeDefined();
+ });
+
it("does NOT error when gsap.from({opacity:0}) and CSS has no opacity:0", async () => {
const html = `
diff --git a/packages/lint/src/rules/gsap.ts b/packages/lint/src/rules/gsap.ts
index b54b7c873..ec2e4c9b8 100644
--- a/packages/lint/src/rules/gsap.ts
+++ b/packages/lint/src/rules/gsap.ts
@@ -1128,11 +1128,18 @@ export const gsapRules: LintRule[] = [
const findings: HyperframeLintFinding[] = [];
const cssOpacityZeroSelectors = new Set();
+ // Single owner of "this declaration list sets opacity to EXACTLY zero" —
+ // boundary-anchored so `opacity: 0.98` never matches. Works for both a CSS
+ // block body (brace already stripped by the block regex) and an inline
+ // style attribute: the declaration ends at `;` or at end of input, which
+ // also catches a final declaration without a trailing semicolon.
+ const opacityExactlyZero = /opacity\s*:\s*0(?:\.0+)?\s*(?:;|$)/;
+
for (const style of styles) {
for (const [, selector, body] of style.content.matchAll(
/([#.][a-zA-Z0-9_-]+)\s*\{([^}]+)\}/g,
)) {
- if (body && /opacity\s*:\s*0\s*[;}]/.test(body)) {
+ if (body && opacityExactlyZero.test(body)) {
cssOpacityZeroSelectors.add((selector ?? "").trim());
}
}
@@ -1140,7 +1147,7 @@ export const gsapRules: LintRule[] = [
for (const tag of tags) {
const inlineStyle = readAttr(tag.raw, "style");
- if (!inlineStyle || !/opacity\s*:\s*0/.test(inlineStyle)) continue;
+ if (!inlineStyle || !opacityExactlyZero.test(inlineStyle)) continue;
const id = readAttr(tag.raw, "id");
const classes = readAttr(tag.raw, "class")?.split(/\s+/).filter(Boolean) ?? [];
if (id) cssOpacityZeroSelectors.add(`#${id}`);
diff --git a/packages/studio/src/components/editor/domEditingDom.ts b/packages/studio/src/components/editor/domEditingDom.ts
index cfd347445..82b564978 100644
--- a/packages/studio/src/components/editor/domEditingDom.ts
+++ b/packages/studio/src/components/editor/domEditingDom.ts
@@ -28,7 +28,7 @@ export function isTextBearingTag(tagName: string): boolean {
return ["div", "span", "p", "strong", "h1", "h2", "h3", "h4", "h5", "h6"].includes(tagName);
}
-const COLOR_GRADING_SOURCE_HIDDEN_ATTR = "data-hf-color-grading-source-hidden";
+import { COLOR_GRADING_SOURCE_HIDDEN_ATTR } from "@hyperframes/core/color-grading";
export function isElementVisibleThroughAncestors(el: HTMLElement): boolean {
const win = el.ownerDocument.defaultView;