transitions-refine 0.3.32 → 0.3.34

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.
Files changed (2) hide show
  1. package/demo.html +167 -34
  2. package/package.json +1 -1
package/demo.html CHANGED
@@ -2383,6 +2383,7 @@
2383
2383
  for(const n of Object.keys(rec.prevVars||{})){try{const pv=rec.prevVars[n];if(pv)el.style.setProperty(n,pv);else el.style.removeProperty(n);}catch{}}
2384
2384
  }
2385
2385
  store.applied&&store.applied.clear();
2386
+ try{tlSyncNoneGuard(TL_LP_GUARD_PHASE_ID,null);}catch{}
2386
2387
  }
2387
2388
 
2388
2389
  // ── registry (per-lane overrides) ──
@@ -2601,16 +2602,53 @@
2601
2602
  // translateVarName?}. A var-backed blur is reported even at 0px — the var is
2602
2603
  // how an ADDED blur (recipe lane the source lacks) goes live. Only meaningful
2603
2604
  // for transform/filter lanes.
2604
- function readElValueHint(el, property){
2605
+ // Direction regex for a phase — used to disambiguate an element that carries
2606
+ // MORE THAN ONE var-backed value across its state rules (e.g. a menu dropdown
2607
+ // whose base rests at scale(var(--dropdown-pre-scale)) but whose transient
2608
+ // `.is-closing` rests at scale(var(--dropdown-closing-scale))). Open and close
2609
+ // are the SAME element, so element-only var discovery would collapse both
2610
+ // phases onto whichever scale var it happened to pick first — writing e.g.
2611
+ // close's 0.99 into --dropdown-pre-scale and never touching the close var.
2612
+ // The phase's own direction (open vs close, from its name/state tokens) lets
2613
+ // us prefer the candidate whose CSS state-class matches that direction.
2614
+ function phaseDirRe(ph){
2615
+ if(!ph)return null;
2616
+ const t=((ph.phase||"")+" "+(ph.toState||"")+" "+(ph.fromState||"")).toLowerCase();
2617
+ // check CLOSE first: a close phase's fromState is often the OPEN state, so
2618
+ // its token string contains "open" too — the closing intent must win.
2619
+ if(/clos|exit|hide|collaps|dismiss|leave|out\b/.test(t))return /clos|exit|hide|collaps|dismiss|leave|out/;
2620
+ if(/open|reveal|show|enter|expand|\bin\b/.test(t))return /open|reveal|show|enter|expand/;
2621
+ return null;
2622
+ }
2623
+ // phaseHint (optional): {phase,toState,fromState} — makes var discovery
2624
+ // direction-aware so each phase of a grouped open/close binds to ITS OWN
2625
+ // backing var. Absent → the classic "furthest-from-rest, any state" behavior.
2626
+ function readElValueHint(el, property, phaseHint){
2605
2627
  const out={};
2606
2628
  try{
2607
2629
  if(!el||(property!=="transform"&&property!=="filter"))return out;
2608
2630
  const own=(typeof el.className==="string"?el.className.trim().split(/\s+/):Array.from(el.classList||[])).filter(Boolean);
2631
+ const ownSet=new Set(own);
2609
2632
  const cs=getComputedStyle(el);
2610
2633
  const esc=s=>s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");
2611
2634
  const tokRes=own.map(c=>new RegExp("\\."+esc(c)+"(?![\\w-])"));
2612
2635
  const matchTok=sel=>!own.length||tokRes.some(re=>re.test(sel));
2613
- let bestScale=null,bestBlur=null,bestTranslate=null;
2636
+ // state classes a rule's subject compound carries beyond the element's own
2637
+ // classes (e.g. `.t-dropdown.is-closing` → ["is-closing"] for a #dropdown
2638
+ // whose own classes are t-dropdown/dropdown). Empty → a base/stateless rule.
2639
+ const extraStateClasses=sel=>{
2640
+ for(const part of String(sel).split(",")){
2641
+ const s=part.trim();if(!s)continue;
2642
+ const compounds=s.split(/\s*[>+~]\s*|\s+/).filter(Boolean);
2643
+ let subject=null;
2644
+ for(let i=compounds.length-1;i>=0;i--){if(tokRes.some(re=>re.test(compounds[i]))){subject=compounds[i];break;}}
2645
+ if(!subject)continue;
2646
+ return (subject.match(/\.[-\w]+/g)||[]).map(c=>c.slice(1)).filter(c=>!ownSet.has(c));
2647
+ }
2648
+ return [];
2649
+ };
2650
+ // candidate lists — one entry per matching rule that declares the value.
2651
+ const scaleCands=[],blurCands=[],translateCands=[];
2614
2652
  // custom-property definitions seen on matching rules — lets a var chain
2615
2653
  // resolve to its token var (--t-page-from-x: calc(var(--page-slide-distance)
2616
2654
  // * -1) → --page-slide-distance), which is the var shared across members.
@@ -2619,59 +2657,138 @@
2619
2657
  const rootVar=(name)=>{let n=name,guard=0;
2620
2658
  while(guard++<4){const def=varDefs.get(n);const nm=def&&def.match(/var\(\s*(--[\w-]+)/);if(!nm)break;n=nm[1];}
2621
2659
  return n;};
2622
- const texts=[];
2660
+ const rules=[];
2623
2661
  const collect=list=>{for(const rule of Array.from(list||[])){
2624
2662
  if(rule.cssRules&&rule.selectorText===undefined){collect(rule.cssRules);continue;}
2625
2663
  const sel=rule.selectorText;if(!sel||!matchTok(sel))continue;
2626
- if(rule.cssText)texts.push(rule.cssText);}};
2664
+ if(rule.cssText)rules.push({sel,txt:rule.cssText});}};
2627
2665
  for(const sheet of Array.from(document.styleSheets||[])){let r;try{r=sheet.cssRules;}catch{continue;}collect(r);}
2628
- for(const txt of texts){const defRe=/(--[\w-]+)\s*:\s*([^;}]+)/g;let dm;
2666
+ for(const{txt} of rules){const defRe=/(--[\w-]+)\s*:\s*([^;}]+)/g;let dm;
2629
2667
  while((dm=defRe.exec(txt)))if(!varDefs.has(dm[1]))varDefs.set(dm[1],dm[2].trim());}
2630
- for(const txt of texts){
2668
+ for(const{sel,txt} of rules){
2669
+ const st=extraStateClasses(sel);
2631
2670
  if(property==="transform"){
2632
2671
  const tm=txt.match(/transform\s*:\s*([^;}]+)/i);
2633
2672
  if(tm){const sc=tm[1].match(/scale\(\s*([^),]+)/i);
2634
2673
  if(sc){const inner=sc[1].trim();const vm=inner.match(/var\(\s*(--[\w-]+)/);
2635
2674
  const num=vm?parseFloat(cs.getPropertyValue(vm[1])):parseFloat(inner);
2636
- // pick the value furthest from the resting 1 (the real pre-scale);
2637
- // a var-backed scale at rest (1) still reports its var — the hook
2638
- // for ADDING a start-scale the source neutralized (e.g. icon swap).
2639
- if(Number.isFinite(num)&&(Math.abs(num-1)>1e-4||vm)){
2640
- const better=!bestScale||Math.abs(num-1)>Math.abs(bestScale.scale-1)||(bestScale.varName==null&&vm);
2641
- if(better)bestScale={scale:num,...(vm?{varName:vm[1]}:{})};}}
2675
+ // a var-backed scale at rest (1) still counts the hook for ADDING
2676
+ // a start-scale the source neutralized (e.g. icon swap).
2677
+ if(Number.isFinite(num)&&(Math.abs(num-1)>1e-4||vm))
2678
+ scaleCands.push({scale:num,st,...(vm?{varName:vm[1]}:{})});}
2642
2679
  const tr=tm[1].match(/translate(?:X|Y|3d)?\(\s*([^),]+)/i);
2643
2680
  if(tr){const inner=tr[1].trim();const vm=inner.match(/var\(\s*(--[\w-]+)/);
2644
2681
  const vn=vm?rootVar(vm[1]):null;
2645
2682
  // distance is a magnitude — direction lives in the CSS (± calc).
2646
2683
  const num=Math.abs(vn?parseFloat(cs.getPropertyValue(vn)):parseFloat(inner));
2647
- // a var-backed translate at rest (0) still reports its var the
2648
- // hook for ADDING a slide distance the source neutralized (e.g. a
2649
- // generic text swap with --text-swap-translate-y: 0).
2650
- if(Number.isFinite(num)&&(num>1e-4||vn)){
2651
- const better=!bestTranslate||num>bestTranslate.translate||(bestTranslate.translateVarName==null&&vn);
2652
- if(better)bestTranslate={translate:num,...(vn?{translateVarName:vn}:{})};}}}
2684
+ // a var-backed translate at rest (0) still counts the hook for
2685
+ // ADDING a slide the source neutralized (e.g. a generic text swap).
2686
+ if(Number.isFinite(num)&&(num>1e-4||vn))
2687
+ translateCands.push({translate:num,st,...(vn?{translateVarName:vn}:{})});}}
2653
2688
  }else{
2654
2689
  const fm=txt.match(/filter\s*:\s*([^;}]+)/i);
2655
2690
  if(fm){const bl=fm[1].match(/blur\(\s*([^)]+)\)/i);
2656
2691
  if(bl){const inner=bl[1].trim();const vm=inner.match(/var\(\s*(--[\w-]+)/);
2657
2692
  const num=vm?parseFloat(cs.getPropertyValue(vm[1])):parseFloat(inner);
2658
- // var-backed 0px still reports the var (the hook for ADDING blur).
2659
- if(Number.isFinite(num)&&(num>1e-4||vm)&&(!bestBlur||num>bestBlur.blur))
2660
- bestBlur={blur:num,...(vm?{varName:vm[1]}:{})};}}
2693
+ // var-backed 0px still counts (the hook for ADDING blur).
2694
+ if(Number.isFinite(num)&&(num>1e-4||vm))
2695
+ blurCands.push({blur:num,st,...(vm?{varName:vm[1]}:{})});}}
2696
+ }
2697
+ }
2698
+ const dirRe=phaseDirRe(phaseHint);
2699
+ // Choose a candidate: furthest-from-rest wins, a var-backed one beats a
2700
+ // literal. `key` is the magnitude field (scale→|x-1|, blur/translate→x).
2701
+ const pick=(cands,restVal)=>{let best=null;for(const c of cands){
2702
+ const mag=restVal===1?Math.abs(c.scale-1):(c.blur!=null?c.blur:c.translate);
2703
+ const bmag=best==null?-1:(restVal===1?Math.abs(best.scale-1):(best.blur!=null?best.blur:best.translate));
2704
+ const cVar=c.varName||c.translateVarName, bVar=best&&(best.varName||best.translateVarName);
2705
+ if(best==null||mag>bmag||(!bVar&&cVar))best=c;}return best;};
2706
+ // Direction-aware selection (only when we know the phase): prefer a
2707
+ // candidate on a state rule matching the phase direction; else a
2708
+ // base/stateless rule; else fall back to the global best (classic path).
2709
+ const choose=(cands,restVal)=>{
2710
+ if(!cands.length)return null;
2711
+ if(phaseHint){
2712
+ if(dirRe){const m=cands.filter(c=>c.st.some(cl=>dirRe.test(cl)));if(m.length)return pick(m,restVal);}
2713
+ const base=cands.filter(c=>!c.st.length);if(base.length)return pick(base,restVal);
2714
+ }
2715
+ return pick(cands,restVal);
2716
+ };
2717
+ const bestScale=choose(scaleCands,1);
2718
+ const bestBlur=choose(blurCands,0);
2719
+ const bestTranslate=choose(translateCands,0);
2720
+ if(bestScale){const{st,...rest}=bestScale;Object.assign(out,rest);}
2721
+ if(bestBlur){const{st,...rest}=bestBlur;Object.assign(out,rest);}
2722
+ if(bestTranslate){const{st,...rest}=bestTranslate;Object.assign(out,rest);}
2723
+ }catch{}
2724
+ return out;
2725
+ }
2726
+ // ── FLIP "jump-to-start" guard ──────────────────────────────────────────
2727
+ // Many transitions teleport to a start offset before animating, using a
2728
+ // state class that sets `transition: none` (the FLIP pattern), e.g.
2729
+ // `.t-text-swap.is-enter-start { transform: translateY(...); transition: none }`.
2730
+ // Our live-preview mirrors edited timings as an INLINE `transition` on the
2731
+ // element; inline wins (specificity 1,0,0,0) over that class rule, so the
2732
+ // instant teleport becomes an animation — the entering content then eases
2733
+ // in from the EXIT position instead of its start offset, flipping the
2734
+ // direction. Fix generally: find those page-authored `transition: none`
2735
+ // start-states and re-assert them with `!important` so the teleport stays
2736
+ // instant regardless of our inline transition. We only match STATE variants
2737
+ // (a selector whose subject compound carries a class the element is NOT
2738
+ // currently wearing) so we never freeze an element's BASE transition — the
2739
+ // base is what carries the lanes we add. @media blocks (e.g. reduced-motion)
2740
+ // are skipped so we don't promote their `transition: none` to always-on.
2741
+ const _TL_NONE_CACHE=new WeakMap();
2742
+ function tlCollectNoneSelectors(el){
2743
+ let cached=_TL_NONE_CACHE.get(el); if(cached)return cached;
2744
+ const out=new Set();
2745
+ try{
2746
+ const own=(typeof el.className==="string"?el.className.trim().split(/\s+/):Array.from(el.classList||[])).filter(Boolean);
2747
+ if(own.length){
2748
+ const ownSet=new Set(own);
2749
+ const esc=s=>s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");
2750
+ const tokRes=own.map(c=>new RegExp("\\."+esc(c)+"(?![\\w-])"));
2751
+ for(const sheet of Array.from(document.styleSheets||[])){
2752
+ let rules;try{rules=sheet.cssRules;}catch{continue;}
2753
+ for(const rule of Array.from(rules||[])){
2754
+ if(rule.selectorText===undefined)continue; // skip @media / @supports etc.
2755
+ const tv=rule.style&&rule.style.getPropertyValue("transition");
2756
+ if(!tv||!/^\s*none\s*$/i.test(tv))continue;
2757
+ for(const part of rule.selectorText.split(",")){
2758
+ const s=part.trim(); if(!s)continue;
2759
+ // subject = rightmost compound that carries one of the element's tokens.
2760
+ const compounds=s.split(/\s*[>+~]\s*|\s+/).filter(Boolean);
2761
+ let subject=null;
2762
+ for(let i=compounds.length-1;i>=0;i--){ if(tokRes.some(re=>re.test(compounds[i]))){subject=compounds[i];break;} }
2763
+ if(!subject)continue;
2764
+ const subjClasses=(subject.match(/\.[-\w]+/g)||[]).map(c=>c.slice(1));
2765
+ // require a state class the element isn't currently wearing.
2766
+ if(subjClasses.some(c=>!ownSet.has(c)))out.add(s);
2767
+ }
2768
+ }
2661
2769
  }
2662
2770
  }
2663
- if(bestScale)Object.assign(out,bestScale);
2664
- if(bestBlur)Object.assign(out,bestBlur);
2665
- if(bestTranslate)Object.assign(out,bestTranslate);
2666
2771
  }catch{}
2772
+ _TL_NONE_CACHE.set(el,out);
2667
2773
  return out;
2668
2774
  }
