Companion to animxyz-scss-analysis.html (full analysis of packages/core/src).
This plan turns the Issues Found and Modernization sections into eight independent
work briefs, each intended for one subagent in its own git worktree.
| Decision | Choice |
|---|---|
| Release posture | Two tracks: Track A = safe fixes → 0.x patch/minor; Track B = breaking modernization → v1.0 |
xyz-apply |
Fix + document (arg order, token parsing, tests, docs entry) |
_fancy.scss |
Leave orphaned for now — no task, do not delete, do not wire in |
xyz attribute |
Clean break to data-xyz at v1.0 — no dual-selector emission |
@property |
Adopt fully in v1.0 (browser floor: Baseline July 2024 — Chrome 85+/Safari 16.4+/Firefox 128+) |
| Stagger indexing | Progressive enhancement: keep nth-child ladder as fallback, add sibling-index() where supported |
| Cascade layers | Full @layer structure; remove !important and mode source-order dependence |
@starting-style mode, scroll-driven utilities |
Backlog — out of scope for this effort |
| v1.0 scope | Core + Vue/Vue3/React wrappers + docs site |
| Testing | Compile + snapshot tests + GitHub Actions (no browser visual tests) |
| Branching | Track A worktrees branch from master, PR → master. Create long-lived v1 branch from master; Track B worktrees branch from v1, PR → v1. v1 → master when v1.0 ships |
All briefs base off the docs Astro refactor (PR #123, branch astro-refactor),
which is merging to master imminently. Base every worktree on master once #123
has merged; if you must start before then, branch from origin/astro-refactor
instead. That refactor does not touch packages/core/** at all (verified: the only
package change is a vue devDep bump in packages/vue3), so every SCSS brief below
applies verbatim — but it replaces the repo tooling and moves the docs site, which
changes shared mechanics for all briefs:
- Package manager: Yarn → npm. npm workspaces (
packages/*,examples/*,site),package-lock.json,.npmrc,packageManager: npm@11.8.0.yarn.lockandlerna.jsonare gone. Usenpmeverywhere the briefs say "yarn". - Task runner: Lerna → Turborepo. Root scripts are
turbo run build|dev|lint|test.turbo.jsondefines atesttask (dependsOn: ["^build"]). A package participates by defining its owntest/lintscripts; turbo discovers them. - Versioning/release: Lerna → Changesets.
@changesets/cli,.changeset/config.json, scriptschangeset/version-packages(changeset version) /release(turbo run build && changeset publish). Every brief must add a changeset (npx changeset) describing its version bump — that is now how versions move. Track A entries arepatch/minor; Track B (v1.0) entries aremajor.@animxyz/siteis in the changesetignorelist (unpublished). - Docs site:
docs/(Gridsome) →site/(Astro + Vue 3 islands).docs/is fully removed. Doc prose is now markdown insite/src/content/sections/*.md(27 section files); interactive demos are Vue 3 SFCs insite/src/components/vue/; the site consumes@animxyz/coreand@animxyz/vue3via workspace*. Any brief that toucheddocs/now targetssite/. - commitlint still enforced (root
commit-msg+lint-staged, now also linting.astro). Conventional-commit rule stands.
Base branch: master. (.changeset/config.json baseBranch was corrected to
master, matching the repo default, so changeset status/version detect changed
packages correctly. All briefs base off master post-#123.)
A1 (test infra) ──► A2 (xyz-apply) ──► merge to master
└──► A3 (build hygiene) ──► merge to master
A4 (animationcancel fix, independent) ──► merge to master
A5 (vue wrapper fixes, independent) ──► merge to master
A6 (react wrapper fixes, independent) ──► merge to master
│
master (with A1–A3) ──► create/rebase v1 ─────┘
v1 ──► B1 (data-xyz, core+wrappers+docs) [independent]
v1 ──► B2 (@property) [independent]
v1 ──► B3 (@layer) ──► B4 (sibling-index + cleanup) [B4 needs B3 merged]
B1+B2+B3+B4 merged ──► B5 (docs site + migration guide + release prep)
A1 must merge first — every other brief's acceptance criteria reference its test harness. B1/B2/B3 are mutually independent and can run in parallel; expect small snapshot-fixture merge conflicts when they land (resolution: regenerate the snapshot, review the diff).
Driver: how much of the work is subtle, correctness-critical reasoning (Opus) vs. well-specified scaffolding or mechanical sweeps (Sonnet). Everything in Track A/B below ships in published packages, so correctness-dense briefs get Opus.
| Brief | Model | Effort | Rationale |
|---|---|---|---|
| A1 · test-infra | Sonnet | high | Net-new harness/CI/snapshot, but the design is spelled out; judgment on normalization/thresholds, not deep reasoning. |
| A2 · xyz-apply | Opus | high | Parser rewrite whose original bugs are substring collisions (thin/origin, up/flip-up); easy to pass the happy path and reintroduce them. |
| A3 · build-hygiene | Sonnet | high | Mostly mechanical; the one trap is preserving !default overridability during the time-map dedup. |
| A4 · animation-cancel | Opus | high | Async lifecycle + browser-quirk reasoning (cancel event never fired, worst-case timeout bound, numeric-string coercion); a wrong fallback reintroduces the hang. |
| A5 · vue-wrappers | Opus | high | Three framework-internals bugs across Vue 2/3 merge semantics; easy to "fix" listener composition by clobbering the internal hook again. |
| A6 · react-wrapper | Opus | high | Ref composition (object + callback) and Fragment edge case, repro-first; genuine React reasoning. |
| B1 · data-xyz | Sonnet | high | ~370-occurrence sweep is breadth not depth; the one hard part (dynamic sandbox attribute generation) is well-scoped in the brief. |
| B2 · @property | Opus | xhigh | Densest reasoning in the plan: per-var syntax, initial-value independence, inherits: true invariant, level tables that must not fail to parse. Most silent failure modes → most headroom. |
| B3 · @layer | Opus | high | Cascade architecture; remove !important without regressing reduced-motion precedence. Systemic reasoning across the whole output. |
| B4 · sibling-index | Opus | high | @supports always-true trap + specificity/precedence trap dependent on B3's layers; small surface, every step is a trap. |
| B5 · docs-release | Sonnet | medium | Editorial review + Changesets orchestration; low reasoning depth (bump to high only for the browser-support matrix). |
Shared rules for every brief:
- Branch names as given; worktree per branch; conventional commits (repo uses commitlint).
- Add a changeset (
npx changeset) in every PR: Track A =patch/minor, Track B =major. - "Snapshot diff reviewed" means: regenerate the CSS snapshot, and the PR description must summarize what changed in the output and why every change is intended.
- Never edit
dist/by hand; it is build output. - The repo is an npm-workspaces + Turborepo monorepo (post-#123). Install at the
root with
npm install; run a package's scripts from its dir or vianpm run <script> -w <workspace>/turbo run <task>.
Agent: Sonnet / high effort.
Goal: a safety net that catches compile errors, output regressions, and size creep.
Build:
packages/core/test/with a Node test runner (plainnode:testis fine — avoid adding heavy frameworks):- Compile test:
sass build.scsscompiles without errors or deprecation warnings (fail onDEPRECATION, which currently fires — A3 fixes it; land A1 with the deprecation check markedtodo/allowed-fail until A3 merges, then flip it). - Mixin API tests: small fixture
.scssfiles that@usethe source and invokexyz-utility,xyz-var,xyz-set-all,xyz-animation,xyz-make-keyframes— assert they compile and spot-check emitted declarations with string matching. Include a fixture forxyz-applyasserting the CURRENT failure ("all is not a valid level"), marked as a known-bug test for A2 to flip to a success assertion. - Snapshot test: compile
build.scss(expanded), normalize (strip comments, collapse whitespace), commit astest/__snapshots__/animxyz.css. Test recompiles and diffs; nonzero diff fails with instructions to regenerate via atest:update-snapshotnpm script. - Size budget: gzipped size of minified build must stay under a threshold (current: ~13.2 KB gzip for expanded; measure the actual min.css gzip and set budget = measured + 10%).
- Compile test:
- Wire it into the toolchain: add a
testscript topackages/core/package.json(turbo'stesttask alreadydependsOn: ["^build"], so the snapshot compiles against fresh output).turbo run testfrom the root must run it. .github/workflows/ci.yml(net-new — no workflow exists yet, even onastro-refactor):npm ci, thenturbo run lint teston push/PR tomasterandv1. Node 22 (current Active LTS; the core build's cssnano 8 requires Node >= 22.11),actions/setup-nodewithcache: npm. No publishing (release is a separate changeset flow).
Toolchain note: base off post-#123 master (npm + Turborepo). Use npm, not
yarn, throughout. Add a patch changeset (test infra is not user-facing but keeps
the changeset habit consistent, or use npx changeset --empty).
Acceptance: turbo run test (and npm test -w @animxyz/core) green locally and
in Actions; snapshot committed; README note in packages/core on running/updating
tests.
Agent: Opus / high effort.
Goal: xyz-apply('fade up-100% in-rotate-right') works.
The bugs (all in packages/core/src/_functions.scss, mixin at line 137):
- Line 169 calls
xyz-utility($utility-name, $utility-level, $utility-mode)but the signature (line 76) isxyz-utility($name, $mode, $level)— level and mode are swapped, so every invocation errors. - Mode detection uses
string.index($utility, $mode)substring matching:thinandoriginboth containinand would be mis-assigned modeinonce bug 1 is fixed. - Utility-name detection loops all map keys keeping the LAST substring match — only
correct today by accident of map ordering (
upappears beforeflip-up).
Fix approach: rewrite the token parser deterministically. For each
space-separated token: (a) check for an exact mode prefix in-/out-/appear- and
strip it; (b) match the remainder against utility names by exact match first, then
longest-prefix match where the next char is -; (c) the rest after name- is the
level, else 'default'. Keep @error messages listing valid names/levels. Use
string.split only if A3 (which bumps the sass floor) has merged — otherwise keep
xyz-str-split.
Also: flip A1's known-bug fixture to positive assertions covering: bare utility,
leveled, mode-prefixed, mode+level, thin/origin (regression for bug 2),
flip-up vs up (regression for bug 3), and an invalid token asserting the @error.
Add a short "Using with Sass" docs section for xyz-apply as a new markdown file in
site/src/content/sections/ (Astro content collection — model it on the existing
utilities.md/variables.md, and register it in whatever section index/ordering the
site uses) with a copy-paste example.
Acceptance: all new tests green; snapshot unchanged (this mixin emits nothing unless called); docs entry renders.
Agent: Sonnet / high effort.
Goal: remove the 2020-era build scaffolding that has no runtime effect.
packages/core/build.scss:@import 'src/animxyz';→@use 'src/animxyz' as *;. Kills the deprecation warning; flip A1's deprecation check to enforcing.- Drop postcss-calc from
build:postcssinpackages/core/package.jsonand remove the--xyz-*-calcshim variables in_functions.scss(lines ~190–205,TODOcomments reference postcss-calc bug #77, fixed upstream years ago). Write thecalc()expressions directly into--xyz-stagger-delay,--xyz-total-delay, andanimation-delay. These shim names are undocumented internals — removing them is patch-safe, but call it out in the changelog. xyz-str-split→string.split()(Dart Sass ≥ 1.57). Bump thesassdevDependency and note the floor inpackages/core/README.mdfor source consumers.- Dedup the time maps: introduce
$xyz-time-levelsand derive$xyz-duration-levels/$xyz-delay-levels/$xyz-stagger-levelsfrom it while keeping all three public names and!defaultoverridability (users who override one of the three today must see identical behavior — add a mixin-API test proving an override of$xyz-delay-levelsstill works). - Keep
xyz-include-once(harmless, still guards double-inclusion via multiple@use ... withentry points).
Out of scope here: backface-visibility, perspective hack, [xyz] reset — those
move with the v1.0 briefs.
Acceptance: zero sass warnings; snapshot diff shows ONLY the -calc variable
removal; size budget still green; time-map override test green.
Agent: Opus / high effort.
Goal: the framework wrappers' animation-done hook survives cancelled animations. (Found by external review; verified. JS bug, not SCSS — independent of A1–A3.)
The bug (utils/getXyzAnimationHook.js, shared by the Vue/Vue3/React packages):
lines 12 and 77 use the event name animationcancelled. The DOM event is
animationcancel — the listener has never fired in any browser.
Impact: completion normally arrives via animationend, but a cancelled
animation (element hidden, animation-name overridden, xyz-none applied
mid-flight) never fires animationend. The numeric-duration path recovers via its
setTimeout, but the default 'auto' path has NO timeout fallback — done() is
never called and the wrapper transition hangs (Vue <XyzTransition> stuck in its
transition state, React equivalent likewise).
Fix:
- Rename both usages to
animationcancel. 1b. While in this file's orbit: hardenutils/getXyzDurationForMode.jsto coerce numeric strings —duration="500"in a Vue template (nov-bind) is a string and silently takes the event path instead of a 500 ms timeout. Coerce withparseFloatwhen the string is fully numeric; keep'auto'and mode-object behavior unchanged. - Do NOT rely on the event alone: Chromium did not fire
animationcancelat all for many years. Add a safety-net timeout to the'auto'path — e.g. compute a worst-case bound from the elements' computedanimation-duration+animation-delayonce animations are running, falling back to a generous constant — sodone()always eventually fires even where cancel events are missing. - Verify manually in
examples/vue(or vue3): toggle an<XyzTransition>and force-cancel mid-animation (setdisplay:noneor addxyz-nonevia devtools); confirm the transition completes instead of hanging. Note the verification in the PR.
Acceptance: both event names corrected; auto-path timeout in place; numeric-string durations take the timeout path; manual cancel scenario verified in at least one wrapper example; no change to compiled CSS (snapshot untouched).
Agent: Opus / high effort.
Goal: fix three verified bugs in the Vue packages plus per-package hygiene. All patch-safe. Independent of A1–A4 (no SCSS, no shared-utils overlap with A4).
- Vue 2 reverse-stagger off-by-N (
packages/vue/src/components/XyzTransitionGroup.js:31):--xyz-index-revis computed from the UNFILTEREDchildren.lengthwhile the forward index iterates the filteredrawChildren. Any filtered node (text/ whitespace,v-ifcomment placeholder, unkeyed node) skews every reverse index. Fix:rawChildren.length - index - 1(matching the Vue 3 implementation). - Vue 3 listener clobbering (
packages/vue3/src/utils.js:29–39, 68):mergeDatais a plain object spread, so a user's@enter/@leave/@appearon<XyzTransition>REPLACES the internal animation hook (which ownsduration="auto", nested end-detection, andappearVisible). Vue 2 composes handlers viavue-functional-data-merge. Fix: compose same-namedon*handlers inmergeData(call both; the internal hook owns thedonecallback). Also fix the style merge to normalize string/arraystylevalues before spreading (a child withstyle="color: red"currently gets its style destroyed by{...'color: red'}). - Vue 3 undeclared internal dependency
(
packages/vue3/src/components/XyzTransitionGroup.js:2): importsgetTransitionRawChildrenfrom@vue/runtime-core, which is (a) not independencies— it resolves today only because npm hoists Vue's transitive copy, and breaks for any consumer whose layout doesn't hoist it — and (b) an undocumented internal export Vue can move without notice. Fix: vendor a small local filter (the Vue 2 package already has one to model) and drop the import. - Hygiene: real
index.d.tstypings for both packages (both are currently one-linedeclare modulestubs — every consumer getsany); addunbind/unmountedcleanup calling the hook's clear routine soappearVisibleIntersectionObservers and pending timeouts don't outlive unmounted elements (coordinate the small export needed fromutils/getXyzAnimationHook.jswith A4 — rebase whichever lands second).
Verification: in examples/vue and examples/vue3: a keyed list containing a
v-if placeholder shows correct stagger-rev ordering; a <XyzTransition @enter>
user handler fires AND duration="auto" still completes. The Vue 3 fixes are also
exercised by the docs site (site/ consumes @animxyz/vue3) — sanity-check
npm run dev -w @animxyz/site renders transitions without console errors. Note
results in the PR.
Acceptance: fixes in both packages; typings compile against the examples' usage; no compiled-CSS change.
Agent: Opus / high effort.
Goal: the React wrapper survives callback refs and empty children.
- Callback refs crash the end listener
(
packages/react/src/components/XyzTransitionBase.js:20,utils.js:33):child.ref || fallbackRefassumes an object ref. A callback ref makesnodeRefa function:CSSTransitionreceives an invalidnodeRefandaddEndListenerinvokes the animation hook withnodeRef.current === undefined, which throws inclearXyzElementProperties. Fix: always pass the internal ref object toCSSTransition, and compose it with the child's ref (handle object AND callback forms) when cloning. - Empty-state
Fragmentpath (XyzTransition.js:14+XyzTransitionBase.js:29): with no child,<Fragment/>flows intocloneElement(child, { xyz, className, style, ref })— Fragments accept only key/children, so React warns, the ref never attaches, and the end listener fires the hook with a null element (same TypeError as #1). FIRST STEP: build a repro (toggle<XyzTransition>to empty undermode="out-in") to confirm severity, then fix — likely by rendering a real empty element (or guarding the hook against a missing node AND skipping prop injection for Fragments). GuardanimationHookagainst null element regardless (defense in depth; also covers #1). - Hygiene: real
index.d.tstypings (currently a one-line stub); clear observer/timeout ononExited/unmount (coordinate with A4/A5 as above).
Verification: repro from #2 recorded in the PR (before/after); a child with a
callback ref transitions without throwing; examples/react runs clean with no
console warnings from the wrapper.
Acceptance: no crashes or React warnings across the ref/empty scenarios; typings compile; no compiled-CSS change.
Setup (whoever starts first): git branch v1 master && git push -u origin v1
after A1–A3 have merged to master.
Agent: Sonnet / high effort. (Consider handing the dynamic sandbox-attribute tracing in step 4 to Opus if run as a sub-task.)
Goal: the configuration attribute becomes standards-conforming data-xyz.
Clean break — v1.0 CSS matches [data-xyz~='…'] only. Selector count stays ~1,441.
- Core: add
$xyz-attribute: 'data-xyz' !default;in_variables.scssand use it everywhere selectors/attributes are generated:xyz-make-utilitiesandxyz-make-keyframes(_functions.scss:98–135), and the[xyz]reset +[xyz~='inherit']blocks (_core.scss:158–165). The var is the escape hatch for users who want to build with the legacy attribute. - Vue + Vue3 (
packages/vue{,3}/src/directives/xyz.js):setAttribute('data-xyz', …)andgetAttribute('data-xyz'). The directive staysv-xyz. Checkpackages/vue{,3}/src/components/and the shared rootutils/for any other attribute reads/writes. - React (
packages/react/src/components/): thexyzprop currently passes through as a bare attribute; map propxyz→ rendereddata-xyzattribute. Keep thexyzprop name (it's ergonomic and now legal since it renders data-*). Updateindex.d.tstypings in all three wrapper packages if they reference the attribute. - Docs site (
site/) + examples: migrate everyxyz="…"usage. Post-#123 counts (verified onastro-refactor): ~321 occurrences insite/— 247 insite/src/content/sections/*.md, 83 in*.vue, 18 in*.astro— plus 47 inexamples/. This is NOT a blind find/replace:- Some
xyz="…"are live demo attributes; others are attribute strings inside displayed code samples (both should readdata-xyzfor a consistent v1 story, but confirm you're not breaking a code sample that's deliberately showing legacy). - Interactive sandbox components build the attribute string dynamically —
site/src/components/vue/reusable/Sandbox.vue,XyzModifiersInput.vue,XyzModifiersPresets.vue(and thev-xyzdirective usages likev-xyz="data.utilities"). These need code changes to emitdata-xyz, not text swaps. Thev-xyzdirective NAME stays; what it writes is fixed in core/wrapper (steps 1–3). Trace the sandbox's generated-markup path end to end. - Site's own SCSS hooks the attribute:
site/src/styles/core/_animations.scsslines ~22 ([xyz] {) and ~51 ([xyz='']:hover::after) must become[data-xyz…]. (Note: a[xyzUtility]match inBannerSquare.vueis JS array destructuring — not an attribute selector, leave it.)
- Some
- Migration guide:
MIGRATION-v1.mdat repo root — find/replace guidance (xyz="→data-xyz="), wrapper behavior notes, and the$xyz-attributeescape hatch for Sass users.
Gotchas:
- A5/A6 edit the same wrapper files (directives, XyzTransitionBase) — make sure
v1has been rebased on master with A5/A6 merged before starting, or coordinate. - The docs render via
@animxyz/vue3(Astro Vue islands), so the site is a live integration test for the attribute change — build it and click through, don't just grep-and-commit.
Acceptance: core snapshot regenerated (every selector renamed — diff should be
mechanical and complete: zero remaining [xyz selectors in the compiled CSS);
site/ builds (npm run build -w @animxyz/site) and demos animate; wrapper
examples in examples/ run against the rebuilt core CSS; tests green.
Agent: Opus / xhigh effort (most invariants to hold simultaneously; most silent failure modes).
Goal: register the dial variables for type safety and individually transitionable custom properties. Browser floor becomes Baseline July 2024 (document in README).
Constraints discovered during analysis — read carefully:
inherits: trueon EVERY registered property. Nested composition (.xyz-nestedreading a parent's dials) and the stagger relay depend on inheritance.inherits: falsesilently breaks the library.@property initial-valuemust be computationally independent — it CANNOT bevar(--xyz-…-default). Therefore the--xyz-*-defaulttheming variables and their:rootblock STAY (they are tiers 3–4 of the fallback chain and the documented theming API). Register the per-element dial vars (--xyz-translate-x,--xyz-in-opacity, …) with type + identity initial-value; since an unset registered property now resolves to its typed initial value, audit whether the identity fallbacks written inside the keyframes/xyz-var()chains can be simplified — but keepxyz-var()'s default-var tiers intact.- Composite/freeform vars (
--xyz-keyframes,--xyz-transform, and anything whose value is itself a var() composition) getsyntax: "*"or stay unregistered —"*"cannot have an initial-value requirement issue but also gets no typing; prefer unregistered where registration buys nothing. - Suggested types:
<time>(duration/delay/stagger),<number>(opacity, scale, iterate is tricky —infiniteis a keyword, use"*"),<length-percentage>(translate x/y),<length>(translate-z, perspective),<angle>(rotate, skew). Note translate/rotate levels include%/turnvalues — verify each level table against the chosen syntax; a registered property whose set value fails to parse behaves like unset (that's the type safety, but it must not reject legit levels). - Kill the
--xyz-perspective-none@supportshack (_core.scss:42–46):perspective(none)is universally supported at this browser floor; use it directly as the identity value. - Generate registrations from
$xyz-variables-map(add asyntaxkey per entry) rather than hand-writing ~96@propertyblocks.
Acceptance: snapshot diff = added @property blocks + removed perspective hack
(and any agreed fallback simplifications); a new fixture test setting a garbage value
(e.g. --xyz-duration: red) documents the typed-reject behavior; size budget
adjusted if needed (registrations add bytes — measure and update with justification).
Agent: Opus / high effort.
Goal: cascade correctness by declaration, not source order or !important.
- Layer plan (single top-level
xyzlayer with sublayers, declared up front):@layer xyz.defaults, xyz.index, xyz.utilities, xyz.triggers.in, xyz.triggers.out, xyz.triggers.appear, xyz.overrides; - Map existing output:
:rootdefaults +@property→defaults; nth-child ladder →index;[data-xyz~=…]selectors →utilities; mode trigger classes →triggers.<mode>(the sublayer ORDER now guarantees appear beats in/out — remove the "Appear must come last" comment/constraint in$xyz-modes,_variables.scss:6); paused/none/absolute/reduced-motion →overrides, DROPPING their!importants (_core.scss:130–142). - Wrap emission via the existing mixins so Sass users including
xyz-core/xyz-utilitiesget the same layers. Provide$xyz-layer: 'xyz' !default(empty string = emit unlayered, the escape hatch). - Document the new override contract in README: user CSS (unlayered) now beats
AnimXYZ by default; to lose to AnimXYZ deliberately, put styles in a layer
declared before
xyz.
Gotchas: @keyframes and @property at-rules are fine inside layers but
name-collision semantics don't change — keep them in defaults. Verify
prefers-reduced-motion override still wins against utilities/triggers from its
layer position (it currently relies on being late + same specificity).
Acceptance: compiled CSS contains zero !important; reorder test — a fixture
that flips $xyz-modes order compiles to identical behavior (snapshot proves layer
order, not source order, decides); docs updated; snapshot diff reviewed.
Agent: Opus / high effort.
Goal: sibling-index() where supported; ladder fallback elsewhere; misc cleanup.
- Feature-detect correctly:
@supports (--x: sibling-index())is ALWAYS true — custom property declarations accept any value. Test against a real property instead:@supports (animation-delay: calc(1s * (sibling-index() - 1))). - Inside the
@supports, set--xyz-index: calc(sibling-index() - 1)and--xyz-index-rev: calc(sibling-count() - sibling-index())on the same targets that use%xyz-indexed(_core.scss:54–63). Precedence trap: the nth-child ladder rules have higher specificity than a bare class selector — place the enhancement in a later sublayer (e.g.xyz.index.modernafterxyz.index.ladder; requires B3's layer scaffolding) so it wins regardless of specificity. - Keep
$xyz-index-levels: 20ladder as fallback; add a$xyz-index-levels: 0option that skips ladder emission entirely for Chromium-only consumers. - Leftover cleanups: remove
backface-visibility: visible(_functions.scss:197— it sets the initial value; breaking only for users who relied on the library re-forcing it, hence v1 not 0.x); dedup the per-axis boilerplate in$xyz-variables-map/$xyz-utilities-mapwith a small generator loop (pure refactor, snapshot must be byte-identical for that commit).
Acceptance: fixture page with 30 siblings staggers correctly in Chromium
(manual verification note in PR with the compiled fixture attached); snapshot diff =
@supports block + removed backface-visibility only; refactor commit is
snapshot-neutral.
Agent: Sonnet / medium effort (bump to high for the browser-support matrix).
Goal: v1.0 is explainable and shippable.
- Docs site: browser-support section (Baseline 2024 floor, sibling-index
enhancement matrix),
@layeroverride contract,xyz-applydocs (from A2) verified against v1,data-xyzeverywhere (B1 did the mechanical sweep; this brief does editorial review). Add a note thatXyzTransitionGroup(all frameworks) sets--xyz-indexinline, so the CSS sibling-index/ladder cap does not apply when using the wrapper components. - Finish
MIGRATION-v1.md: data-xyz, browser floor, @layer override behavior change, removed!importants, removed-calcvars,backface-visibility,$xyz-attribute/$xyz-layerescape hatches. - Release via Changesets, not Lerna. Ensure a
majorchangeset exists for each published package taken to v1.0.0 (@animxyz/core,@animxyz/vue,@animxyz/vue3,@animxyz/react);@animxyz/siteis ignored/unpublished.updateInternalDependencies: patchhandles wrapper→core version bumps. Runnpx changeset versionon a release branch and review the generated CHANGELOGs (Changesets writes them — don't hand-author). Regenerate size stats (buildStats.js) and update any size claims in READMEs andsite/content. - Update
animxyz-scss-analysis.html's Issues section footnotes if kept in-repo (optional: mark resolved items). Note its "76 doc examples" figure is pre-#123 and now ~321 insite/.
Acceptance: docs build passes; every item in the decisions record is either shipped or explicitly listed in the backlog below.
_fancy.scss: stays orphaned by decision. Revisit after v1.0.@starting-style+transition-behavior: allow-discreteenter/exit mode (no-JS mount/unmount animation) — strongest post-v1 feature candidate.- Scroll-driven animation utilities (
animation-timeline: view()/scroll()). - Typed
attr()(Chromium-only): reading levels straight from attributes could eventually delete most generated selectors. Watch cross-engine progress. - Browser visual-regression tests (Playwright) if the compile/snapshot net proves insufficient.