transitions-refine 0.3.26 → 0.3.28
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 +191 -58
- package/package.json +1 -1
- package/server/relay.mjs +14 -10
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,32 +2610,55 @@
|
|
|
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);
|
|
2601
2634
|
if(sc){const inner=sc[1].trim();const vm=inner.match(/var\(\s*(--[\w-]+)/);
|
|
2602
2635
|
const num=vm?parseFloat(cs.getPropertyValue(vm[1])):parseFloat(inner);
|
|
2603
|
-
// pick the value furthest from the resting 1 (the real pre-scale)
|
|
2604
|
-
|
|
2605
|
-
|
|
2636
|
+
// pick the value furthest from the resting 1 (the real pre-scale);
|
|
2637
|
+
// a var-backed scale at rest (1) still reports its var — the hook
|
|
2638
|
+
// for ADDING a start-scale the source neutralized (e.g. icon swap).
|
|
2639
|
+
if(Number.isFinite(num)&&(Math.abs(num-1)>1e-4||vm)){
|
|
2640
|
+
const better=!bestScale||Math.abs(num-1)>Math.abs(bestScale.scale-1)||(bestScale.varName==null&&vm);
|
|
2641
|
+
if(better)bestScale={scale:num,...(vm?{varName:vm[1]}:{})};}}
|
|
2642
|
+
const tr=tm[1].match(/translate(?:X|Y|3d)?\(\s*([^),]+)/i);
|
|
2643
|
+
if(tr){const inner=tr[1].trim();const vm=inner.match(/var\(\s*(--[\w-]+)/);
|
|
2644
|
+
const vn=vm?rootVar(vm[1]):null;
|
|
2645
|
+
// distance is a magnitude — direction lives in the CSS (± calc).
|
|
2646
|
+
const num=Math.abs(vn?parseFloat(cs.getPropertyValue(vn)):parseFloat(inner));
|
|
2647
|
+
if(Number.isFinite(num)&&num>1e-4&&(!bestTranslate||num>bestTranslate.translate))
|
|
2648
|
+
bestTranslate={translate:num,...(vn?{translateVarName:vn}:{})};}}
|
|
2606
2649
|
}else{
|
|
2607
2650
|
const fm=txt.match(/filter\s*:\s*([^;}]+)/i);
|
|
2608
2651
|
if(fm){const bl=fm[1].match(/blur\(\s*([^)]+)\)/i);
|
|
2609
2652
|
if(bl){const inner=bl[1].trim();const vm=inner.match(/var\(\s*(--[\w-]+)/);
|
|
2610
2653
|
const num=vm?parseFloat(cs.getPropertyValue(vm[1])):parseFloat(inner);
|
|
2611
|
-
|
|
2654
|
+
// var-backed 0px still reports the var (the hook for ADDING blur).
|
|
2655
|
+
if(Number.isFinite(num)&&(num>1e-4||vm)&&(!bestBlur||num>bestBlur.blur))
|
|
2612
2656
|
bestBlur={blur:num,...(vm?{varName:vm[1]}:{})};}}
|
|
2613
2657
|
}
|
|
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);}
|
|
2658
|
+
}
|
|
2620
2659
|
if(bestScale)Object.assign(out,bestScale);
|
|
2621
2660
|
if(bestBlur)Object.assign(out,bestBlur);
|
|
2661
|
+
if(bestTranslate)Object.assign(out,bestTranslate);
|
|
2622
2662
|
}catch{}
|
|
2623
2663
|
return out;
|
|
2624
2664
|
}
|
|
@@ -3240,7 +3280,10 @@
|
|
|
3240
3280
|
"Updates ",
|
|
3241
3281
|
s.patches.map((pp,pi)=>h("span",{className:"tl-sug-phase",key:pi},
|
|
3242
3282
|
(cap(pp.phase)||pp.phase||"Phase"),
|
|
3243
|
-
pp.durationMs!=null&&h("span",{className:"tl-sug-phase-dur"},pp.durationMs+"ms")
|
|
3283
|
+
pp.durationMs!=null&&h("span",{className:"tl-sug-phase-dur"},pp.durationMs+"ms"),
|
|
3284
|
+
pp.scale!=null&&h("span",{className:"tl-sug-phase-dur"},"scale "+pp.scale),
|
|
3285
|
+
pp.blur!=null&&h("span",{className:"tl-sug-phase-dur"},"blur "+pp.blur+"px"),
|
|
3286
|
+
pp.translate!=null&&h("span",{className:"tl-sug-phase-dur"},"slide "+pp.translate+"px")))))
|
|
3244
3287
|
: h(React.Fragment,null,
|
|
3245
3288
|
s.title&&h("div",{className:"tl-sug-title"},s.title),
|
|
3246
3289
|
(s.from||s.to)&&h("div",{className:"tl-sug-delta"},
|
|
@@ -3532,14 +3575,21 @@
|
|
|
3532
3575
|
const base=tgt.baseLanes||[];
|
|
3533
3576
|
const bmap=new Map(base.map(t=>[t.laneId,t]));
|
|
3534
3577
|
for(const t of (tgt.effectiveTimings||[])){
|
|
3535
|
-
const b=bmap.get(t.laneId);
|
|
3536
|
-
|
|
3578
|
+
const b=bmap.get(t.laneId);
|
|
3579
|
+
// a SYNTHETIC lane (recipe added a property the source never animated,
|
|
3580
|
+
// e.g. blur on a filter-less transition) has no base — write it as an
|
|
3581
|
+
// addition: the lane's timing plus its value(s) below.
|
|
3582
|
+
if(!b&&!t.synthetic)continue;
|
|
3583
|
+
if(!b){
|
|
3584
|
+
out.push({property:t.property,member:t.member||null,selector:t.selector||null,phase:tgt.phase||null,added:true,
|
|
3585
|
+
from:null,to:{durationMs:t.durationMs,delayMs:t.delayMs,easing:t.easing}});
|
|
3586
|
+
}else if(t.durationMs!==b.durationMs||t.delayMs!==b.delayMs||(t.easing||"")!==(b.easing||"")){
|
|
3537
3587
|
out.push({property:t.property,member:t.member||null,selector:t.selector||null,phase:tgt.phase||null,
|
|
3538
3588
|
from:{durationMs:b.durationMs,delayMs:b.delayMs,easing:b.easing},
|
|
3539
3589
|
to:{durationMs:t.durationMs,delayMs:t.delayMs,easing:t.easing}});
|
|
3540
3590
|
}
|
|
3541
|
-
// value edits (scale/blur) live as overrides on the effective
|
|
3542
|
-
// carry their own from-value + backing var; emit one change each.
|
|
3591
|
+
// value edits (scale/blur/translate) live as overrides on the effective
|
|
3592
|
+
// lane and carry their own from-value + backing var; emit one change each.
|
|
3543
3593
|
if(t.scale!=null){
|
|
3544
3594
|
out.push({property:t.property,member:t.member||null,selector:t.selector||null,phase:tgt.phase||null,varName:t.varName||null,
|
|
3545
3595
|
from:{scale:t.scaleFrom},to:{scale:t.scale}});
|
|
@@ -3548,6 +3598,10 @@
|
|
|
3548
3598
|
out.push({property:t.property,member:t.member||null,selector:t.selector||null,phase:tgt.phase||null,varName:t.varName||null,
|
|
3549
3599
|
from:{blur:t.blurFrom},to:{blur:t.blur}});
|
|
3550
3600
|
}
|
|
3601
|
+
if(t.translate!=null){
|
|
3602
|
+
out.push({property:t.property,member:t.member||null,selector:t.selector||null,phase:tgt.phase||null,varName:t.translateVarName||null,
|
|
3603
|
+
from:{translate:t.translateFrom},to:{translate:t.translate}});
|
|
3604
|
+
}
|
|
3551
3605
|
}
|
|
3552
3606
|
}
|
|
3553
3607
|
return out;
|
|
@@ -4602,7 +4656,10 @@
|
|
|
4602
4656
|
const[refineLabel,setRefineLabel]=useState(null);
|
|
4603
4657
|
const[appliedIds,setAppliedIds]=useState({});
|
|
4604
4658
|
const[refineMode,setRefineMode]=useState("llm"); // llm (Agent) | deterministic
|
|
4605
|
-
|
|
4659
|
+
// small | replace — last selected tab persists (same pattern as tl-theme-pref)
|
|
4660
|
+
const[refineType,setRefineType]=useState(()=>{
|
|
4661
|
+
try{const v=localStorage.getItem("tl-refine-tab-pref");return v==="small"||v==="replace"?v:"small";}catch{return "small";}
|
|
4662
|
+
});
|
|
4606
4663
|
const[llmAvailable,setLlmAvailable]=useState(null); // null=unknown, true/false from /health
|
|
4607
4664
|
const[cliInstalled,setCliInstalled]=useState(null); // null=unknown, true/false from /health
|
|
4608
4665
|
const[agentReason,setAgentReason]=useState(null); // why no CLI is wired (/health agentReason)
|
|
@@ -4741,7 +4798,8 @@
|
|
|
4741
4798
|
// attach the lane's non-resting scale/blur value (+ backing var) so the
|
|
4742
4799
|
// agent + deterministic engine can refine transform/filter VALUES too.
|
|
4743
4800
|
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}:{})
|
|
4801
|
+
...(v.scale!=null?{scale:v.scale}:{}),...(v.blur!=null?{blur:v.blur}:{}),...(v.varName?{varName:v.varName}:{}),
|
|
4802
|
+
...(v.translate!=null?{translate:v.translate}:{}),...(v.translateVarName?{translateVarName:v.translateVarName}:{})};};
|
|
4745
4803
|
const timings=et.map(withVal);
|
|
4746
4804
|
const selector=(active.bindings&&active.bindings.selector)||(et[0]&&et[0].selector)||null;
|
|
4747
4805
|
// A recipe swap (replace/both) on a grouped phase should update its related
|
|
@@ -4769,6 +4827,7 @@
|
|
|
4769
4827
|
// (idle/error), tabs just preview copy and never auto-scan.
|
|
4770
4828
|
const changeRefineType=useCallback((t)=>{
|
|
4771
4829
|
setRefineType(t);
|
|
4830
|
+
try{localStorage.setItem("tl-refine-tab-pref",t);}catch{}
|
|
4772
4831
|
if(!active)return;
|
|
4773
4832
|
if(scanModeRef.current==="deterministic")return;
|
|
4774
4833
|
if(refinePhase!=="done"&&refinePhase!=="scanning")return;
|
|
@@ -4818,6 +4877,27 @@
|
|
|
4818
4877
|
: (s.patch&&s.patch.property?[{...s.patch,phase:(active&&active.phase)||null}]:[]);
|
|
4819
4878
|
if(!patches.length)return;
|
|
4820
4879
|
const groupId=active&&active.groupId;
|
|
4880
|
+
// Recipe timing per phase: a replace emits a phase-level timing patch
|
|
4881
|
+
// (property:"all", duration+easing) plus lane-level value patches. An
|
|
4882
|
+
// ADDED lane (e.g. a blur/filter lane the source never had) must ride the
|
|
4883
|
+
// recipe's phase timing — NOT the source lane's leftover timing — so the
|
|
4884
|
+
// whole recipe moves in lockstep (e.g. filter deblurs exactly as the page
|
|
4885
|
+
// finishes sliding, instead of lagging on the old duration). Collect the
|
|
4886
|
+
// phase timing up front so synthesis is order-independent.
|
|
4887
|
+
const phaseTiming=new Map(); // phaseKey -> {durationMs,delayMs,easing}
|
|
4888
|
+
for(const p of patches){
|
|
4889
|
+
if(p.durationMs==null&&p.easing==null&&p.delayMs==null)continue;
|
|
4890
|
+
const key=p.phase||"";
|
|
4891
|
+
const cur=phaseTiming.get(key)||{};
|
|
4892
|
+
// a coarse property:"all" patch describes the whole phase; prefer it.
|
|
4893
|
+
if(p.property==="all"||!phaseTiming.has(key)){
|
|
4894
|
+
phaseTiming.set(key,{
|
|
4895
|
+
durationMs:p.durationMs??cur.durationMs,
|
|
4896
|
+
delayMs:p.delayMs??cur.delayMs,
|
|
4897
|
+
easing:p.easing??cur.easing,
|
|
4898
|
+
});
|
|
4899
|
+
}
|
|
4900
|
+
}
|
|
4821
4901
|
for(const p of patches){
|
|
4822
4902
|
if(!p.property)continue;
|
|
4823
4903
|
const timingOverride={};
|
|
@@ -4835,6 +4915,7 @@
|
|
|
4835
4915
|
// map the property patch onto the matching lane(s) — a property can span
|
|
4836
4916
|
// several members in a grouped phase, so apply to each.
|
|
4837
4917
|
const et=target.effectiveTimings||[];
|
|
4918
|
+
const memberLanes=p.member?et.filter(t=>(t.member===p.member||t.memberId===p.member)):et;
|
|
4838
4919
|
let lanes=p.property==="all"?et:et.filter(t=>t.property===p.property);
|
|
4839
4920
|
if(p.member)lanes=lanes.filter(t=>(t.member===p.member||t.memberId===p.member));
|
|
4840
4921
|
const ids=lanes.length?lanes.map(t=>t.laneId):[p.property];
|
|
@@ -4842,31 +4923,79 @@
|
|
|
4842
4923
|
for(const id of ids)registry.setPropOverride(target.id,id,timingOverride);
|
|
4843
4924
|
}
|
|
4844
4925
|
|
|
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
|
-
|
|
4926
|
+
// Value edits (scale/blur/translate) must land on concrete lanes so live
|
|
4927
|
+
// preview can write the right backing var. For coarse recipe patches that
|
|
4928
|
+
// use property:"all" and omit varName, fan out by lane type. When a
|
|
4929
|
+
// recipe prescribes a value the source has NO lane for (e.g. blur on a
|
|
4930
|
+
// transition that never animated `filter`), SYNTHESIZE the lane: an
|
|
4931
|
+
// override keyed by a fresh laneId that carries its own property, member
|
|
4932
|
+
// and timing, which the registry surfaces as a new effective lane.
|
|
4933
|
+
// The backing var is probed from the live stylesheets at apply time
|
|
4934
|
+
// (readElValueHint reports var-backed values even at their resting 0),
|
|
4935
|
+
// so the patch doesn't need to know the page's CSS custom properties.
|
|
4936
|
+
const sibling=(prop)=>memberLanes.find(t=>t.property===prop)||memberLanes[0]||et[0]||null;
|
|
4937
|
+
// Prefer this patch's own timing, then the recipe's phase timing, then
|
|
4938
|
+
// the sibling lane's timing — so an added lane matches the recipe, not
|
|
4939
|
+
// the source's stale duration/easing.
|
|
4940
|
+
const pt=phaseTiming.get(p.phase||"")||{};
|
|
4941
|
+
// recipe timing to stamp on a value lane (blur/scale/translate) so it
|
|
4942
|
+
// rides the recipe duration/easing, not the source's leftover timing.
|
|
4943
|
+
const recipeTiming=()=>{const t={};if(pt.durationMs!=null)t.durationMs=pt.durationMs;if(pt.delayMs!=null)t.delayMs=pt.delayMs;if(pt.easing!=null)t.easing=pt.easing;return t;};
|
|
4944
|
+
const synthTiming=(base)=>({
|
|
4945
|
+
durationMs:p.durationMs??pt.durationMs??(base&&base.durationMs)??250,
|
|
4946
|
+
delayMs:p.delayMs??pt.delayMs??(base&&base.delayMs)??0,
|
|
4947
|
+
easing:p.easing??pt.easing??(base&&base.easing)??"ease",
|
|
4948
|
+
});
|
|
4949
|
+
const applyValue=(prop,mk)=>{
|
|
4950
|
+
// mk(lane, hint) → override fields for one lane, or null to skip.
|
|
4951
|
+
let vLanes=lanes.filter(t=>t.property===prop);
|
|
4952
|
+
if(!vLanes.length&&p.property==="all")vLanes=et.filter(t=>t.property===prop&&(!p.member||t.member===p.member||t.memberId===p.member));
|
|
4953
|
+
if(vLanes.length){
|
|
4954
|
+
const rt=recipeTiming();
|
|
4955
|
+
for(const lane of vLanes){
|
|
4956
|
+
const hint=laneValueHint(lane.selector,prop);
|
|
4957
|
+
const ov=mk(lane,hint);
|
|
4958
|
+
// an EXISTING lane the recipe reuses (e.g. Page side-by-side's
|
|
4959
|
+
// filter lane at the source's 300ms) must ALSO adopt the recipe's
|
|
4960
|
+
// phase duration/easing, so the value settles in lockstep.
|
|
4961
|
+
if(ov)registry.setPropOverride(target.id,lane.laneId,{...rt,...ov});
|
|
4962
|
+
}
|
|
4963
|
+
return;
|
|
4868
4964
|
}
|
|
4869
|
-
|
|
4965
|
+
// no such lane on this member → synthesize one next to its siblings.
|
|
4966
|
+
// A new filter lane rides the transform (slide) timing per the skill.
|
|
4967
|
+
const sib=sibling(prop==="filter"?"transform":"opacity");
|
|
4968
|
+
if(!sib)return;
|
|
4969
|
+
const hint=laneValueHint(sib.selector,prop);
|
|
4970
|
+
const ov=mk(null,hint);
|
|
4971
|
+
if(!ov)return;
|
|
4972
|
+
const laneId=(sib.memberId?sib.memberId+"::":"")+prop;
|
|
4973
|
+
registry.setPropOverride(target.id,laneId,{
|
|
4974
|
+
property:prop,memberId:sib.memberId??null,member:sib.member??null,selector:sib.selector??null,
|
|
4975
|
+
...synthTiming(sib),...ov,
|
|
4976
|
+
});
|
|
4977
|
+
};
|
|
4978
|
+
if(p.scale!=null)applyValue("transform",(lane,hint)=>{
|
|
4979
|
+
const ov={scale:p.scale,scaleFrom:(lane&&lane.scale)??hint.scale??(s.from!=null?parseFloat(s.from):undefined)};
|
|
4980
|
+
const vn=p.varName??(lane&&lane.varName)??hint.varName;
|
|
4981
|
+
if(vn!=null)ov.varName=vn;
|
|
4982
|
+
return ov;
|
|
4983
|
+
});
|
|
4984
|
+
if(p.blur!=null)applyValue("filter",(lane,hint)=>{
|
|
4985
|
+
const ov={blur:p.blur,blurFrom:(lane&&lane.blur)??hint.blur??0};
|
|
4986
|
+
const vn=p.varName??(lane&&lane.varName)??hint.varName;
|
|
4987
|
+
if(vn!=null)ov.varName=vn;
|
|
4988
|
+
return ov;
|
|
4989
|
+
});
|
|
4990
|
+
if(p.translate!=null)applyValue("transform",(lane,hint)=>{
|
|
4991
|
+
const ov={translate:p.translate,translateFrom:(lane&&lane.translate)??hint.translate??0};
|
|
4992
|
+
const vn=p.translateVarName??p.varName??(lane&&lane.translateVarName)??hint.translateVarName;
|
|
4993
|
+
// a translate distance can only go live through a backing var — the
|
|
4994
|
+
// inline transition string can't express keyframe positions. Still
|
|
4995
|
+
// record the edit (Accept writes it) even when no var exists.
|
|
4996
|
+
if(vn!=null)ov.translateVarName=vn;
|
|
4997
|
+
return ov;
|
|
4998
|
+
});
|
|
4870
4999
|
}
|
|
4871
5000
|
setAppliedIds(prev=>({...prev,[s.id]:true}));
|
|
4872
5001
|
},[registry,active,entries]);
|
|
@@ -4892,11 +5021,13 @@
|
|
|
4892
5021
|
if(typeof document==="undefined")return;
|
|
4893
5022
|
const applied=liveAppliedRef.current;
|
|
4894
5023
|
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
|
-
|
|
5024
|
+
// collect the var-backed scale/blur/translate overrides on a set of lanes
|
|
5025
|
+
// into a {varName: cssValue} map (scale → unitless, blur/translate → px).
|
|
5026
|
+
const lanesToVars=(lanes)=>{const v={};for(const l of (lanes||[])){
|
|
5027
|
+
if(l.varName){
|
|
5028
|
+
if(l.scale!=null)v[l.varName]=String(l.scale);
|
|
5029
|
+
if(l.blur!=null)v[l.varName]=l.blur+"px";}
|
|
5030
|
+
if(l.translate!=null&&l.translateVarName)v[l.translateVarName]=l.translate+"px";}return v;};
|
|
4900
5031
|
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
5032
|
for(const item of entries){
|
|
4902
5033
|
if(item.kind==="phase")continue; // phases handled by the state-scoped effect
|
|
@@ -4962,9 +5093,11 @@
|
|
|
4962
5093
|
if(livePhaseRef.current) txTeardownPhase(livePhaseRef.current);
|
|
4963
5094
|
const store={observers:[],ptr:null,applied:new Map(),pending:false};
|
|
4964
5095
|
livePhaseRef.current=store;
|
|
4965
|
-
const lanesToVars=(lanes)=>{const v={};for(const l of (lanes||[])){
|
|
4966
|
-
if(l.
|
|
4967
|
-
|
|
5096
|
+
const lanesToVars=(lanes)=>{const v={};for(const l of (lanes||[])){
|
|
5097
|
+
if(l.varName){
|
|
5098
|
+
if(l.scale!=null)v[l.varName]=String(l.scale);
|
|
5099
|
+
if(l.blur!=null)v[l.varName]=l.blur+"px";}
|
|
5100
|
+
if(l.translate!=null&&l.translateVarName)v[l.translateVarName]=l.translate+"px";}return v;};
|
|
4968
5101
|
const inUI=el=>el&&el.closest&&el.closest("[data-timeline-panel],[data-tl-ui]");
|
|
4969
5102
|
const safeMatches=(el,sel)=>{ if(!sel)return false; try{return el.matches(sel);}catch{return false;} };
|
|
4970
5103
|
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.28",
|
|
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,16 +265,20 @@ 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",
|
|
271
|
-
" - Icon swap → 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) AND transform scale 0.25 (start-scale) on the swapping icon",
|
|
272
272
|
" - Skeleton loader and reveal → filter blur 2px (Small)",
|
|
273
273
|
" - Texts reveal → filter blur 3px (Medium)",
|
|
274
274
|
" - Success check → filter blur 8px (Large)",
|
|
275
275
|
" - Modal open/close → transform scale 0.96 (Large)",
|
|
276
276
|
" - Menu dropdown → transform scale 0.97 (Medium) open, 0.99 (Tiny) close",
|
|
277
277
|
" - Tooltip open/close → transform scale 0.98 (Small)",
|
|
278
|
+
"Recipe timing & easing — SET the transition to the recipe's documented values; do NOT keep the source's duration/easing when they differ from the recipe (a value being an on-grid token is NOT a reason to keep it if the recipe uses a different one):",
|
|
279
|
+
" - Easing: most surface/slide/reveal recipes use cubic-bezier(0.22, 1, 0.36, 1) (Smooth ease out) — page side-by-side, panel reveal, menu dropdown, modal, tooltip, accordion, tabs sliding, card resize, texts reveal, success check. Cross-fade swaps use ease-in-out — icon swap, text states swap. Overshoot curves stay as documented — notification badge / number pop-in cubic-bezier(0.34, 1.36, 0.64, 1), avatar return cubic-bezier(0.34, 3.85, 0.64, 1).",
|
|
280
|
+
" - SYMMETRIC recipes (SAME duration + easing BOTH directions — do NOT split open/close): page side-by-side (250ms), tabs sliding (250ms), accordion (250ms), icon swap (250ms), text states swap (150ms). Emit identical durationMs/easing for every phase.",
|
|
281
|
+
" - ASYMMETRIC recipes (open slower, close faster): menu dropdown / modal / tooltip / panel reveal → open 250ms, close 150ms.",
|
|
278
282
|
"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.",
|
|
279
283
|
].join("\n");
|
|
280
284
|
|
|
@@ -305,7 +309,7 @@ function buildPrompt(job) {
|
|
|
305
309
|
"",
|
|
306
310
|
"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
311
|
"",
|
|
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
|
|
312
|
+
"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
313
|
"",
|
|
310
314
|
];
|
|
311
315
|
if (needsRecipe) {
|
|
@@ -317,7 +321,7 @@ function buildPrompt(job) {
|
|
|
317
321
|
);
|
|
318
322
|
if (multiPhase) {
|
|
319
323
|
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
|
|
324
|
+
"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). Set each phase's `durationMs` AND `easing` from \"Recipe timing & easing\" above (SYMMETRIC recipes use the SAME duration+easing for every phase — do NOT split open/close; ASYMMETRIC recipes use open 250ms / close 150ms). The added value lanes (blur/translate) automatically inherit their phase's timing on Apply, so you don't need to repeat duration/easing on those lane-level patches. Also include a single `patch` for live preview (first timing patch is fine). Apply will write every phase from `patches`.",
|
|
321
325
|
"",
|
|
322
326
|
);
|
|
323
327
|
}
|
|
@@ -327,7 +331,7 @@ function buildPrompt(job) {
|
|
|
327
331
|
"refineType is \"replace\": suggest a WHOLE-TRANSITION replacement ONLY — do NOT propose motion-token tweaks (no kind \"duration\"/\"delay\"/\"easing\").",
|
|
328
332
|
multiPhase
|
|
329
333
|
? "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
|
|
334
|
+
: "Pick the SINGLE best-fit recipe from the list above. Emit ONE suggestion with kind \"replace\": set its `patch` to the recipe's documented duration AND easing (see \"Recipe timing & easing\" — do NOT keep the source's easing when the recipe differs) 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
335
|
"Answer in ONE response — do NOT read or search files.",
|
|
332
336
|
);
|
|
333
337
|
} else if (refineType === "both") {
|
|
@@ -337,7 +341,7 @@ function buildPrompt(job) {
|
|
|
337
341
|
MISSING_BLUR_NOTE,
|
|
338
342
|
multiPhase
|
|
339
343
|
? "(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
|
|
344
|
+
: "(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 recipe's documented duration AND easing (see \"Recipe timing & easing\" — do NOT keep the source's easing when the recipe differs) 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
345
|
"Answer in ONE response — do NOT read or search files.",
|
|
342
346
|
);
|
|
343
347
|
} else {
|
|
@@ -354,8 +358,8 @@ function buildPrompt(job) {
|
|
|
354
358
|
'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
359
|
"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
360
|
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.',
|
|
361
|
+
? '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".'
|
|
362
|
+
: '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
363
|
);
|
|
360
364
|
return lines.join("\n");
|
|
361
365
|
}
|
|
@@ -503,7 +507,7 @@ function buildApplyPrompt(job) {
|
|
|
503
507
|
"",
|
|
504
508
|
"Steps:",
|
|
505
509
|
"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))
|
|
510
|
+
"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
511
|
"3. Make the minimal edit. Do not reformat or change unrelated code.",
|
|
508
512
|
"",
|
|
509
513
|
'Output ONLY a JSON object — no prose, no markdown fences — shaped exactly like:',
|