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.
- package/demo.html +167 -34
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
-
//
|
|
2637
|
-
// a
|
|
2638
|
-
|
|
2639
|
-
|
|
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
|
|
2648
|
-
//
|
|
2649
|
-
|
|
2650
|
-
|
|
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
|
|
2659
|
-
if(Number.isFinite(num)&&(num>1e-4||vm)
|
|
2660
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|