transitions-refine 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/demo.html +60 -9
  2. package/package.json +1 -1
package/demo.html CHANGED
@@ -1136,6 +1136,40 @@
1136
1136
  }
1137
1137
 
1138
1138
  // ── preview controller ──
1139
+ // ── transition capture ──
1140
+ // Real-world transitions are usually triggered by interaction (hover, a
1141
+ // click on a *different* button, focus, JS state) — not by clicking the
1142
+ // element that carries the transition. So we observe transitions as they
1143
+ // actually run and remember each property's from/to computed values; the
1144
+ // preview then replays them with no synthetic click required.
1145
+ const _txCapture = new WeakMap(); // Element -> Map<prop,{from,to}>
1146
+ let _txInstalled = false;
1147
+ function _txCamel(p){return p.replace(/-([a-z])/g,(_,c)=>c.toUpperCase());}
1148
+ function _txOnRun(e){
1149
+ const el=e.target, prop=e.propertyName;
1150
+ if(!el||!prop||typeof el.getAnimations!=="function")return;
1151
+ if(el.closest&&el.closest("[data-timeline-panel]"))return;
1152
+ try{
1153
+ const ck=_txCamel(prop);
1154
+ const anims=el.getAnimations();
1155
+ let anim=anims.find(a=>a.transitionProperty===prop);
1156
+ if(!anim)anim=anims.find(a=>{try{const k=a.effect&&a.effect.getKeyframes();return k&&k.length&&(ck in k[0]||ck in k[k.length-1]);}catch{return false;}});
1157
+ if(!anim||!anim.effect)return;
1158
+ const kf=anim.effect.getKeyframes();
1159
+ if(!kf||kf.length<2)return;
1160
+ const from=kf[0][ck], to=kf[kf.length-1][ck];
1161
+ if(from==null||to==null)return;
1162
+ let rec=_txCapture.get(el); if(!rec){rec=new Map();_txCapture.set(el,rec);}
1163
+ rec.set(prop,{from:String(from),to:String(to)});
1164
+ }catch{}
1165
+ }
1166
+ function _txInstallCapture(){
1167
+ if(_txInstalled||typeof document==="undefined")return;
1168
+ _txInstalled=true;
1169
+ // capture phase + the bubbling transitionrun event reaches document
1170
+ document.addEventListener("transitionrun",_txOnRun,true);
1171
+ }
1172
+
1139
1173
  class PreviewController {
1140
1174
  state="idle"; listeners=new Set(); cleanups=[]; animations=[]; progressListeners=new Set(); _rafId=null; scanner=null; _gen=0;
1141
1175
  rate=1; loop=false; _current=null;
@@ -1157,15 +1191,14 @@
1157
1191
  if(entry.bindings.type!=="css")return;
1158
1192
  this.animations=[];
1159
1193
  const et=entry.effectiveTimings||entry.properties.map(p=>({property:p,durationMs:entry.durationMs,delayMs:entry.delayMs,easing:entry.easing}));
1160
- for(const wr of entry.bindings.elements){const el=wr.deref();if(!el)continue;const saved=el.style.transition;
1161
- const tv=et.map(t=>`${t.property} ${formatCssTime(t.durationMs)} ${t.easing} ${formatCssTime(t.delayMs)}`).join(", ");
1162
- el.style.transition=tv;el.click();
1194
+ for(const wr of entry.bindings.elements){const el=wr.deref();if(!el)continue;
1195
+ const restore=this._arm(el,et);
1163
1196
  requestAnimationFrame(()=>{if(this._gen!==gen)return;const running=el.getAnimations();
1164
1197
  for(const a of running){a.pause();a.playbackRate=this.rate;this.animations.push(a);}
1165
1198
  const t=this._pendingSeek??seekMs;
1166
1199
  if(t!=null){for(const a of this.animations){try{a.currentTime=t;}catch{}}this._ep(t);}
1167
1200
  });
1168
- this.cleanups.push(()=>{el.style.transition=saved;});}
1201
+ this.cleanups.push(restore);}
1169
1202
  this._setState("paused");
1170
1203
  if(seekMs!=null)this._ep(seekMs);
1171
1204
  }
@@ -1177,14 +1210,32 @@
1177
1210
  this._pendingSeek=timeMs;
1178
1211
  }
1179
1212
  _finish(){this._stopPL();if(this.animations.length>0){let end=0;for(const a of this.animations){const t=a.effect?.getTiming();const e=(t?.delay??0)+(Number(t?.duration)||0);if(e>end)end=e;}this._ep(end);}this.animations=[];for(const c of this.cleanups)c();this.cleanups=[];if(this.scanner)this.scanner.unpause();this._setState("idle");}
