transitions-refine 0.1.0 → 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.
- package/demo.html +60 -9
- package/package.json +1 -1
- package/server/inject.mjs +6 -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;
|
|
1161
|
-
const
|
|
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(
|
|
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;
|
|
1183
|
-
const
|
|
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(
|
|
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.
|
|
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": {
|
package/server/inject.mjs
CHANGED
|
@@ -19,6 +19,10 @@ const DEMO_PATH = fileURLToPath(new URL("../demo.html", import.meta.url));
|
|
|
19
19
|
const REACT_URL = "https://esm.sh/react@19";
|
|
20
20
|
const REACT_DOM_URL = "https://esm.sh/react-dom@19";
|
|
21
21
|
const REACT_DOM_CLIENT_URL = "https://esm.sh/react-dom@19/client";
|
|
22
|
+
// `deps=` (not `external=`) so esm.sh resolves border-beam's own react/react-dom
|
|
23
|
+
// to the same esm.sh/react@19 the panel uses — one React instance, no import map
|
|
24
|
+
// required on the host page.
|
|
25
|
+
const BORDER_BEAM_URL = "https://esm.sh/border-beam@1.2.0?deps=react@19,react-dom@19";
|
|
22
26
|
|
|
23
27
|
const CUT_MARKER = "// ── demo boxes ──";
|
|
24
28
|
|
|
@@ -46,7 +50,8 @@ function buildJs(scriptSrc) {
|
|
|
46
50
|
js = js
|
|
47
51
|
.replace(/import\s+React\s+from\s+["']react["'];?/, `import React from "${REACT_URL}";`)
|
|
48
52
|
.replace(/import\s+\{\s*createRoot\s*\}\s+from\s+["']react-dom\/client["'];?/, `import { createRoot } from "${REACT_DOM_CLIENT_URL}";`)
|
|
49
|
-
.replace(/import\s+\{\s*createPortal\s*\}\s+from\s+["']react-dom["'];?/, `import { createPortal } from "${REACT_DOM_URL}";`)
|
|
53
|
+
.replace(/import\s+\{\s*createPortal\s*\}\s+from\s+["']react-dom["'];?/, `import { createPortal } from "${REACT_DOM_URL}";`)
|
|
54
|
+
.replace(/import\s+\{\s*BorderBeam\s*\}\s+from\s+["']border-beam["'];?/, `import { BorderBeam } from "${BORDER_BEAM_URL}";`);
|
|
50
55
|
|
|
51
56
|
// Point the relay client at whatever origin served this module, so the panel
|
|
52
57
|
// works on any port the CLI chose (the script is served BY the relay).
|