2775
+ function tlSyncNoneGuard(id,selectors){
2776
+ try{
2777
+ let st=document.getElementById(id);
2778
+ if(!selectors||!selectors.size){ if(st)st.remove(); return; }
2779
+ if(!st){ st=document.createElement("style"); st.id=id; (document.head||document.documentElement).appendChild(st); }
2780
+ const css=Array.from(selectors).map(s=>s+"{transition:none !important;}").join("\n");
2781
+ if(st.textContent!==css)st.textContent=css;
2782
+ }catch{}
2783
+ }
2784
+ const TL_LP_GUARD_FLAT_ID="tl-live-none-guard-flat";
2785
+ const TL_LP_GUARD_PHASE_ID="tl-live-none-guard-phase";
2669
2786
  // Same, resolving the element from a CSS selector (used when only a lane's
2670
2787
  // selector is known, e.g. a grouped phase member).
2671
- function laneValueHint(selector, property){
2788
+ function laneValueHint(selector, property, phaseHint){
2672
2789
  if(!selector||(property!=="transform"&&property!=="filter"))return {};
2673
2790
  let el=null;try{el=document.querySelector(selector);}catch{return {};}
2674
- return el?readElValueHint(el,property):{};
2791
+ return el?readElValueHint(el,property,phaseHint):{};
2675
2792
  }
2676
2793
 
2677
2794
  // ── hooks ──
@@ -4802,7 +4919,10 @@
4802
4919
  refineCacheRef.current.delete(active.id+"::"+type);
4803
4920
  // attach the lane's non-resting scale/blur/translate value (+ backing var)
4804
4921
  // so the agent can refine transform/filter VALUES too.
4805
- const withVal=(t)=>{const v=laneValueHint(t.selector,t.property);return {property:t.property,member:t.member||null,durationMs:t.durationMs,delayMs:t.delayMs,easing:t.easing,
4922
+ // direction-aware per phase — see readElValueHint/phaseDirRe: a grouped
4923
+ // open/close shares one element, so each phase's value must be read with
4924
+ // ITS OWN state context or both collapse onto one backing var.
4925
+ const withVal=(phaseCtx)=>(t)=>{const v=laneValueHint(t.selector,t.property,phaseCtx);return {property:t.property,member:t.member||null,durationMs:t.durationMs,delayMs:t.delayMs,easing:t.easing,
4806
4926
  ...(v.scale!=null?{scale:v.scale}:{}),...(v.blur!=null?{blur:v.blur}:{}),...(v.varName?{varName:v.varName}:{}),
4807
4927
  ...(v.translate!=null?{translate:v.translate}:{}),...(v.translateVarName?{translateVarName:v.translateVarName}:{})};};
4808
4928
  try{
@@ -4823,12 +4943,12 @@
4823
4943
  const ret=rep.effectiveTimings||[];
4824
4944
  const sel=(ret[0]&&ret[0].selector)||null;
4825
4945
  const ph=gp.length>1?gp.map(x=>({phase:x.phase||x.phaseLabel||"phase",label:x.phaseLabel||x.phase||"Phase",
4826
- timings:(x.effectiveTimings||[]).map(withVal)})):undefined;
4827
- transitions.push({transitionId:rep.id,label:rep.groupLabel||rep.label,selector:sel,timings:ret.map(withVal),phases:ph});
4946
+ timings:(x.effectiveTimings||[]).map(withVal(x))})):undefined;
4947
+ transitions.push({transitionId:rep.id,label:rep.groupLabel||rep.label,selector:sel,timings:ret.map(withVal(rep)),phases:ph});
4828
4948
  }else{
4829
4949
  const fet=e.effectiveTimings||[];
4830
4950
  const sel=(e.bindings&&e.bindings.selector)||(fet[0]&&fet[0].selector)||null;
4831
- transitions.push({transitionId:e.id,label:e.label,selector:sel,timings:fet.map(withVal)});
4951
+ transitions.push({transitionId:e.id,label:e.label,selector:sel,timings:fet.map(withVal(e))});
4832
4952
  }
