transitions-refine 0.3.26 → 0.3.27
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 +154 -56
- package/package.json +1 -1
- package/server/relay.mjs +9 -9
package/demo.html
CHANGED
|
@@ -2443,11 +2443,25 @@
|
|
|
2443
2443
|
}
|
|
2444
2444
|
return out;
|
|
2445
2445
|
}
|
|
2446
|
-
_applyOverrides(baseLanes,po){
|
|
2447
|
-
|
|
2446
|
+
_applyOverrides(baseLanes,po,memberFilter){
|
|
2447
|
+
const out=baseLanes.map(t=>{const o=po[t.laneId]||{};return {...t,durationMs:o.durationMs??t.durationMs,delayMs:o.delayMs??t.delayMs,easing:o.easing??t.easing,spring:o.spring??t.spring,
|
|
2448
2448
|
// value overrides (scale/blur) come only from applied refine suggestions;
|
|
2449
2449
|
// they carry their own from-value + backing var so Accept + live preview work.
|
|
2450
|
-
scale:o.scale??t.scale,blur:o.blur??t.blur,varName:o.varName??t.varName,scaleFrom:o.scaleFrom??t.scaleFrom,blurFrom:o.blurFrom??t.blurFrom
|
|
2450
|
+
scale:o.scale??t.scale,blur:o.blur??t.blur,varName:o.varName??t.varName,scaleFrom:o.scaleFrom??t.scaleFrom,blurFrom:o.blurFrom??t.blurFrom,
|
|
2451
|
+
translate:o.translate??t.translate,translateFrom:o.translateFrom??t.translateFrom,translateVarName:o.translateVarName??t.translateVarName};});
|
|
2452
|
+
// Synthetic lanes: a refine recipe can ADD a lane the source never
|
|
2453
|
+
// animated (e.g. blur on a transition with no `filter`). Overrides that
|
|
2454
|
+
// carry their own `property` and match no base lane become new lanes so
|
|
2455
|
+
// the transition string, live preview and Accept all see them.
|
|
2456
|
+
// memberFilter: undefined → include all; otherwise only lanes whose
|
|
2457
|
+
// memberId matches (null for flat entries).
|
|
2458
|
+
const have=new Set(baseLanes.map(t=>t.laneId));
|
|
2459
|
+
for(const[laneId,o]of Object.entries(po||{})){
|
|
2460
|
+
if(have.has(laneId)||!o||!o.property)continue;
|
|
2461
|
+
if(memberFilter!==undefined&&(o.memberId??null)!==memberFilter)continue;
|
|
2462
|
+
out.push({laneId,synthetic:true,...o});
|
|
2463
|
+
}
|
|
2464
|
+
return out;
|
|
2451
2465
|
}
|
|
2452
2466
|
_notify(){
|
|
2453
2467
|
this.effectiveCache.clear();
|
|
@@ -2463,7 +2477,7 @@
|
|
|
2463
2477
|
const members=(ph.members||[]).map(m=>{
|
|
2464
2478
|
const mb=baseLanes.filter(l=>l.memberId===m.id);
|
|
2465
2479
|
return {memberId:m.id,label:m.label||m.id,selector:m.selector,toState:m.toState,
|
|
2466
|
-
lanes:this._applyOverrides(mb,po),baseLanes:mb};
|
|
2480
|
+
lanes:this._applyOverrides(mb,po,m.id),baseLanes:mb};
|
|
2467
2481
|
});
|
|
2468
2482
|
const item={id,kind:"phase",groupId:g.id,groupLabel:g.label||g.id,component:g.component||null,
|
|
2469
2483
|
phase:ph.phase||null,phaseLabel:ph.label||ph.phase||"Phase",
|
|
@@ -2581,9 +2595,12 @@
|
|
|
2581
2595
|
}
|
|
2582
2596
|
|
|
2583
2597
|
// Read the non-resting ("pre") VALUE a lane animates from the live stylesheets:
|
|
2584
|
-
// transform-scale (≠1)
|
|
2585
|
-
// backing
|
|
2586
|
-
// {scale
|
|
2598
|
+
// transform-scale (≠1), transform-translate distance (≠0) and filter-blur,
|
|
2599
|
+
// plus the CSS custom property backing each (var-backed values are the ones we
|
|
2600
|
+
// can live-preview). Returns {scale?, blur?, varName?, translate?,
|
|
2601
|
+
// translateVarName?}. A var-backed blur is reported even at 0px — the var is
|
|
2602
|
+
// how an ADDED blur (recipe lane the source lacks) goes live. Only meaningful
|
|
2603
|
+
// for transform/filter lanes.
|
|
2587
2604
|
function readElValueHint(el, property){
|
|
2588
2605
|
const out={};
|
|
2589
2606
|
try{
|
|
@@ -2593,8 +2610,24 @@
|
|
|
2593
2610
|
const esc=s=>s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");
|
|
2594
2611
|
const tokRes=own.map(c=>new RegExp("\\."+esc(c)+"(?![\\w-])"));
|
|
2595
2612
|
const matchTok=sel=>!own.length||tokRes.some(re=>re.test(sel));
|
|
2596
|
-
let bestScale=null,bestBlur=null;
|
|
2597
|
-
|
|
2613
|
+
let bestScale=null,bestBlur=null,bestTranslate=null;
|
|
2614
|
+
// custom-property definitions seen on matching rules — lets a var chain
|
|
2615
|
+
// resolve to its token var (--t-page-from-x: calc(var(--page-slide-distance)
|
|
2616
|
+
// * -1) → --page-slide-distance), which is the var shared across members.
|
|
2617
|
+
const varDefs=new Map();
|
|
2618
|
+
// walk nested var() definitions to the innermost ("token") var.
|
|
2619
|
+
const rootVar=(name)=>{let n=name,guard=0;
|
|
2620
|
+
while(guard++<4){const def=varDefs.get(n);const nm=def&&def.match(/var\(\s*(--[\w-]+)/);if(!nm)break;n=nm[1];}
|
|
2621
|
+
return n;};
|
|
2622
|
+
const texts=[];
|
|
2623
|
+
const collect=list=>{for(const rule of Array.from(list||[])){
|
|
2624
|
+
if(rule.cssRules&&rule.selectorText===undefined){collect(rule.cssRules);continue;}
|
|
2625
|
+
const sel=rule.selectorText;if(!sel||!matchTok(sel))continue;
|
|
2626
|
+
if(rule.cssText)texts.push(rule.cssText);}};
|
|
2627
|
+
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;
|
|
2629
|
+
while((dm=defRe.exec(txt)))if(!varDefs.has(dm[1]))varDefs.set(dm[1],dm[2].trim());}
|
|
2630
|
+
for(const txt of texts){
|
|
2598
2631
|
if(property==="transform"){
|
|
2599
2632
|
const tm=txt.match(/transform\s*:\s*([^;}]+)/i);
|
|
2600
2633
|
if(tm){const sc=tm[1].match(/scale\(\s*([^),]+)/i);
|
|
@@ -2602,23 +2635,27 @@
|
|
|
2602
2635
|
const num=vm?parseFloat(cs.getPropertyValue(vm[1])):parseFloat(inner);
|
|
2603
2636
|
// pick the value furthest from the resting 1 (the real pre-scale).
|
|
2604
2637
|
if(Number.isFinite(num)&&Math.abs(num-1)>1e-4&&(!bestScale||Math.abs(num-1)>Math.abs(bestScale.scale-1)))
|
|
2605
|
-
bestScale={scale:num,...(vm?{varName:vm[1]}:{})};}
|
|
2638
|
+
bestScale={scale:num,...(vm?{varName:vm[1]}:{})};}
|
|
2639
|
+
const tr=tm[1].match(/translate(?:X|Y|3d)?\(\s*([^),]+)/i);
|
|
2640
|
+
if(tr){const inner=tr[1].trim();const vm=inner.match(/var\(\s*(--[\w-]+)/);
|
|
2641
|
+
const vn=vm?rootVar(vm[1]):null;
|
|
2642
|
+
// distance is a magnitude — direction lives in the CSS (± calc).
|
|
2643
|
+
const num=Math.abs(vn?parseFloat(cs.getPropertyValue(vn)):parseFloat(inner));
|
|
2644
|
+
if(Number.isFinite(num)&&num>1e-4&&(!bestTranslate||num>bestTranslate.translate))
|
|
2645
|
+
bestTranslate={translate:num,...(vn?{translateVarName:vn}:{})};}}
|
|
2606
2646
|
}else{
|
|
2607
2647
|
const fm=txt.match(/filter\s*:\s*([^;}]+)/i);
|
|
2608
2648
|
if(fm){const bl=fm[1].match(/blur\(\s*([^)]+)\)/i);
|
|
2609
2649
|
if(bl){const inner=bl[1].trim();const vm=inner.match(/var\(\s*(--[\w-]+)/);
|
|
2610
2650
|
const num=vm?parseFloat(cs.getPropertyValue(vm[1])):parseFloat(inner);
|
|
2611
|
-
|
|
2651
|
+
// var-backed 0px still reports the var (the hook for ADDING blur).
|
|
2652
|
+
if(Number.isFinite(num)&&(num>1e-4||vm)&&(!bestBlur||num>bestBlur.blur))
|
|
2612
2653
|
bestBlur={blur:num,...(vm?{varName:vm[1]}:{})};}}
|
|
2613
2654
|
}
|
|
2614
|
-
}
|
|
2615
|
-
const collect=list=>{for(const rule of Array.from(list||[])){
|
|
2616
|
-
if(rule.cssRules&&rule.selectorText===undefined){collect(rule.cssRules);continue;}
|
|
2617
|
-
const sel=rule.selectorText;if(!sel||!matchTok(sel))continue;
|
|
2618
|
-
if(rule.cssText)consider(rule.cssText);}};
|
|
2619
|
-
for(const sheet of Array.from(document.styleSheets||[])){let r;try{r=sheet.cssRules;}catch{continue;}collect(r);}
|
|
2655
|
+
}
|
|
2620
2656
|
if(bestScale)Object.assign(out,bestScale);
|
|
2621
2657
|
if(bestBlur)Object.assign(out,bestBlur);
|
|
2658
|
+
if(bestTranslate)Object.assign(out,bestTranslate);
|
|
2622
2659
|
}catch{}
|
|
2623
2660
|
return out;
|
|
2624
2661
|
}
|
|
@@ -3240,7 +3277,10 @@
|
|
|
3240
3277
|
"Updates ",
|
|
3241
3278
|
s.patches.map((pp,pi)=>h("span",{className:"tl-sug-phase",key:pi},
|
|
3242
3279
|
(cap(pp.phase)||pp.phase||"Phase"),
|
|
3243
|
-
pp.durationMs!=null&&h("span",{className:"tl-sug-phase-dur"},pp.durationMs+"ms")
|
|
3280
|
+
pp.durationMs!=null&&h("span",{className:"tl-sug-phase-dur"},pp.durationMs+"ms"),
|
|
3281
|
+
pp.scale!=null&&h("span",{className:"tl-sug-phase-dur"},"scale "+pp.scale),
|
|
3282
|
+
pp.blur!=null&&h("span",{className:"tl-sug-phase-dur"},"blur "+pp.blur+"px"),
|
|
3283
|
+
pp.translate!=null&&h("span",{className:"tl-sug-phase-dur"},"slide "+pp.translate+"px")))))
|
|
3244
3284
|
: h(React.Fragment,null,
|
|
3245
3285
|
s.title&&h("div",{className:"tl-sug-title"},s.title),
|
|
3246
3286
|
(s.from||s.to)&&h("div",{className:"tl-sug-delta"},
|
|
@@ -3532,14 +3572,21 @@
|
|
|
3532
3572
|
const base=tgt.baseLanes||[];
|
|
3533
3573
|
const bmap=new Map(base.map(t=>[t.laneId,t]));
|
|
3534
3574
|
for(const t of (tgt.effectiveTimings||[])){
|
|
3535
|
-
const b=bmap.get(t.laneId);
|
|
3536
|
-
|
|
3575
|
+
const b=bmap.get(t.laneId);
|
|
3576
|
+
// a SYNTHETIC lane (recipe added a property the source never animated,
|
|
3577
|
+
// e.g. blur on a filter-less transition) has no base — write it as an
|
|
3578
|
+
// addition: the lane's timing plus its value(s) below.
|
|
3579
|
+
if(!b&&!t.synthetic)continue;
|
|
3580
|
+
if(!b){
|
|
3581
|
+
out.push({property:t.property,member:t.member||null,selector:t.selector||null,phase:tgt.phase||null,added:true,
|
|
3582
|
+
from:null,to:{durationMs:t.durationMs,delayMs:t.delayMs,easing:t.easing}});
|
|
3583
|
+
}else if(t.durationMs!==b.durationMs||t.delayMs!==b.delayMs||(t.easing||"")!==(b.easing||"")){
|
|
3537
3584
|
out.push({property:t.property,member:t.member||null,selector:t.selector||null,phase:tgt.phase||null,
|
|
3538
3585
|
from:{durationMs:b.durationMs,delayMs:b.delayMs,easing:b.easing},
|
|
3539
3586
|
to:{durationMs:t.durationMs,delayMs:t.delayMs,easing:t.easing}});
|
|
3540
3587
|
}
|
|
3541
|
-
// value edits (scale/blur) live as overrides on the effective
|
|
3542
|
-
// carry their own from-value + backing var; emit one change each.
|
|
3588
|
+
// value edits (scale/blur/translate) live as overrides on the effective
|
|
3589
|
+
// lane and carry their own from-value + backing var; emit one change each.
|
|
3543
3590
|
if(t.scale!=null){
|
|
3544
3591
|
out.push({property:t.property,member:t.member||null,selector:t.selector||null,phase:tgt.phase||null,varName:t.varName||null,
|
|
3545
3592
|
from:{scale:t.scaleFrom},to:{scale:t.scale}});
|
|
@@ -3548,6 +3595,10 @@
|
|
|
3548
3595
|
out.push({property:t.property,member:t.member||null,selector:t.selector||null,phase:tgt.phase||null,varName:t.varName||null,
|
|
3549
3596
|
from:{blur:t.blurFrom},to:{blur:t.blur}});
|
|
3550
3597
|
}
|
|
3598
|
+
if(t.translate!=null){
|
|
3599
|
+
out.push({property:t.property,member:t.member||null,selector:t.selector||null,phase:tgt.phase||null,varName:t.translateVarName||null,
|
|
3600
|
+
from:{translate:t.translateFrom},to:{translate:t.translate}});
|
|
3601
|
+
}
|
|
3551
3602
|
}
|
|
3552
3603
|
}
|
|
3553
3604
|
return out;
|
|
@@ -4602,7 +4653,10 @@
|
|
|
4602
4653
|
const[refineLabel,setRefineLabel]=useState(null);
|
|
4603
4654
|
const[appliedIds,setAppliedIds]=useState({});
|
|
4604
4655
|
const[refineMode,setRefineMode]=useState("llm"); // llm (Agent) | deterministic
|
|
4605
|
-
|
|
4656
|
+
// small | replace — last selected tab persists (same pattern as tl-theme-pref)
|
|
4657
|
+
const[refineType,setRefineType]=useState(()=>{
|
|
4658
|
+
try{const v=localStorage.getItem("tl-refine-tab-pref");return v==="small"||v==="replace"?v:"small";}catch{return "small";}
|
|
4659
|
+
});
|
|
4606
4660
|
const[llmAvailable,setLlmAvailable]=useState(null); // null=unknown, true/false from /health
|
|
4607
4661
|
const[cliInstalled,setCliInstalled]=useState(null); // null=unknown, true/false from /health
|
|
4608
4662
|
const[agentReason,setAgentReason]=useState(null); // why no CLI is wired (/health agentReason)
|
|
@@ -4741,7 +4795,8 @@
|
|
|
4741
4795
|
// attach the lane's non-resting scale/blur value (+ backing var) so the
|
|
4742
4796
|
// agent + deterministic engine can refine transform/filter VALUES too.
|
|
4743
4797
|
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,
|
|
4744
|
-
...(v.scale!=null?{scale:v.scale}:{}),...(v.blur!=null?{blur:v.blur}:{}),...(v.varName?{varName:v.varName}:{})
|
|
4798
|
+
...(v.scale!=null?{scale:v.scale}:{}),...(v.blur!=null?{blur:v.blur}:{}),...(v.varName?{varName:v.varName}:{}),
|
|
4799
|
+
...(v.translate!=null?{translate:v.translate}:{}),...(v.translateVarName?{translateVarName:v.translateVarName}:{})};};
|
|
4745
4800
|
const timings=et.map(withVal);
|
|
4746
4801
|
const selector=(active.bindings&&active.bindings.selector)||(et[0]&&et[0].selector)||null;
|
|
4747
4802
|
// A recipe swap (replace/both) on a grouped phase should update its related
|
|
@@ -4769,6 +4824,7 @@
|
|
|
4769
4824
|
// (idle/error), tabs just preview copy and never auto-scan.
|
|
4770
4825
|
const changeRefineType=useCallback((t)=>{
|
|
4771
4826
|
setRefineType(t);
|
|
4827
|
+
try{localStorage.setItem("tl-refine-tab-pref",t);}catch{}
|
|
4772
4828
|
if(!active)return;
|
|
4773
4829
|
if(scanModeRef.current==="deterministic")return;
|
|
4774
4830
|
if(refinePhase!=="done"&&refinePhase!=="scanning")return;
|
|
@@ -4835,6 +4891,7 @@
|
|
|
4835
4891
|
// map the property patch onto the matching lane(s) — a property can span
|
|
4836
4892
|
// several members in a grouped phase, so apply to each.
|
|
4837
4893
|
const et=target.effectiveTimings||[];
|
|
4894
|
+
const memberLanes=p.member?et.filter(t=>(t.member===p.member||t.memberId===p.member)):et;
|
|
4838
4895
|
let lanes=p.property==="all"?et:et.filter(t=>t.property===p.property);
|
|
4839
4896
|
if(p.member)lanes=lanes.filter(t=>(t.member===p.member||t.memberId===p.member));
|
|
4840
4897
|
const ids=lanes.length?lanes.map(t=>t.laneId):[p.property];
|
|
@@ -4842,31 +4899,68 @@
|
|
|
4842
4899
|
for(const id of ids)registry.setPropOverride(target.id,id,timingOverride);
|
|
4843
4900
|
}
|
|
4844
4901
|
|
|
4845
|
-
// Value edits (scale/blur) must land on concrete lanes so live
|
|
4846
|
-
// can write the right backing var. For coarse recipe patches that
|
|
4847
|
-
// property:"all" and omit varName, fan out by lane type.
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4902
|
+
// Value edits (scale/blur/translate) must land on concrete lanes so live
|
|
4903
|
+
// preview can write the right backing var. For coarse recipe patches that
|
|
4904
|
+
// use property:"all" and omit varName, fan out by lane type. When a
|
|
4905
|
+
// recipe prescribes a value the source has NO lane for (e.g. blur on a
|
|
4906
|
+
// transition that never animated `filter`), SYNTHESIZE the lane: an
|
|
4907
|
+
// override keyed by a fresh laneId that carries its own property, member
|
|
4908
|
+
// and timing, which the registry surfaces as a new effective lane.
|
|
4909
|
+
// The backing var is probed from the live stylesheets at apply time
|
|
4910
|
+
// (readElValueHint reports var-backed values even at their resting 0),
|
|
4911
|
+
// so the patch doesn't need to know the page's CSS custom properties.
|
|
4912
|
+
const sibling=(prop)=>memberLanes.find(t=>t.property===prop)||memberLanes[0]||et[0]||null;
|
|
4913
|
+
const synthTiming=(base)=>({
|
|
4914
|
+
durationMs:p.durationMs??(base&&base.durationMs)??250,
|
|
4915
|
+
delayMs:p.delayMs??(base&&base.delayMs)??0,
|
|
4916
|
+
easing:p.easing??(base&&base.easing)??"ease",
|
|
4917
|
+
});
|
|
4918
|
+
const applyValue=(prop,mk)=>{
|
|
4919
|
+
// mk(lane, hint) → override fields for one lane, or null to skip.
|
|
4920
|
+
let vLanes=lanes.filter(t=>t.property===prop);
|
|
4921
|
+
if(!vLanes.length&&p.property==="all")vLanes=et.filter(t=>t.property===prop&&(!p.member||t.member===p.member||t.memberId===p.member));
|
|
4922
|
+
if(vLanes.length){
|
|
4923
|
+
for(const lane of vLanes){
|
|
4924
|
+
const hint=laneValueHint(lane.selector,prop);
|
|
4925
|
+
const ov=mk(lane,hint);
|
|
4926
|
+
if(ov)registry.setPropOverride(target.id,lane.laneId,ov);
|
|
4927
|
+
}
|
|
4928
|
+
return;
|
|
4868
4929
|
}
|
|
4869
|
-
|
|
4930
|
+
// no such lane on this member → synthesize one next to its siblings.
|
|
4931
|
+
// A new filter lane rides the transform (slide) timing per the skill.
|
|
4932
|
+
const sib=sibling(prop==="filter"?"transform":"opacity");
|
|
4933
|
+
if(!sib)return;
|
|
4934
|
+
const hint=laneValueHint(sib.selector,prop);
|
|
4935
|
+
const ov=mk(null,hint);
|
|
4936
|
+
if(!ov)return;
|
|
4937
|
+
const laneId=(sib.memberId?sib.memberId+"::":"")+prop;
|
|
4938
|
+
registry.setPropOverride(target.id,laneId,{
|
|
4939
|
+
property:prop,memberId:sib.memberId??null,member:sib.member??null,selector:sib.selector??null,
|
|
4940
|
+
...synthTiming(sib),...ov,
|
|
4941
|
+
});
|
|
4942
|
+
};
|
|
4943
|
+
if(p.scale!=null)applyValue("transform",(lane,hint)=>{
|
|
4944
|
+
const ov={scale:p.scale,scaleFrom:(lane&&lane.scale)??hint.scale??(s.from!=null?parseFloat(s.from):undefined)};
|
|
4945
|
+
const vn=p.varName??(lane&&lane.varName)??hint.varName;
|
|
4946
|
+
if(vn!=null)ov.varName=vn;
|
|
4947
|
+
return ov;
|
|
4948
|
+
});
|
|
4949
|
+
if(p.blur!=null)applyValue("filter",(lane,hint)=>{
|
|
4950
|
+
const ov={blur:p.blur,blurFrom:(lane&&lane.blur)??hint.blur??0};
|
|
4951
|
+
const vn=p.varName??(lane&&lane.varName)??hint.varName;
|
|
4952
|
+
if(vn!=null)ov.varName=vn;
|
|
4953
|
+
return ov;
|
|
4954
|
+
});
|
|
4955
|
+
if(p.translate!=null)applyValue("transform",(lane,hint)=>{
|
|
4956
|
+
const ov={translate:p.translate,translateFrom:(lane&&lane.translate)??hint.translate??0};
|
|
4957
|
+
const vn=p.translateVarName??p.varName??(lane&&lane.translateVarName)??hint.translateVarName;
|
|
4958
|
+
// a translate distance can only go live through a backing var — the
|
|
4959
|
+
// inline transition string can't express keyframe positions. Still
|
|
4960
|
+
// record the edit (Accept writes it) even when no var exists.
|
|
4961
|
+
if(vn!=null)ov.translateVarName=vn;
|
|
4962
|
+
return ov;
|
|
4963
|
+
});
|
|
4870
4964
|
}
|
|
4871
4965
|
setAppliedIds(prev=>({...prev,[s.id]:true}));
|
|
4872
4966
|
},[registry,active,entries]);
|
|
@@ -4892,11 +4986,13 @@
|
|
|
4892
4986
|
if(typeof document==="undefined")return;
|
|
4893
4987
|
const applied=liveAppliedRef.current;
|
|
4894
4988
|
const desired=new Map(); // el → {css, vars:{name:value}}
|
|
4895
|
-
// collect the var-backed scale/blur overrides on a set of lanes
|
|
4896
|
-
// {varName: cssValue} map (scale → unitless, blur → px)
|
|
4897
|
-
const lanesToVars=(lanes)=>{const v={};for(const l of (lanes||[])){
|
|
4898
|
-
if(l.
|
|
4899
|
-
|
|
4989
|
+
// collect the var-backed scale/blur/translate overrides on a set of lanes
|
|
4990
|
+
// into a {varName: cssValue} map (scale → unitless, blur/translate → px).
|
|
4991
|
+
const lanesToVars=(lanes)=>{const v={};for(const l of (lanes||[])){
|
|
4992
|
+
if(l.varName){
|
|
4993
|
+
if(l.scale!=null)v[l.varName]=String(l.scale);
|
|
4994
|
+
if(l.blur!=null)v[l.varName]=l.blur+"px";}
|
|
4995
|
+
if(l.translate!=null&&l.translateVarName)v[l.translateVarName]=l.translate+"px";}return v;};
|
|
4900
4996
|
const add=(els,css,vars)=>{ for(const el of els){ if(!el||(el.closest&&el.closest("[data-timeline-panel],[data-tl-ui]")))continue; desired.set(el,{css,vars:vars||{}}); } };
|
|
4901
4997
|
for(const item of entries){
|
|
4902
4998
|
if(item.kind==="phase")continue; // phases handled by the state-scoped effect
|
|
@@ -4962,9 +5058,11 @@
|
|
|
4962
5058
|
if(livePhaseRef.current) txTeardownPhase(livePhaseRef.current);
|
|
4963
5059
|
const store={observers:[],ptr:null,applied:new Map(),pending:false};
|
|
4964
5060
|
livePhaseRef.current=store;
|
|
4965
|
-
const lanesToVars=(lanes)=>{const v={};for(const l of (lanes||[])){
|
|
4966
|
-
if(l.
|
|
4967
|
-
|
|
5061
|
+
const lanesToVars=(lanes)=>{const v={};for(const l of (lanes||[])){
|
|
5062
|
+
if(l.varName){
|
|
5063
|
+
if(l.scale!=null)v[l.varName]=String(l.scale);
|
|
5064
|
+
if(l.blur!=null)v[l.varName]=l.blur+"px";}
|
|
5065
|
+
if(l.translate!=null&&l.translateVarName)v[l.translateVarName]=l.translate+"px";}return v;};
|
|
4968
5066
|
const inUI=el=>el&&el.closest&&el.closest("[data-timeline-panel],[data-tl-ui]");
|
|
4969
5067
|
const safeMatches=(el,sel)=>{ if(!sel)return false; try{return el.matches(sel);}catch{return false;} };
|
|
4970
5068
|
const fresh=sel=>{ if(!sel)return []; try{return Array.from(document.querySelectorAll(sel)).filter(e=>!inUI(e));}catch{return [];} };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "transitions-refine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.27",
|
|
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": {
|
package/server/relay.mjs
CHANGED
|
@@ -265,9 +265,9 @@ const RECIPES_BLOCK = [
|
|
|
265
265
|
"- Card hover tilt: 3D tilt toward the pointer (19-card-tilt.md)",
|
|
266
266
|
"- Plus to menu morph: a circular trigger becomes the surface it opens (20-plus-menu-morph.md)",
|
|
267
267
|
"- Accordion expand: a collapsible body grows/shrinks in height (21-accordion.md)",
|
|
268
|
-
"Recipe non-resting values — the pre-blur/pre-scale the recipe animates FROM (settling to blur 0 / scale 1). When you pick one of these recipes you MUST also emit a lane-level value patch for
|
|
268
|
+
"Recipe non-resting values — the pre-blur/pre-scale/slide-distance the recipe animates FROM (settling to blur 0 / scale 1 / translate 0). When you pick one of these recipes you MUST also emit a lane-level value patch for EACH of its values, EVEN IF the input has no such lane (add the lane so the element gains the recipe's blur/scale/distance):",
|
|
269
269
|
" - Panel reveal → filter blur 2px (Small)",
|
|
270
|
-
" - Page side-by-side → filter blur 3px (Medium) on the sliding page",
|
|
270
|
+
" - Page side-by-side → filter blur 3px (Medium) AND transform translate distance 8px, both on the sliding page",
|
|
271
271
|
" - Icon swap → filter blur 2px (Small)",
|
|
272
272
|
" - Skeleton loader and reveal → filter blur 2px (Small)",
|
|
273
273
|
" - Texts reveal → filter blur 3px (Medium)",
|
|
@@ -305,7 +305,7 @@ function buildPrompt(job) {
|
|
|
305
305
|
"",
|
|
306
306
|
"Infer each declaration's USAGE (modal close, dropdown open, tooltip, badge, resize, color/theme change…) from the label/selector. Match on usage intent, not the nearest number.",
|
|
307
307
|
"",
|
|
308
|
-
"Some timings carry VALUES too: a `transform` lane may include `scale` (its non-resting pre-scale, e.g. 0.8) and a `filter` lane may include `blur` (its non-resting pre-blur in px). When present, also check these against the Scales/Blur tokens by USAGE (a dropdown-open surface should pre-scale to 0.97, not whatever number it has) and propose a fix when they differ. A lane may also carry `varName` (the CSS custom property backing
|
|
308
|
+
"Some timings carry VALUES too: a `transform` lane may include `scale` (its non-resting pre-scale, e.g. 0.8) and/or `translate` (its non-resting slide distance in px, e.g. 30), and a `filter` lane may include `blur` (its non-resting pre-blur in px). When present, also check these against the Scales/Blur tokens and the recipe values by USAGE (a dropdown-open surface should pre-scale to 0.97, not whatever number it has; a page slide should travel the recipe's distance) and propose a fix when they differ. A lane may also carry `varName` / `translateVarName` (the CSS custom property backing each value) — pass them straight through in the patch so the edit targets that variable.",
|
|
309
309
|
"",
|
|
310
310
|
];
|
|
311
311
|
if (needsRecipe) {
|
|
@@ -317,7 +317,7 @@ function buildPrompt(job) {
|
|
|
317
317
|
);
|
|
318
318
|
if (multiPhase) {
|
|
319
319
|
lines.push(
|
|
320
|
-
"RELATED PHASES: `phases` lists this transition's related states (e.g. open AND close). They are ONE motion — a recipe swap must update them TOGETHER. For the replace suggestion emit a `patches` array with ONE timing entry per phase in `phases`, shaped `{\"phase\":<phase>,\"property\":\"all\",\"durationMs\":...,\"easing\":...}`. If the recipe uses transform pre-scale
|
|
320
|
+
"RELATED PHASES: `phases` lists this transition's related states (e.g. open AND close). They are ONE motion — a recipe swap must update them TOGETHER. For the replace suggestion emit a `patches` array with ONE timing entry per phase in `phases`, shaped `{\"phase\":<phase>,\"property\":\"all\",\"durationMs\":...,\"easing\":...}`. If the recipe uses transform pre-scale, filter pre-blur or a slide distance (see \"Recipe non-resting values\" above), add EXTRA per-lane value patches for those lanes (not property:\"all\"), e.g. `{\"phase\":\"open\",\"property\":\"transform\",\"member\":\"Panel\",\"varName\":\"--dropdown-pre-scale\",\"scale\":0.97}`, `{\"phase\":\"open\",\"property\":\"filter\",\"member\":\"Panel\",\"varName\":\"--panel-blur\",\"blur\":2}` or `{\"phase\":\"forward\",\"property\":\"transform\",\"member\":\"Page panel\",\"translateVarName\":\"--page-slide-distance\",\"translate\":8}`. Copy each lane's `member` + `varName`/`translateVarName` from the input timings when that lane exists; when the recipe adds a lane the input LACKS (e.g. Page side-by-side prescribes a 3px blur but the captured page has no `filter` lane), STILL emit the value patch — reuse the sibling lane's `member` so it targets the right element and omit the var name (Apply discovers the page's backing variable itself). Take each phase duration from MOTION TOKENS (open usually slower than close, e.g. 250ms/150ms). Also include a single `patch` for live preview (first timing patch is fine). Apply will write every phase from `patches`.",
|
|
321
321
|
"",
|
|
322
322
|
);
|
|
323
323
|
}
|
|
@@ -327,7 +327,7 @@ function buildPrompt(job) {
|
|
|
327
327
|
"refineType is \"replace\": suggest a WHOLE-TRANSITION replacement ONLY — do NOT propose motion-token tweaks (no kind \"duration\"/\"delay\"/\"easing\").",
|
|
328
328
|
multiPhase
|
|
329
329
|
? "Pick the SINGLE best-fit recipe from the list above. Emit ONE suggestion with kind \"replace\": include the `patches` array (one entry per related phase, as described above) AND a single `patch` (the first phase) for the live preview, add a `reference` field with the reference filename, and name the recipe in `title`/`reason`. If no recipe genuinely fits the usage, return an empty suggestions array."
|
|
330
|
-
: "Pick the SINGLE best-fit recipe from the list above. Emit ONE suggestion with kind \"replace\": set its `patch` to the motion-token duration/easing for the recipe's phase on the property that already transitions (or \"all\") so Apply works live, add a `reference` field with the reference filename, and name the recipe in `title`/`reason`. If the recipe prescribes a non-resting pre-blur/pre-scale (see \"Recipe non-resting values\" above), put
|
|
330
|
+
: "Pick the SINGLE best-fit recipe from the list above. Emit ONE suggestion with kind \"replace\": set its `patch` to the motion-token duration/easing for the recipe's phase on the property that already transitions (or \"all\") so Apply works live, add a `reference` field with the reference filename, and name the recipe in `title`/`reason`. If the recipe prescribes a non-resting pre-blur/pre-scale/slide-distance (see \"Recipe non-resting values\" above), put those values on the `patch` too (add `blur` for a filter-blur recipe like Page side-by-side → 3px, `scale` for a scale recipe, `translate` for a slide distance like Page side-by-side → 8px) so the sliding/scaling element gains them even when the captured transition had no such lane. If no recipe genuinely fits the usage, return an empty suggestions array.",
|
|
331
331
|
"Answer in ONE response — do NOT read or search files.",
|
|
332
332
|
);
|
|
333
333
|
} else if (refineType === "both") {
|
|
@@ -337,7 +337,7 @@ function buildPrompt(job) {
|
|
|
337
337
|
MISSING_BLUR_NOTE,
|
|
338
338
|
multiPhase
|
|
339
339
|
? "(2) Whole-transition replacement (kind \"replace\"): ALWAYS evaluate one — pick the SINGLE best-fit recipe. Emit at most ONE \"replace\" suggestion with the `patches` array (one entry per related phase) AND a single `patch` (the first phase), a `reference` field, and the recipe named in `title`/`reason`. If no recipe genuinely fits, omit the replace suggestion."
|
|
340
|
-
: "(2) Whole-transition replacement (kind \"replace\"): ALWAYS evaluate one — pick the SINGLE best-fit recipe from the list above. Emit at most ONE \"replace\" suggestion: set its `patch` to the motion-token duration/easing for the recipe's phase on the property that already transitions (or \"all\"); if the recipe prescribes a non-resting pre-blur/pre-scale (see \"Recipe non-resting values\" above) put
|
|
340
|
+
: "(2) Whole-transition replacement (kind \"replace\"): ALWAYS evaluate one — pick the SINGLE best-fit recipe from the list above. Emit at most ONE \"replace\" suggestion: set its `patch` to the motion-token duration/easing for the recipe's phase on the property that already transitions (or \"all\"); if the recipe prescribes a non-resting pre-blur/pre-scale/slide-distance (see \"Recipe non-resting values\" above) put those `blur`/`scale`/`translate` values on the `patch` too so e.g. Page side-by-side gains its 3px blur and 8px distance. Add a `reference` field with the reference filename, and name the recipe in `title`/`reason`. If no recipe genuinely fits, omit the replace suggestion.",
|
|
341
341
|
"Answer in ONE response — do NOT read or search files.",
|
|
342
342
|
);
|
|
343
343
|
} else {
|
|
@@ -354,8 +354,8 @@ function buildPrompt(job) {
|
|
|
354
354
|
'A scale tweak looks like {"id":"transform-scale","kind":"scale","property":"transform","member":"Menu","title":"Scale → Medium","from":"0.8","to":"0.97","patch":{"property":"transform","member":"Menu","scale":0.97},"reason":"…"}; a blur tweak like {"id":"filter-blur","kind":"blur","property":"filter","member":"Panel","title":"Blur → Small","from":"8px","to":"2px","patch":{"property":"filter","member":"Panel","blur":2},"reason":"…"}.',
|
|
355
355
|
"CRITICAL — `member`: every suggestion and its `patch` MUST echo the `member` of the input lane it came from, copied VERBATIM from that lane in the data above. This is REQUIRED whenever any input lane carries a `member` — most importantly when several lanes share the same `property` (e.g. a dropdown where a caret does `transform: rotate` AND a panel does `transform: scale`): the `member` is the ONLY way to tell which lane a `transform` tweak targets. Omitting it mislabels the suggestion onto the wrong element. Omit `member` only for lanes that genuinely have none.",
|
|
356
356
|
multiPhase
|
|
357
|
-
? 'A multi-phase replace suggestion ALSO carries a `patches` array. Put timing in phase-level entries (`property:"all"`, duration/easing), and put scale/blur in lane-level entries (`property:"transform"`/`"filter"` plus the lane `member` and `varName` from input timings). In each `patch`/`patches` entry include only changed fields (durationMs, delayMs, easing, scale, blur — plus `member` copied from input lane and `varName` when present); `property` must match an input property or "all".'
|
|
358
|
-
: 'In each `patch` include only the changed fields (durationMs, delayMs, easing, scale, blur — plus `member` copied from the input lane, and `varName` if the input lane had one); `property` must match an input property or "all". If nothing should change, return an empty suggestions array.',
|
|
357
|
+
? 'A multi-phase replace suggestion ALSO carries a `patches` array. Put timing in phase-level entries (`property:"all"`, duration/easing), and put scale/blur/translate in lane-level entries (`property:"transform"`/`"filter"` plus the lane `member` and `varName`/`translateVarName` from input timings). In each `patch`/`patches` entry include only changed fields (durationMs, delayMs, easing, scale, blur, translate — plus `member` copied from input lane and `varName`/`translateVarName` when present); `property` must match an input property or "all".'
|
|
358
|
+
: 'In each `patch` include only the changed fields (durationMs, delayMs, easing, scale, blur, translate — plus `member` copied from the input lane, and `varName`/`translateVarName` if the input lane had one); `property` must match an input property or "all". If nothing should change, return an empty suggestions array.',
|
|
359
359
|
);
|
|
360
360
|
return lines.join("\n");
|
|
361
361
|
}
|
|
@@ -503,7 +503,7 @@ function buildApplyPrompt(job) {
|
|
|
503
503
|
"",
|
|
504
504
|
"Steps:",
|
|
505
505
|
"1. Find where this transition is defined in the source. Search by the per-change `selector`/`member`, the `component` hint, and class names. Handle plain CSS, CSS Modules, styled-components/emotion template literals, Tailwind utilities/config, inline style objects, and Motion/Framer variants — the browser selector is a hint, the real declaration may live in any of these.",
|
|
506
|
-
"2. For each change, edit the source so that property's transition uses the `to` values. A change's `to` may include timing (durationMs in ms, easing, delayMs in ms) AND/OR values: `scale` (the non-resting transform pre-scale, e.g. set `transform: scale(0.97)` on the pre-open/closed state — never the resting scale(1))
|
|
506
|
+
"2. For each change, edit the source so that property's transition uses the `to` values. A change's `to` may include timing (durationMs in ms, easing, delayMs in ms) AND/OR values: `scale` (the non-resting transform pre-scale, e.g. set `transform: scale(0.97)` on the pre-open/closed state — never the resting scale(1)), `blur` (the non-resting filter pre-blur in px, e.g. `filter: blur(2px)` on that same state) and `translate` (the non-resting slide distance in px — the offset the element travels from, keeping the existing direction/sign convention). A change with `added:true` introduces a lane the source never animated — add that property to the transition (with the `to` timing) AND give the pre-state its non-resting value from the paired value change. If the change carries `varName`, the value is backed by that CSS custom property — update the variable's value at its definition instead of the inline declaration. Keep the file's existing unit/format conventions (e.g. `0.25s` vs `250ms`) and only touch the named property on the right member + phase.",
|
|
507
507
|
"3. Make the minimal edit. Do not reformat or change unrelated code.",
|
|
508
508
|
"",
|
|
509
509
|
'Output ONLY a JSON object — no prose, no markdown fences — shaped exactly like:',
|