transitions-refine 0.3.33 → 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 +97 -34
- package/package.json +1 -1
package/demo.html
CHANGED
|
@@ -2602,16 +2602,53 @@
|
|
|
2602
2602
|
// translateVarName?}. A var-backed blur is reported even at 0px — the var is
|
|
2603
2603
|
// how an ADDED blur (recipe lane the source lacks) goes live. Only meaningful
|
|
2604
2604
|
// for transform/filter lanes.
|
|
2605
|
-
|
|
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){
|
|
2606
2627
|
const out={};
|
|
2607
2628
|
try{
|
|
2608
2629
|
if(!el||(property!=="transform"&&property!=="filter"))return out;
|
|
2609
2630
|
const own=(typeof el.className==="string"?el.className.trim().split(/\s+/):Array.from(el.classList||[])).filter(Boolean);
|
|
2631
|
+
const ownSet=new Set(own);
|
|
2610
2632
|
const cs=getComputedStyle(el);
|
|
2611
2633
|
const esc=s=>s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");
|
|
2612
2634
|
const tokRes=own.map(c=>new RegExp("\\."+esc(c)+"(?![\\w-])"));
|
|
2613
2635
|
const matchTok=sel=>!own.length||tokRes.some(re=>re.test(sel));
|
|
2614
|
-
|
|
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=[];
|
|
2615
2652
|
// custom-property definitions seen on matching rules — lets a var chain
|
|
2616
2653
|
// resolve to its token var (--t-page-from-x: calc(var(--page-slide-distance)
|
|
2617
2654
|
// * -1) → --page-slide-distance), which is the var shared across members.
|
|
@@ -2620,50 +2657,69 @@
|
|
|
2620
2657
|
const rootVar=(name)=>{let n=name,guard=0;
|
|
2621
2658
|
while(guard++<4){const def=varDefs.get(n);const nm=def&&def.match(/var\(\s*(--[\w-]+)/);if(!nm)break;n=nm[1];}
|
|
2622
2659
|
return n;};
|
|
2623
|
-
const
|
|
2660
|
+
const rules=[];
|
|
2624
2661
|
const collect=list=>{for(const rule of Array.from(list||[])){
|
|
2625
2662
|
if(rule.cssRules&&rule.selectorText===undefined){collect(rule.cssRules);continue;}
|
|
2626
2663
|
const sel=rule.selectorText;if(!sel||!matchTok(sel))continue;
|
|
2627
|
-
if(rule.cssText)
|
|
2664
|
+
if(rule.cssText)rules.push({sel,txt:rule.cssText});}};
|
|
2628
2665
|
for(const sheet of Array.from(document.styleSheets||[])){let r;try{r=sheet.cssRules;}catch{continue;}collect(r);}
|
|
2629
|
-
for(const
|
|
2666
|
+
for(const{txt} of rules){const defRe=/(--[\w-]+)\s*:\s*([^;}]+)/g;let dm;
|
|
2630
2667
|
while((dm=defRe.exec(txt)))if(!varDefs.has(dm[1]))varDefs.set(dm[1],dm[2].trim());}
|
|
2631
|
-
for(const
|
|
2668
|
+
for(const{sel,txt} of rules){
|
|
2669
|
+
const st=extraStateClasses(sel);
|
|
2632
2670
|
if(property==="transform"){
|
|
2633
2671
|
const tm=txt.match(/transform\s*:\s*([^;}]+)/i);
|
|
2634
2672
|
if(tm){const sc=tm[1].match(/scale\(\s*([^),]+)/i);
|
|
2635
2673
|
if(sc){const inner=sc[1].trim();const vm=inner.match(/var\(\s*(--[\w-]+)/);
|
|
2636
2674
|
const num=vm?parseFloat(cs.getPropertyValue(vm[1])):parseFloat(inner);
|
|
2637
|
-
//
|
|
2638
|
-
// a
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
const better=!bestScale||Math.abs(num-1)>Math.abs(bestScale.scale-1)||(bestScale.varName==null&&vm);
|
|
2642
|
-
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]}:{})});}
|
|
2643
2679
|
const tr=tm[1].match(/translate(?:X|Y|3d)?\(\s*([^),]+)/i);
|
|
2644
2680
|
if(tr){const inner=tr[1].trim();const vm=inner.match(/var\(\s*(--[\w-]+)/);
|
|
2645
2681
|
const vn=vm?rootVar(vm[1]):null;
|
|
2646
2682
|
// distance is a magnitude — direction lives in the CSS (± calc).
|
|
2647
2683
|
const num=Math.abs(vn?parseFloat(cs.getPropertyValue(vn)):parseFloat(inner));
|
|
2648
|
-
// a var-backed translate at rest (0) still
|
|
2649
|
-
//
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
const better=!bestTranslate||num>bestTranslate.translate||(bestTranslate.translateVarName==null&&vn);
|
|
2653
|
-
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}:{})});}}
|
|
2654
2688
|
}else{
|
|
2655
2689
|
const fm=txt.match(/filter\s*:\s*([^;}]+)/i);
|
|
2656
2690
|
if(fm){const bl=fm[1].match(/blur\(\s*([^)]+)\)/i);
|
|
2657
2691
|
if(bl){const inner=bl[1].trim();const vm=inner.match(/var\(\s*(--[\w-]+)/);
|
|
2658
2692
|
const num=vm?parseFloat(cs.getPropertyValue(vm[1])):parseFloat(inner);
|
|
2659
|
-
// var-backed 0px still
|
|
2660
|
-
if(Number.isFinite(num)&&(num>1e-4||vm)
|
|
2661
|
-
|
|
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]}:{})});}}
|
|
2662
2696
|
}
|
|
2663
2697
|
}
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
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);}
|
|
2667
2723
|
}catch{}
|
|
2668
2724
|
return out;
|
|
2669
2725
|
}
|
|
@@ -2729,10 +2785,10 @@
|
|
|
2729
2785
|
const TL_LP_GUARD_PHASE_ID="tl-live-none-guard-phase";
|
|
2730
2786
|
// Same, resolving the element from a CSS selector (used when only a lane's
|
|
2731
2787
|
// selector is known, e.g. a grouped phase member).
|
|
2732
|
-
function laneValueHint(selector, property){
|
|
2788
|
+
function laneValueHint(selector, property, phaseHint){
|
|
2733
2789
|
if(!selector||(property!=="transform"&&property!=="filter"))return {};
|
|
2734
2790
|
let el=null;try{el=document.querySelector(selector);}catch{return {};}
|
|
2735
|
-
return el?readElValueHint(el,property):{};
|
|
2791
|
+
return el?readElValueHint(el,property,phaseHint):{};
|
|
2736
2792
|
}
|
|
2737
2793
|
|
|
2738
2794
|
// ── hooks ──
|
|
@@ -4863,7 +4919,10 @@
|
|
|
4863
4919
|
refineCacheRef.current.delete(active.id+"::"+type);
|
|
4864
4920
|
// attach the lane's non-resting scale/blur/translate value (+ backing var)
|
|
4865
4921
|
// so the agent can refine transform/filter VALUES too.
|
|
4866
|
-
|
|
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,
|
|
4867
4926
|
...(v.scale!=null?{scale:v.scale}:{}),...(v.blur!=null?{blur:v.blur}:{}),...(v.varName?{varName:v.varName}:{}),
|
|
4868
4927
|
...(v.translate!=null?{translate:v.translate}:{}),...(v.translateVarName?{translateVarName:v.translateVarName}:{})};};
|
|
4869
4928
|
try{
|
|
@@ -4884,12 +4943,12 @@
|
|
|
4884
4943
|
const ret=rep.effectiveTimings||[];
|
|
4885
4944
|
const sel=(ret[0]&&ret[0].selector)||null;
|
|
4886
4945
|
const ph=gp.length>1?gp.map(x=>({phase:x.phase||x.phaseLabel||"phase",label:x.phaseLabel||x.phase||"Phase",
|
|
4887
|
-
timings:(x.effectiveTimings||[]).map(withVal)})):undefined;
|
|
4888
|
-
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});
|
|
4889
4948
|
}else{
|
|
4890
4949
|
const fet=e.effectiveTimings||[];
|
|
4891
4950
|
const sel=(e.bindings&&e.bindings.selector)||(fet[0]&&fet[0].selector)||null;
|
|
4892
|
-
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))});
|
|
4893
4952
|
}
|
|
4894
4953
|
}
|
|
4895
4954
|
const{id}=await relayCreateJob({scope:"all",label:"All transitions",mode,refineType:"replace",transitions});
|
|
@@ -4897,7 +4956,7 @@
|
|
|
4897
4956
|
return;
|
|
4898
4957
|
}
|
|
4899
4958
|
const et=active.effectiveTimings||[];
|
|
4900
|
-
const timings=et.map(withVal);
|
|
4959
|
+
const timings=et.map(withVal(active));
|
|
4901
4960
|
const selector=(active.bindings&&active.bindings.selector)||(et[0]&&et[0].selector)||null;
|
|
4902
4961
|
// A recipe swap (replace) on a grouped phase should update its related
|
|
4903
4962
|
// phases (open + close) together — they're one motion. Send the whole
|
|
@@ -4906,7 +4965,7 @@
|
|
|
4906
4965
|
if(type==="replace"&&active.kind==="phase"&&active.groupId){
|
|
4907
4966
|
const gp=entries.filter(e=>e.kind==="phase"&&e.groupId===active.groupId);
|
|
4908
4967
|
if(gp.length>1)phases=gp.map(e=>({phase:e.phase||e.phaseLabel||"phase",label:e.phaseLabel||e.phase||"Phase",
|
|
4909
|
-
timings:(e.effectiveTimings||[]).map(withVal)}));
|
|
4968
|
+
timings:(e.effectiveTimings||[]).map(withVal(e))}));
|
|
4910
4969
|
}
|
|
4911
4970
|
const{id}=await relayCreateJob({transitionId:active.id,label:active.label,selector,
|
|
4912
4971
|
phase:active.phase||null,group:active.groupLabel||null,timings,mode,refineType:type,scope:"selected",phases});
|
|
@@ -5088,6 +5147,10 @@
|
|
|
5088
5147
|
delayMs:p.delayMs??pt.delayMs??(base&&base.delayMs)??0,
|
|
5089
5148
|
easing:p.easing??pt.easing??(base&&base.easing)??"ease",
|
|
5090
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};
|
|
5091
5154
|
const applyValue=(prop,mk)=>{
|
|
5092
5155
|
// mk(lane, hint) → override fields for one lane, or null to skip.
|
|
5093
5156
|
let vLanes=lanes.filter(t=>t.property===prop);
|
|
@@ -5095,7 +5158,7 @@
|
|
|
5095
5158
|
if(vLanes.length){
|
|
5096
5159
|
const rt=recipeTiming();
|
|
5097
5160
|
for(const lane of vLanes){
|
|
5098
|
-
const hint=laneValueHint(lane.selector,prop);
|
|
5161
|
+
const hint=laneValueHint(lane.selector,prop,phaseHint);
|
|
5099
5162
|
const ov=mk(lane,hint);
|
|
5100
5163
|
// an EXISTING lane the recipe reuses (e.g. Page side-by-side's
|
|
5101
5164
|
// filter lane at the source's 300ms) must ALSO adopt the recipe's
|
|
@@ -5108,7 +5171,7 @@
|
|
|
5108
5171
|
// A new filter lane rides the transform (slide) timing per the skill.
|
|
5109
5172
|
const sib=sibling(prop==="filter"?"transform":"opacity");
|
|
5110
5173
|
if(!sib)return;
|
|
5111
|
-
const hint=laneValueHint(sib.selector,prop);
|
|
5174
|
+
const hint=laneValueHint(sib.selector,prop,phaseHint);
|
|
5112
5175
|
const ov=mk(null,hint);
|
|
5113
5176
|
if(!ov)return;
|
|
5114
5177
|
const laneId=(sib.memberId?sib.memberId+"::":"")+prop;
|
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": {
|