1213
+ // Arm an element so its transition runs now. If we captured the real
1214
+ // transition earlier, replay it from→to (no click, no side effects);
1215
+ // otherwise fall back to clicking the element. Returns a restore fn.
1216
+ _arm(el,et){
1217
+ const saved=el.style.cssText;
1218
+ const tv=et.map(t=>`${t.property} ${formatCssTime(t.durationMs)} ${t.easing} ${formatCssTime(t.delayMs)}`).join(", ");
1219
+ const rec=_txCapture.get(el);
1220
+ const caps=rec?et.map(t=>[t.property,rec.get(t.property)]).filter(x=>x[1]):[];
1221
+ if(caps.length){
1222
+ el.style.transition="none";
1223
+ for(const[p,v]of caps)el.style.setProperty(p,v.from);
1224
+ void el.offsetWidth; // commit the from-state before transitioning
1225
+ el.style.transition=tv;
1226
+ for(const[p,v]of caps)el.style.setProperty(p,v.to);
1227
+ }else{
1228
+ el.style.transition=tv;el.click();
1229
+ }
1230
+ return ()=>{try{el.style.cssText=saved;}catch{}};
1231
+ }
1180
1232
  _playCss(entry,gen){if(entry.bindings.type!=="css")return;this.animations=[];
1181
1233
  const et = entry.effectiveTimings || entry.properties.map(p=>({property:p,durationMs:entry.durationMs,delayMs:entry.delayMs,easing:entry.easing}));
1182
- for(const wr of entry.bindings.elements){const el=wr.deref();if(!el)continue;const saved=el.style.transition;
1183
- const tv=et.map(t=>`${t.property} ${formatCssTime(t.durationMs)} ${t.easing} ${formatCssTime(t.delayMs)}`).join(", ");
1184
- el.style.transition=tv;el.click();
1234
+ for(const wr of entry.bindings.elements){const el=wr.deref();if(!el)continue;
1235
+ const restore=this._arm(el,et);
1185
1236
  requestAnimationFrame(()=>{if(this._gen!==gen)return;const running=el.getAnimations();for(const a of running){a.playbackRate=this.rate;this.animations.push(a);}this._startPL();
1186
1237
  if(running.length>0){Promise.allSettled(running.map(a=>a.finished)).then(()=>{if(this._gen!==gen||this.state!=="playing")return;if(this.loop&&this._current){this.play(this._current);}else{this._finish();}});}else{this._finish();}});
1187
- this.cleanups.push(()=>{el.style.transition=saved;});}}
1238
+ this.cleanups.push(restore);}}
1188
1239
  _startPL(){this._stopPL();const tick=()=>{if(this.animations.length>0){let cur=0;for(const a of this.animations){const c=Number(a.currentTime)||0;if(c>cur)cur=c;}this._ep(cur);}this._rafId=requestAnimationFrame(tick);};this._rafId=requestAnimationFrame(tick);}
1189
1240
  _stopPL(){if(this._rafId!==null){cancelAnimationFrame(this._rafId);this._rafId=null;}}
1190
1241
  _ep(p){for(const fn of this.progressListeners)fn(p);}
@@ -1196,7 +1247,7 @@
1196
1247
  observer=null;rafId=null;running=false;paused=false;
1197
1248
  constructor(root,reg){this.root=root;this.registry=reg;}
1198
1249
  pause(){this.paused=true;} unpause(){this.paused=false;this._sched();}
1199
- start(){if(this.running)return;this.running=true;this.scan();this.observer=new MutationObserver(()=>this._sched());this.observer.observe(this.root,{childList:true,subtree:true,attributes:true,attributeFilter:["class","style"]});}
1250
+ start(){if(this.running)return;this.running=true;_txInstallCapture();this.scan();this.observer=new MutationObserver(()=>this._sched());this.observer.observe(this.root,{childList:true,subtree:true,attributes:true,attributeFilter:["class","style"]});}
1200
1251
  stop(){this.running=false;this.observer?.disconnect();this.observer=null;if(this.rafId!==null){cancelAnimationFrame(this.rafId);this.rafId=null;}}
1201
1252
  _sched(){if(this.rafId!==null)return;this.rafId=requestAnimationFrame(()=>{this.rafId=null;if(this.running&&!this.paused)this.scan();});}
1202
1253
  scan(){const seen=new Map();const w=document.createTreeWalker(this.root,NodeFilter.SHOW_ELEMENT);let n=w.currentNode;while(n){if(n instanceof HTMLElement)this._proc(n,seen);n=w.nextNode();}this.registry.replaceAll(Array.from(seen.values()));}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "transitions-refine",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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": {