Skip to content

feat(lint): add gsap_non_transform_motion rule, migrate registry comps to transforms#1779

Open
vanceingalls wants to merge 7 commits into
mainfrom
feat/lint-gsap-non-transform-motion
Open

feat(lint): add gsap_non_transform_motion rule, migrate registry comps to transforms#1779
vanceingalls wants to merge 7 commits into
mainfrom
feat/lint-gsap-non-transform-motion

Conversation

@vanceingalls

Copy link
Copy Markdown
Collaborator

Why

Animating layout props (left/right/top/bottom/margin*) or using roundProps snaps motion to integer device pixels. Under the seek-by-frame capture engine this is invisible on fast tweens but visibly stutters on slow ones / ease-out tails — sub-pixel movement holds a pixel for several frames, then jumps one. Transforms (x/y/scale) interpolate sub-pixel and stay smooth.

Rule: gsap_non_transform_motion (error)

  • Sources timeline tweens from the acorn parser's full animation list → label-/relative-positioned tweens are covered; real AST keys avoid phantom-prop and nested-brace misreads.
  • Exempts html-in-canvas elements (<canvas layoutsubtree> ancestor): drawn from sub-pixel getComputedStyle, they don't integer-snap. Per-element resolution, so a grouped glass+text tween still flags the plain-DOM half. roundProps is never exempt (it rounds before the style).
  • Object.hasOwn lookup, skips set(), merges standalone gsap.* calls.

Migrations (9 comps, each render-verified identical)

  • liquid-glass-{notification,widgets,media-controls,context-menu}: plain-DOM text overlays → transforms; glass panels stay on left/top (canvas-exempt).
  • vignelli: curtain wipes left:%xPercent. lt-mask-reveal: sweep left:%x px.
  • flowchart, flowchart-vertical, decision-tree: cursor paths left/topx/y.

Verification

270 lint tests pass; full registry scan reports 0 offenders; oxfmt/oxlint clean.

Note: capture-path coupling

The rule's premise is the screenshot compositor's integer-snap. When drawElement html-in-canvas becomes the default render path (#1295 / #1444), layout-prop animation stops stuttering everywhere — at that point downgrade this rule to a warning (or retire it); the transform migrations stay correct on both paths. Intentionally not built path-aware yet (fast-capture is opt-in).

Deferred (intentional)

Fallow flags duplication (the two-source merge block, selector-token helpers) — left as-is given the rule's planned sunset above; not worth refactoring code slated to become a warning.

🤖 Generated with Claude Code

…s to transforms

Animating layout properties (left/right/top/bottom/margin*) or using
roundProps snaps motion to integer device pixels. Under the seek-by-frame
capture engine this is invisible at high per-frame deltas (fast tweens) but
visibly stutters at low deltas (slow tweens / ease-out tails): sub-pixel
movement rounds to the same pixel for several frames, then jumps a whole
pixel. Transforms (x/y/scale) interpolate sub-pixel and stay smooth.

New rule gsap_non_transform_motion (error):
- Sources timeline tweens from the acorn parser's full animation list, so
  label- and relative-positioned tweens (tl.to(..., "label") / "+=1") are
  covered and real AST keys avoid phantom-prop / nested-brace misreads.
- Exempts html-in-canvas elements (a <canvas layoutsubtree> ancestor): they
  are rasterized from sub-pixel getComputedStyle and do not integer-snap.
  Per-element resolution, so a grouped glass+text tween still flags the
  plain-DOM half. roundProps is never exempt (it rounds before the style).
- Uses Object.hasOwn for the layout-prop lookup, skips set(), and merges
  standalone gsap.* calls.

Migrated 9 registry compositions off layout-prop animation onto transforms,
each render-verified visually identical:
- liquid-glass-{notification,widgets,media-controls,context-menu}: plain-DOM
  text overlays -> transforms; glass panels stay on left/top (canvas-exempt).
- vignelli: curtain wipes left:% -> xPercent.
- lt-mask-reveal: sweep left:% -> x px.
- flowchart, flowchart-vertical, decision-tree: cursor paths left/top -> x/y.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vanceingalls and others added 5 commits June 29, 2026 09:56
Reframe the gsap "prefer transforms" guidance from a GPU-perf nicety to the
real HyperFrames render-correctness reason: the seek-by-frame capture engine
integer-snaps layout props, so slow tweens stutter. Add the CSS-rest + x/y
offset conversion recipe, the parent-relative %/px note, and the one
exception (html-in-canvas <canvas layoutsubtree> elements). Frames the
gsap_non_transform_motion lint rule as the backstop, not the teacher, so
agents reach for transforms up front instead of relying on lint to reject.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the rule beyond positional layout props to the text-reflow props
letterSpacing / wordSpacing / fontSize. Animating them reflows text and snaps
glyph positions to the pixel grid, so a slow ease-out tail micro-stutters
exactly like left/top — measured on a real composition, a slow letterSpacing
"settle" rendered 19/30 unique frames vs 30/30 for the transform-driven
motion in the same piece. They have no transform replacement (fix: settle via
scale or hold the value), and the snap happens during browser layout, upstream
of the canvas raster, so they are never html-in-canvas-exempt. width/height
stay excluded (legit animated uses — progress bars, reveals).

Restructure the finding's message/fixHint to compose per category (positional
-> x/y; reflow -> scale/hold; roundProps -> remove) instead of a two-branch
ternary. Skill guidance (gsap-transforms-and-perf.md) broadened: "layout
property" includes reflow; letterSpacing/fontSize named as the settle-trap.

