transitions-refine 0.3.25 → 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 +26 -7
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,26 @@ 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/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
|
+
" - Panel reveal → filter blur 2px (Small)",
|
|
270
|
+
" - Page side-by-side → filter blur 3px (Medium) AND transform translate distance 8px, both on the sliding page",
|
|
271
|
+
" - Icon swap → filter blur 2px (Small)",
|
|
272
|
+
" - Skeleton loader and reveal → filter blur 2px (Small)",
|
|
273
|
+
" - Texts reveal → filter blur 3px (Medium)",
|
|
274
|
+
" - Success check → filter blur 8px (Large)",
|
|
275
|
+
" - Modal open/close → transform scale 0.96 (Large)",
|
|
276
|
+
" - Menu dropdown → transform scale 0.97 (Medium) open, 0.99 (Tiny) close",
|
|
277
|
+
" - Tooltip open/close → transform scale 0.98 (Small)",
|
|
268
278
|
"Tie-break: prefer the lower-overhead recipe (card resize over panel reveal, dropdown over modal). If no recipe genuinely fits, return an empty suggestions array.",
|
|
269
279
|
].join("\n");
|
|
270
280
|
|
|
281
|
+
// Shared guidance for the token-tweak passes (small + both). Mirrors the
|
|
282
|
+
// deterministic engine's "missing-blur" path: some usages animate a non-resting
|
|
283
|
+
// pre-blur that a bare opacity/transform capture won't show, so the agent must
|
|
284
|
+
// ADD it rather than only tweak blur lanes that already exist.
|
|
285
|
+
const MISSING_BLUR_NOTE =
|
|
286
|
+
"MISSING BLUR: blur can be ABSENT, not just off-token. When the inferred usage clearly implies a non-resting pre-blur per the Blur tokens above (e.g. a page slide / page side-by-side panel → 3px Medium; an icon swap, text swap, panel reveal, skeleton reveal or number pop-in → 2px Small; a text reveal → 3px Medium; a success check → 8px Large) but the input timings carry NO `filter` lane, ADD one: emit a `blur` suggestion with `property:\"filter\"`, `from:\"0px\"`, `to` the usage token (e.g. \"3px\"), echoing the `member` (and `varName` if present) of the sibling lanes so the sliding/swapping element gains the recipe's blur. Only add blur when the usage genuinely calls for it — never on motion that is intentionally opacity-only (a plain fade, a colour/theme change).";
|
|
287
|
+
|
|
271
288
|
function buildPrompt(job) {
|
|
272
289
|
const r = job.request || {};
|
|
273
290
|
const rawType = r.refineType || "small";
|
|
@@ -288,7 +305,7 @@ function buildPrompt(job) {
|
|
|
288
305
|
"",
|
|
289
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.",
|
|
290
307
|
"",
|
|
291
|
-
"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.",
|
|
292
309
|
"",
|
|
293
310
|
];
|
|
294
311
|
if (needsRecipe) {
|
|
@@ -300,7 +317,7 @@ function buildPrompt(job) {
|
|
|
300
317
|
);
|
|
301
318
|
if (multiPhase) {
|
|
302
319
|
lines.push(
|
|
303
|
-
"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
|
|
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`.",
|
|
304
321
|
"",
|
|
305
322
|
);
|
|
306
323
|
}
|
|
@@ -310,21 +327,23 @@ function buildPrompt(job) {
|
|
|
310
327
|
"refineType is \"replace\": suggest a WHOLE-TRANSITION replacement ONLY — do NOT propose motion-token tweaks (no kind \"duration\"/\"delay\"/\"easing\").",
|
|
311
328
|
multiPhase
|
|
312
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."
|
|
313
|
-
: "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 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/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.",
|
|
314
331
|
"Answer in ONE response — do NOT read or search files.",
|
|
315
332
|
);
|
|
316
333
|
} else if (refineType === "both") {
|
|
317
334
|
lines.push(
|
|
318
335
|
"refineType is \"both\": produce TWO independent groups in the SAME suggestions array — the UI shows them in separate tabs, so include each group whenever it applies.",
|
|
319
336
|
"(1) Motion-token tweaks (kind \"duration\"/\"delay\"/\"easing\"/\"scale\"/\"blur\"): for each declaration, propose the token value only where it DIFFERS from the current one. Use \"scale\" for an off-token transform pre-scale and \"blur\" for an off-token filter pre-blur, picked by usage.",
|
|
337
|
+
MISSING_BLUR_NOTE,
|
|
320
338
|
multiPhase
|
|
321
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."
|
|
322
|
-
: "(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\")
|
|
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.",
|
|
323
341
|
"Answer in ONE response — do NOT read or search files.",
|
|
324
342
|
);
|
|
325
343
|
} else {
|
|
326
344
|
lines.push(
|
|
327
345
|
"refineType is \"small\": suggest motion-token tweaks ONLY — for each declaration, propose the token value only where it DIFFERS from the current one (kind \"duration\"/\"delay\"/\"easing\"/\"scale\"/\"blur\"). Use \"scale\" for an off-token transform pre-scale and \"blur\" for an off-token filter pre-blur, picked by usage. Do NOT propose a whole-transition replacement (no kind \"replace\") — the Replace tab requests that separately.",
|
|
346
|
+
MISSING_BLUR_NOTE,
|
|
328
347
|
"Answer in ONE response using ONLY the data above. Do NOT read or search files, run tools, spawn subagents, or explore the codebase — the motion tokens contain everything you need. This is a quick judgement, not a coding task.",
|
|
329
348
|
);
|
|
330
349
|
}
|
|
@@ -335,8 +354,8 @@ function buildPrompt(job) {
|
|
|
335
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":"…"}.',
|
|
336
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.",
|
|
337
356
|
multiPhase
|
|
338
|
-
? '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".'
|
|
339
|
-
: '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.',
|
|
340
359
|
);
|
|
341
360
|
return lines.join("\n");
|
|
342
361
|
}
|
|
@@ -484,7 +503,7 @@ function buildApplyPrompt(job) {
|
|
|
484
503
|
"",
|
|
485
504
|
"Steps:",
|
|
486
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.",
|
|
487
|
-
"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.",
|
|
488
507
|
"3. Make the minimal edit. Do not reformat or change unrelated code.",
|
|
489
508
|
"",
|
|
490
509
|
'Output ONLY a JSON object — no prose, no markdown fences — shaped exactly like:',
|