4833
4953
  }
4834
4954
  const{id}=await relayCreateJob({scope:"all",label:"All transitions",mode,refineType:"replace",transitions});
@@ -4836,7 +4956,7 @@
4836
4956
  return;
4837
4957
  }
4838
4958
  const et=active.effectiveTimings||[];
4839
- const timings=et.map(withVal);
4959
+ const timings=et.map(withVal(active));
4840
4960
  const selector=(active.bindings&&active.bindings.selector)||(et[0]&&et[0].selector)||null;
4841
4961
  // A recipe swap (replace) on a grouped phase should update its related
4842
4962
  // phases (open + close) together — they're one motion. Send the whole
@@ -4845,7 +4965,7 @@
4845
4965
  if(type==="replace"&&active.kind==="phase"&&active.groupId){
4846
4966
  const gp=entries.filter(e=>e.kind==="phase"&&e.groupId===active.groupId);
4847
4967
  if(gp.length>1)phases=gp.map(e=>({phase:e.phase||e.phaseLabel||"phase",label:e.phaseLabel||e.phase||"Phase",
4848
- timings:(e.effectiveTimings||[]).map(withVal)}));
4968
+ timings:(e.effectiveTimings||[]).map(withVal(e))}));
4849
4969
  }
4850
4970
  const{id}=await relayCreateJob({transitionId:active.id,label:active.label,selector,
4851
4971
  phase:active.phase||null,group:active.groupLabel||null,timings,mode,refineType:type,scope:"selected",phases});