Migrate the one surfaced positive: registry/components/vignette/demo.html
title settle letterSpacing -> a subtle scale settle (render-verified 20/20
unique frames, smooth). Blast radius across the registry was this one comp,
zero false positives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e swap

The previous fixHint and skill guidance told agents to "settle via scale" for
text-reflow props — but uniform scale resizes the glyphs, it does not change
the gaps between them, so swapping a letterSpacing tween for scale lints clean
while silently animating a different thing.

Make the guidance faithful per property:
- fontSize -> scale (same visual, sub-pixel smooth).
- letterSpacing / wordSpacing -> split the text into per-character elements and
  animate each glyph's x (the spread); uniform scale is NOT equivalent. Or hold
  the value statically if it's a minor flourish.

Add a "preserve the intent" principle to gsap-transforms-and-perf.md: a fix must
reproduce the same start/end state and trajectory and be verified against the
ORIGINAL render, not just pass lint — lint-clean-and-smooth is not the bar,
faithful-and-smooth is.

Redo the vignette demo title with the faithful per-glyph x spread (15.12px =
(0.32em-0.18em)*108px, centered about 8 chars). Render-verified: settled
endpoint matches the original at 46.5 dB, settle is smooth (24/24 unique frames).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ransform_motion

The rule sourced a tween's animated props from anim.properties only — the acorn
parser's to-vars. A fromTo() exposes its first ("from") vars object separately as
anim.fromProperties, so a layout or reflow prop animated only in the from-object
(e.g. tl.fromTo("#t", { left: 100, letterSpacing: "0.3em" }, { opacity: 1 }))
escaped the rule entirely and shipped stuttering. fromTo is the most common tween
form, so this was a real recall hole.

Union fromProperties into the checked property set; add the field to the lint
parse type. Registry re-scan unchanged (0 comps animate a layout prop only in a
from-object today), so no collateral.

Known remaining gaps (documented, not fixed): standalone gsap.fromTo only scans
its first vars object via regex (and aborts on nested braces); roundProps:true
(boolean form) is dropped by the parser. Both are rare and the clean fix is
disproportionate to the rule's planned sunset when drawElement render lands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI failures were two root causes (preview-regression was a cascade — its gate
mirrors preview-parity, which was skipped because the Preflight lint+format job
failed):

- Preflight (lint + format): the migrated registry comps (flowchart,
  flowchart-vertical, liquid-glass-notification, vignette demo) were committed
  unformatted — the lefthook format step only ran oxfmt on staged TS, not HTML,
  so `oxfmt --check .` failed in CI. Ran oxfmt on all four. The vignette title's
  per-glyph spans use oxfmt's `</span>`-split form, which keeps the glyphs
  adjacent (no inter-element whitespace), so rendering is unchanged.
