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.
Files changed (2) hide show
  1. package/demo.html +97 -34
  2. 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
- 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){
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
- 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=[];
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 texts=[];
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)texts.push(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 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;
2630
2667
  while((dm=defRe.exec(txt)))if(!varDefs.has(dm[1]))varDefs.set(dm[1],dm[2].trim());}
2631
- for(const txt of texts){
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
- // pick the value furthest from the resting 1 (the real pre-scale);
2638
- // a var-backed scale at rest (1) still reports its var — the hook
2639
- // for ADDING a start-scale the source neutralized (e.g. icon swap).
2640
- if(Number.isFinite(num)&&(Math.abs(num-1)>1e-4||vm)){
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 reports its var the
2649
- // hook for ADDING a slide distance the source neutralized (e.g. a
2650
- // generic text swap with --text-swap-translate-y: 0).
2651
- if(Number.isFinite(num)&&(num>1e-4||vn)){
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 reports the var (the hook for ADDING blur).
2660
- if(Number.isFinite(num)&&(num>1e-4||vm)&&(!bestBlur||num>bestBlur.blur))
2661
- 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]}:{})});}}
2662
2696
  }
2663
2697
  }
2664
- if(bestScale)Object.assign(out,bestScale);
2665
- if(bestBlur)Object.assign(out,bestBlur);
2666
- if(bestTranslate)Object.assign(out,bestTranslate);
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
- 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,
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.33",
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": {