@@ -5027,6 +5147,10 @@
5027
5147
  delayMs:p.delayMs??pt.delayMs??(base&&base.delayMs)??0,
5028
5148
  easing:p.easing??pt.easing??(base&&base.easing)??"ease",
5029
5149
  });
5150
+ // direction context so var discovery binds this patch to the phase's
5151
+ // OWN backing var (open→--dropdown-pre-scale, close→--dropdown-closing-scale)
5152
+ // even though open+close share one element.
5153
+ const phaseHint={phase:p.phase||target.phase,toState:target.toState,fromState:target.fromState};
5030
5154
  const applyValue=(prop,mk)=>{
5031
5155
  // mk(lane, hint) → override fields for one lane, or null to skip.
5032
5156
  let vLanes=lanes.filter(t=>t.property===prop);
@@ -5034,7 +5158,7 @@
5034
5158
  if(vLanes.length){
5035
5159
  const rt=recipeTiming();
5036
5160
  for(const lane of vLanes){
5037
- const hint=laneValueHint(lane.selector,prop);
5161
+ const hint=laneValueHint(lane.selector,prop,phaseHint);
5038
5162
  const ov=mk(lane,hint);
5039
5163
  // an EXISTING lane the recipe reuses (e.g. Page side-by-side's
5040
5164
  // filter lane at the source's 300ms) must ALSO adopt the recipe's
@@ -5047,7 +5171,7 @@
5047
5171
  // A new filter lane rides the transform (slide) timing per the skill.
5048
5172
  const sib=sibling(prop==="filter"?"transform":"opacity");
5049
5173
  if(!sib)return;
5050
- const hint=laneValueHint(sib.selector,prop);
5174
+ const hint=laneValueHint(sib.selector,prop,phaseHint);
5051
5175
  const ov=mk(null,hint);
5052
5176
  if(!ov)return;
5053
5177
  const laneId=(sib.memberId?sib.memberId+"::":"")+prop;
@@ -5147,12 +5271,17 @@
5147
5271
  _TX_LIVE_BASE.delete(el);
5148
5272
  applied.delete(el);
5149
5273
  }
5274
+ // keep FLIP jump-to-start states instant under our inline transition
5275
+ const guard=new Set();
5276
+ for(const el of desired.keys()){ for(const s of tlCollectNoneSelectors(el)) guard.add(s); }
5277
+ tlSyncNoneGuard(TL_LP_GUARD_FLAT_ID,guard);
5150
5278
  },[entries,registry]);