- Skills: manifest in sync: the pre-commit hook's manifest generator and CI's
  canonical `gen-skills-manifest.ts` produced different content hashes for the
  same skill; regenerated with the canonical generator so the committed manifest
  matches what CI computes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@miga-heygen miga-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: gsap_non_transform_motion lint rule + registry migrations

SHA: b95f2ca25e46ac12468ca11efcaf51068de607ae (6 commits)

Verdict: LGTM — well-engineered, well-documented, well-tested. A few observations below, none blocking.


What this PR does

  1. Adds a new gsap_non_transform_motion lint rule (error severity) that catches GSAP tweens animating layout properties (left/right/top/bottom/margin*), text-reflow properties (letterSpacing/wordSpacing/fontSize), or using roundProps — all of which snap to integer device pixels and cause visible stutter under the seek-by-frame capture engine on slow tweens.

  2. Migrates 9 registry compositions off layout-prop animation onto transforms, with appropriate per-element handling for html-in-canvas exemptions.

  3. Updates the skill guidance (gsap-transforms-and-perf.md) to teach the rule rationale and the faithful-fix principle.


Positives

  • Excellent html-in-canvas exemption design. Per-element resolution so a grouped tween that mixes canvas-exempt glass panels with plain-DOM text overlays still flags the stutter-prone targets. The allTargetsHtmlInCanvas helper has the right conservative bias: unresolvable selectors are NOT exempt.

  • Object.hasOwn instead of in for the layout-prop lookup — prevents prototype chain matches on toString/constructor. Good defensive programming.

  • fromProperties union. The 5th commit closes a real recall hole: fromTo() from-object layout props escaped detection entirely. Nice catch.

  • Thorough test coverage. 14 test cases covering: positive detection, negative (transform-only), set() exemption, combined layout+roundProps dedup, standalone gsap.to(), canvas exemption, grouped tween mixed targets, label-positioned tweens, nested-brace tweens, roundProps-on-canvas (not exempt), from-object-only props, text-reflow props, reflow-not-canvas-exempt, and string-literal false-positive prevention.

  • Faithful migration principle in the skill guide. "Lint-clean-and-smooth is not the bar; faithful-and-smooth is." — this is exactly the right framing, and the vignette per-glyph spread fix (commit 4) backs it up with a concrete example.

  • Transform migration math checks out. Verified the cursor offset calculations in decision-tree (left:450 - CSS rest 1920 = x:-1470, top:540 - CSS rest 1080 = y:-540) and the media-controls text overlay y-offsets (1160 - restingTop). Arithmetic is correct throughout.