5151
5279
  // restore everything on unmount
5152
5280
  useEffect(()=>()=>{
5153
5281
  const applied=liveAppliedRef.current;
5154
5282
  for(const [el,rec] of applied){ try{el.style.transition=rec.prevInline;}catch{} for(const n of Object.keys(rec.vars||{})){try{const pv=(rec.prevVars||{})[n];if(pv)el.style.setProperty(n,pv);else el.style.removeProperty(n);}catch{}} _TX_LIVE_BASE.delete(el); }
5155
5283
  applied.clear();
5284
+ tlSyncNoneGuard(TL_LP_GUARD_FLAT_ID,null);
5156
5285
  },[]);
5157
5286
  // ── live styling: grouped phases (state-gated) ──────────────────────────
5158
5287
  // Open and close of one component share the SAME element(s), so a single
@@ -5277,6 +5406,10 @@
5277
5406
  for(const n of Object.keys(rec.prevVars||{})){try{const pv=rec.prevVars[n];if(pv)el.style.setProperty(n,pv);else el.style.removeProperty(n);}catch{}}
5278
5407
  store.applied.delete(el);
5279
5408
  }
5409
+ // keep FLIP jump-to-start states instant under our inline transition
5410
+ const guard=new Set();
5411
+ for(const el of desiredCss.keys()){ for(const s of tlCollectNoneSelectors(el)) guard.add(s); }
5412
+ tlSyncNoneGuard(TL_LP_GUARD_PHASE_ID,guard);
5280
5413
  };
5281
5414
  // Coalesce via microtask (NOT rAF): the gate must update the moment the
5282
5415
  // state class/attr flips — before the browser styles the next frame — so
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "transitions-refine",
3
- "version": "0.3.32",
3
+ "version": "0.3.34",
4
4
  "description": "Live, agent-driven Refine panel for CSS/Motion transitions — injects a timeline + Refine UI and runs transitions.dev suggestions via your coding agent.",
5
5
  "type": "module",
6
6
  "bin": {