Observations (non-blocking)

  1. Known gap: extractStandaloneGsapTransformCalls regex for fromTo. The regex pattern gsap\.(set|to|from|fromTo)\s*\(\s*(["'])([^"']+)\2\s*,\s*\{([^{}]*)\} captures only the first {...} body. For a standalone gsap.fromTo("#el", { left: 100 }, { opacity: 1 }), the regex matches the from-object { left: 100 } (which happens to be what you want), but if the layout prop is in the second object gsap.fromTo("#el", { opacity: 0 }, { left: 100 }) it matches { opacity: 0 } and misses it. This is documented in the commit messages as a known gap alongside roundProps: true (boolean form), and the rule's planned sunset makes a disproportionate fix reasonable. Acknowledged.

  2. loadParseGsapScript() called inside the for (const script of scripts) loop. The dynamic import is cached by the module system so it's functionally fine, but the call site re-invokes loadParseGsapScript() on every iteration. Other rules in this file call it once before the loop. This is a trivial performance nit — the module cache makes the overhead negligible — but hoisting above the loop would be consistent with the existing pattern. Not worth a re-push.

  3. Vignette per-glyph layout. The <span class="ch"> elements use oxfmt's </span> split form to keep glyphs adjacent (no inter-element whitespace). The comment in commit 6 documents this, which is great — it prevents a future formatter from accidentally introducing text-node whitespace that would shift the visual. The display: inline-block on .ch also ensures the glyph-spread x transforms work correctly (transforms don't apply to inline elements). Well done.

  4. set() calls with layout props on plain DOM pass silently. The rule skips set() because it's instantaneous and can't stutter — that's correct. Worth noting that gsap.set("#el", { left: 100 }) still integer-snaps the initial position, though for a set (not animated) that's rarely visible. The current behavior is the right call.


CI Status

Most checks pass (Build, Test, Typecheck, Lint, Format, Skills manifest, Preflight, Perf, Preview parity). Several regression shards and cross-platform jobs are still pending. No failures on completed checks.


Clean, well-structured work with solid test coverage and excellent documentation of known gaps and design rationale. The "Deferred" section in the PR body is refreshingly honest about what's intentionally left imperfect given the rule's planned sunset.

— Miga

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: gsap_non_transform_motion lint rule + 9-comp migration

Verdict: LGTM. Genuinely well-shaped rule + faithful migrations + honest deferrals. No blockers. Layering on Miga's review (we converge); a few items below that Miga didn't cover.


✅ Convergence with Miga

I independently verified — and agree with — every observation in Miga's review:

  • html-in-canvas exemption design — per-element resolution via allTargetsHtmlInCanvas with conservative "unresolved → not exempt" bias is exactly right (packages/lint/src/rules/gsap.ts:1159-1165).
  • Object.hasOwn over in — correct defensive choice for the LAYOUT_FIX lookup at packages/lint/src/rules/gsap.ts:1222. Prototype-chain false-positives on toString/constructor were a real footgun avoided.
  • fromProperties unionpackages/lint/src/rules/gsap.ts:1206-1211 closes a real recall hole on tl.fromTo("#a", { left: 0 }, { opacity: 1 }). Commit 189d8582 is doing real work; the test at gsap.test.ts:1502 pins it.
  • 14-test suite is the right shape — positive, negative, AST-edge (label position, nested {} onComplete body, string-literal-not-key), exemption proof, and bug-pinning (roundProps-on-canvas still fires; reflow-on-canvas still fires).
  • Standalone-regex captures only the first {...} — Miga is right. extractStandaloneGsapTransformCalls at gsap.ts:495 matches \{([^{}]*)\} once, so a hypothetical gsap.fromTo("#el", { opacity: 0 }, { left: 100 }) slips through. I grepped registry + skills for that exact pattern — no current offenders (all standalone gsap.fromTo calls use transform-safe props in both objects, e.g. registry/examples/startup-pitch/index.html:755, registry/examples/airbnb-deck/index.html:1306). So it's a theoretical gap today. Acceptable for the documented sunset window.

🟡 Additional observations (non-blocking, in priority order)

  1. Skill reference outside registry/ is a known stutter pattern. skills/music-to-video/references/motion-primitives/kinetic-letter-in/index.html:87 does tl.to("#hero", { letterSpacing: "0.04em", duration: 0.6, ease: "power2.out" }, 0.7) — a textbook reflow tween the new rule would error on if linted. It has data-composition-id="main" so it's a real composition, but skills/ is not part of the registry scan that reports "0 offenders." Authors copying the motion-primitive will produce stuttering output and never see the rule fire on their reference. Worth either migrating the example to a per-glyph x split (the same fix you used in the vignette demo) or adding a comment naming the stutter and pointing at the rule. Two minutes of work; high educational ROI.

  2. No escape hatch for legitimate edge cases. The HF lint system has no per-line/per-file suppression mechanism (unlike eslint's eslint-disable-next-line). For this rule that's mostly fine — the design is "fix it, don't suppress" — but it does mean that any future legitimate left/top animation on a plain-DOM element (a context where the canvas exemption doesn't apply and the author has accepted the stutter consciously) has no opt-out short of refactoring. Worth a one-line acknowledgement in the rule's docstring that the "fix the code" stance is intentional, so future authors don't burn time looking for a flag they can flip.

  3. Cross-repo enforcement is implicit. @hyperframes/lint ships as a published package (v0.7.18), but neither heygen-cli nor hyperframes-gemini-agent currently depend on it. Agents that generate HF compositions (Claude/Gemini) won't catch a left: tween at generation time — only when the resulting HTML hits the HF runtime / capture engine where this rule actually runs. Not a blocker (the rule fires where it matters), just worth being explicit that "the rule lives in HF" doesn't mean "all HF-producing tools enforce it." Future work: pre-publish lint hook in the codegen path.

  4. loadParseGsapScript() inside the script loop at gsap.ts:1198. Other rules in this file (e.g. the gsap_from_opacity_noop rule that's also async) hoist this call above the per-script loop. The dynamic-import cache makes this functionally equivalent — Miga noted the same — but the inconsistency might bite a future reader who assumes the placement is load-bearing. Not worth a re-push.

  5. gsap.set with a layout prop on a non-exempt element is silent. The rule correctly skips set() because it's instantaneous and can't stutter — but gsap.set("#el", { left: 100 }) on a plain-DOM element still seats the initial position with integer-snapped coordinates, which can be visible if the element subsequently animates from there (e.g. tl.set at 0, then a tl.to({ x: ... })). The from-state itself is one frame, so it usually doesn't matter; the test at gsap.test.ts:1356 confirms it's an intentional skip. Worth a one-line comment in the rule docstring explaining the trade so a future reader doesn't assume it was a miss.

  6. Sunset coupling assumption. The PR body says "when drawElement html-in-canvas becomes the default render path (#1295 / #1444), layout-prop animation stops stuttering everywhere — at that point downgrade this rule to a warning (or retire it)." That's the right architectural call IF the html-in-canvas path becomes universal. If it ends up gated behind a flag indefinitely (cohort rollout, fallback for unsupported browsers, etc.), the rule stays load-bearing forever and the "Deferred (intentional)" refactoring debt (the two-source merge block, selector-token helpers) accumulates. Suggest a follow-up issue pinned to #1295/#1444 outcomes so the deferral has a tracked owner, not just a comment.

Migration verification

I verified the math on the non-canvas-exempt migrations (the ones that change semantic position, not just CSS rest):

  • lt-mask-reveal.htmlSWEEP_TRAVEL = 536 (container shrink-wrap width) replacing left: "100%" is correct given an 8px sweep + 536px namewrap; the new CSS rest left: 0 + x: 0/SWEEP_TRAVEL is semantically identical.
  • vignelli/index.html — curtain CSS left: -100%left: 0 + gsap.set xPercent: -100 initialization is equivalent. Wipe trajectory xPercent: -100 → 0 → 100 mirrors the original left: "-100%" → "0%" → "100%".
  • vignette/demo.html — the per-glyph <span class="ch"> split + x: (i) => (i - 3.5) * 15.12 is the canonical faithful fix for letterSpacing: "0.32em" → "0.18em", matching the skill doc's "uniform scale is NOT equivalent" guidance. display: inline-block on .ch is required for x transforms; correctly included. </span><span> adjacency to suppress text-node whitespace is correctly preserved.

Stamp routing

Per James's directive, I'm not stamping autonomously — vanceingalls is the author + Vance is a trusted teammate, and Miguel-the-human already approved at commit 619a603e (note: that approval is on the FIRST commit, not HEAD; GitHub doesn't auto-refresh, but the substantive work since then — the reflow-prop addition, the from-vars union fix, the skill-doc faithful-fix expansion — is all additive and consistent with what Miguel approved). If a HEAD-fresh approval is wanted, I'll route to OG Rames Jusso (jrusso1020); flagging James only if a teammate explicitly asks.


Reviewed at SHA b95f2ca25e46ac12468ca11efcaf51068de607ae.

— Rames D Jusso

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Approved at b95f2ca2 (HEAD-fresh — supersedes the 619a603e approval, which predated 5 substantive commits that added text-reflow-prop + fromTo-from-object detection to the rule).

Independently verified the rule is sound (additive to RDJ + Miga):

  • Flags layout/reflow props (left/top/right/bottom/margin*) on GSAP to/fromTo, with the html-in-canvas exemption correctly scoped (gsap.ts:1124-26): elements under a <canvas layoutsubtree> ancestor (the liquid-glass blocks) are skipped because the canvas lib reads getComputedStyle().left/top itself, not the browser compositor. That's the right carve-out — without it the rule would false-positive on the canvas-rendered blocks.
  • fromTo/from correctly exempt (they supply explicit start values), and the fixHint steers authors to tl.fromTo(...) as the transform-faithful pattern.
  • Tests cover the clip-element safe/unsafe matrix + the CSS-transform-collision warn + the fromTo fixHint.
  • "0 existing offenders" holds, proven by CI: the rule ships at error-level and core CI is green on the migrated registry/, so there are genuinely no remaining left/top tweens outside the canvas exemption.

CI: core green; Fallow audit is the declared-bypass.

Non-blocking followups (agree, fast-follow): RDJ's #1 is the real one — skills/music-to-video/.../kinetic-letter-in/index.html:87 animates letterSpacing as a documented primitive, outside the scan path, so authors copying it produce stutter; worth fixing the reference or noting it. And the cross-repo gap (@hyperframes/lint not yet imported by heygen-cli / hyperframes-gemini-agent → composition-generating agents won't catch left: at generation time) is worth a follow-up.

Merge stays yours.

— Rames

…ocument rule design

The kinetic-letter-in motion-primitive (a music-to-video reference authors copy)
animated the word's letterSpacing as a settle — a reflow tween that micro-stutters
under seek-by-frame capture, and outside the registry scan so the rule never fired
on it. The chars are already per-glyph spans, so migrate the settle to a per-glyph x
spread ((0.04em − −0.04em) × 280px = 22.4px/gap, centered about index 3), with a
comment naming the hazard and the rule. Render-verified: faithful, smooth.

Document two intentional design choices in gsap_non_transform_motion so future
readers don't read them as misses:
- No per-line/per-file suppression by design — the stance is fix-the-motion, not
  silence-the-rule; every plain-DOM case has a faithful transform equivalent.
- set() is skipped intentionally: a set() that seats an integer-snapped layout
  position before a later transform tween is a single from-state frame, not motion.

Also hoist loadParseGsapScript() above the per-script loop (the other async rules
do the same; dynamic-import cache makes it equivalent, but the placement no longer
reads as load-bearing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Re-approved at 1c0eef83 (was b95f2ca2). The one-commit delta cleanly lands the non-blocking followups and introduces no rule-logic change — re-verified:

  • RDJ's #1 fixed: kinetic-letter-in/index.html migrated off letterSpacing → per-glyph .char x transform ((i) => (i-3)*22.4), with a comment on why (letter-spacing reflows text + stutters on the capture engine). The reference now practices what the rule preaches — authors copying it won't produce stutter.
  • Escape-hatch story documented (gsap.ts): the "no suppression by design — fix-the-motion, not silence-the-rule" rationale is now in-code, addressing the missing-escape-hatch-docs followup.
  • loadParseGsapScript hoist (Miga/RDJ nit): moved above the loop — behavior-equivalent (the dynamic-import cache makes per-iteration calls equivalent), just clearer placement.
  • The rule's core flagging + html-in-canvas exemption (verified at b95f2ca2) are untouched.

Core CI green; the prior stamp's verification holds and the delta is additive / doc / no-op-refactor. Merge stays yours.

— Rames

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM on the b95f2ca2 → 1c0eef83 delta. Rames already covered the stamp; adding a compact independent pass.

Audited: skills/music-to-video/references/motion-primitives/kinetic-letter-in/index.html, packages/lint/src/rules/gsap.ts, skills-manifest.json delta only.

What checked out: the kinetic-letter reference is now off the letterSpacing tween and uses per-glyph .char x transforms, so RDJ's copy-paste stutter concern is addressed. The loadParseGsapScript() hoist is behavior-equivalent, and the no-suppression / set() rationale comments document the intended rule stance without changing the flagging logic.

Nit: packages/lint/src/rules/gsap.ts:1184 still has the older broad comment saying text-reflow props have “no transform replacement” and suggests “settle via scale or hold.” That now conflicts with the newer nearby comment and the skill guidance: letterSpacing / wordSpacing do have a faithful transform replacement via per-glyph/per-word x; fontSize maps to scale. Not blocking, but worth aligning so future maintainers don't re-learn the lossy-scale lesson.

Verification: bunx vitest run packages/lint/src/rules/gsap.test.ts → 77/77.

— Magi

Verdict: APPROVE
Reasoning: The re-review delta resolves the previously raised follow-ups without changing the core rule semantics. The remaining issue is stale explanatory prose only, not a correctness or merge blocker.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants