transitions-refine 0.3.16 → 0.3.18
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/bin/cli.mjs +14 -2
- package/demo.html +162 -16
- package/package.json +1 -1
package/bin/cli.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// 4. starts the local refine relay (serves the panel at /inject.js).
|
|
20
20
|
|
|
21
21
|
import { spawn, spawnSync } from "node:child_process";
|
|
22
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync } from "node:fs";
|
|
22
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, realpathSync } from "node:fs";
|
|
23
23
|
import { dirname, join, resolve } from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
25
|
import { homedir } from "node:os";
|
|
@@ -387,7 +387,19 @@ function main() {
|
|
|
387
387
|
|
|
388
388
|
// Run only when invoked as the CLI entry (npx / node bin/cli.mjs), so tests can
|
|
389
389
|
// import the resolver helpers without triggering a live run.
|
|
390
|
-
|
|
390
|
+
// Run only when invoked as the CLI entry (npx / global install / node bin/cli.mjs).
|
|
391
|
+
// We must resolve symlinks: npx and global installs invoke this via a `.bin/refine`
|
|
392
|
+
// symlink, so process.argv[1] is the symlink path while import.meta.url is the real
|
|
393
|
+
// file. Comparing raw paths fails through the symlink and silently skips main().
|
|
394
|
+
function isCliEntry() {
|
|
395
|
+
if (!process.argv[1]) return false;
|
|
396
|
+
const self = fileURLToPath(import.meta.url);
|
|
397
|
+
const resolve = (p) => {
|
|
398
|
+
try { return realpathSync(p); } catch { return p; }
|
|
399
|
+
};
|
|
400
|
+
return resolve(process.argv[1]) === resolve(self);
|
|
401
|
+
}
|
|
402
|
+
if (isCliEntry()) {
|
|
391
403
|
main();
|
|
392
404
|
}
|
|
393
405
|
|
package/demo.html
CHANGED
|
@@ -1799,6 +1799,63 @@
|
|
|
1799
1799
|
return out;
|
|
1800
1800
|
}
|
|
1801
1801
|
|
|
1802
|
+
// Resolve a grouped phase MEMBER selector to live element(s), SCOPED to the
|
|
1803
|
+
// component instance(s) named by the phase's `stateTarget`. Agent-produced
|
|
1804
|
+
// member selectors are often broad (e.g. `.panel`, a utility class) and, when
|
|
1805
|
+
// queried globally, match elements in OTHER components — so editing one
|
|
1806
|
+
// group's timing leaked onto unrelated transitions, and claiming hid the
|
|
1807
|
+
// wrong flat entries. Restricting the query to each stateTarget subtree keeps
|
|
1808
|
+
// the match inside the intended component (all instances of it, which is
|
|
1809
|
+
// correct) without touching look-alikes elsewhere. Falls back to a global
|
|
1810
|
+
// query only when scoping yields nothing (no/again-unresolvable stateTarget),
|
|
1811
|
+
// so this can only ever NARROW matches — never regress to "applies nothing".
|
|
1812
|
+
function txMemberEls(memberSel, stateTargetSel){
|
|
1813
|
+
if(!memberSel) return [];
|
|
1814
|
+
const out=[];
|
|
1815
|
+
if(stateTargetSel){
|
|
1816
|
+
let roots=[];try{roots=Array.from(document.querySelectorAll(stateTargetSel));}catch{}
|
|
1817
|
+
for(const root of roots){
|
|
1818
|
+
try{ if(root.matches&&root.matches(memberSel)) out.push(root); }catch{}
|
|
1819
|
+
try{ for(const el of root.querySelectorAll(memberSel)) out.push(el); }catch{}
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
const scoped=Array.from(new Set(out));
|
|
1823
|
+
if(scoped.length) return scoped;
|
|
1824
|
+
let all=[];try{all=Array.from(document.querySelectorAll(memberSel));}catch{}
|
|
1825
|
+
return all;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
// Normalize a phase state token (the class/attribute toggled on `stateTarget`)
|
|
1829
|
+
// into a CSS selector fragment that can be concatenated onto a selector:
|
|
1830
|
+
// ".is-open" → ".is-open" (class, already a fragment)
|
|
1831
|
+
// "[data-open=\"true\"]"→ "[data-open=\"true\"]" (attribute, already a fragment)
|
|
1832
|
+
// "is-open" → ".is-open" (bare class name)
|
|
1833
|
+
// "data-open" → "[data-open]" (bare attribute name)
|
|
1834
|
+
// null / "" / "base" → "" (the no-class base state)
|
|
1835
|
+
function stateSuffix(tok){
|
|
1836
|
+
if(tok==null) return "";
|
|
1837
|
+
tok=String(tok).trim();
|
|
1838
|
+
if(!tok||/^base$/i.test(tok)) return "";
|
|
1839
|
+
// already a selector fragment: .class, [attr], :pseudo (e.g. :hover)
|
|
1840
|
+
if(tok[0]==="."||tok[0]==="["||tok[0]===":") return tok;
|
|
1841
|
+
if(/^[\w-]+$/.test(tok)) return "."+tok;
|
|
1842
|
+
return "["+tok+"]";
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// Tear down a state-gated phase live-preview store: stop observers/listeners
|
|
1846
|
+
// and restore every element's original inline transition + custom props.
|
|
1847
|
+
function txTeardownPhase(store){
|
|
1848
|
+
if(!store) return;
|
|
1849
|
+
if(store) store.pending=false;
|
|
1850
|
+
for(const mo of store.observers||[]){try{mo.disconnect();}catch{}}
|
|
1851
|
+
if(store.ptr){try{document.removeEventListener("pointerover",store.ptr,true);document.removeEventListener("pointerout",store.ptr,true);}catch{}}
|
|
1852
|
+
for(const [el,rec] of store.applied||new Map()){
|
|
1853
|
+
try{el.style.transition=rec.prevInline;}catch{}
|
|
1854
|
+
for(const n of Object.keys(rec.prevVars||{})){try{const pv=rec.prevVars[n];if(pv)el.style.setProperty(n,pv);else el.style.removeProperty(n);}catch{}}
|
|
1855
|
+
}
|
|
1856
|
+
store.applied&&store.applied.clear();
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1802
1859
|
// ── registry (per-lane overrides) ──
|
|
1803
1860
|
// A "lane" is one animated property of one element. For flat DOM transitions
|
|
1804
1861
|
// a lane's id is just the property; for agent-grouped phases it is
|
|
@@ -1836,8 +1893,7 @@
|
|
|
1836
1893
|
const memberEls=new Set();
|
|
1837
1894
|
for(const g of this.groups)for(const ph of (g.phases||[]))for(const m of (ph.members||[])){
|
|
1838
1895
|
if(!m.selector)continue;
|
|
1839
|
-
|
|
1840
|
-
for(const el of els)memberEls.add(el);
|
|
1896
|
+
for(const el of txMemberEls(m.selector,ph.stateTarget))memberEls.add(el);
|
|
1841
1897
|
}
|
|
1842
1898
|
if(!memberEls.size)return;
|
|
1843
1899
|
for(const[id,entry]of this.entries){
|
|
@@ -3955,8 +4011,14 @@
|
|
|
3955
4011
|
// ── live styling ──────────────────────────────────────────────────────
|
|
3956
4012
|
// With playback gone, the real component IS the preview. So whenever a
|
|
3957
4013
|
// transition has edits, mirror its effective timings onto the live
|
|
3958
|
-
// element(s)
|
|
3959
|
-
//
|
|
4014
|
+
// element(s) and revert when edits are cleared. Interacting with the
|
|
4015
|
+
// component then animates with the edited values.
|
|
4016
|
+
//
|
|
4017
|
+
// FLAT entries (single-state DOM transitions) are mirrored as an inline
|
|
4018
|
+
// `transition` on their exact bound elements — precise and simple. GROUPED
|
|
4019
|
+
// phases are NOT handled here: open and close share the same base element,
|
|
4020
|
+
// so a single inline slot can't tell them apart (editing open would also
|
|
4021
|
+
// change close). Phases use a state-scoped <style> instead (effect below).
|
|
3960
4022
|
const liveAppliedRef=useRef(new Map()); // el → {prevInline, css}
|
|
3961
4023
|
useEffect(()=>{
|
|
3962
4024
|
if(typeof document==="undefined")return;
|
|
@@ -3969,20 +4031,13 @@
|
|
|
3969
4031
|
if(l.blur!=null)v[l.varName]=l.blur+"px";}return v;};
|
|
3970
4032
|
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||{}}); } };
|
|
3971
4033
|
for(const item of entries){
|
|
4034
|
+
if(item.kind==="phase")continue; // phases handled by the state-scoped effect
|
|
3972
4035
|
const ov=registry.getPropOverrides(item.id);
|
|
3973
4036
|
if(!ov||!Object.keys(ov).length)continue; // only edited transitions go live
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
add(els,transitionLanesToCss(m.lanes),lanesToVars(m.lanes));
|
|
3979
|
-
}
|
|
3980
|
-
}else{
|
|
3981
|
-
const lanes=item.effectiveTimings||[];
|
|
3982
|
-
if(!lanes.length)continue;
|
|
3983
|
-
const els=((item.bindings&&item.bindings.elements)||[]).map(w=>w.deref&&w.deref()).filter(Boolean);
|
|
3984
|
-
add(els,transitionLanesToCss(lanes),lanesToVars(lanes));
|
|
3985
|
-
}
|
|
4037
|
+
const lanes=item.effectiveTimings||[];
|
|
4038
|
+
if(!lanes.length)continue;
|
|
4039
|
+
const els=((item.bindings&&item.bindings.elements)||[]).map(w=>w.deref&&w.deref()).filter(Boolean);
|
|
4040
|
+
add(els,transitionLanesToCss(lanes),lanesToVars(lanes));
|
|
3986
4041
|
}
|
|
3987
4042
|
const varsKey=v=>Object.keys(v).sort().map(k=>k+":"+v[k]).join(";");
|
|
3988
4043
|
// apply new / changed
|
|
@@ -4019,6 +4074,97 @@
|
|
|
4019
4074
|
for(const [el,rec] of applied){ try{el.style.transition=rec.prevInline;}catch{} for(const n of Object.keys(rec.vars||{})){try{const pv=(rec.prevVars||{})[n];if(pv)el.style.setProperty(n,pv);else el.style.removeProperty(n);}catch{}} _TX_LIVE_BASE.delete(el); }
|
|
4020
4075
|
applied.clear();
|
|
4021
4076
|
},[]);
|
|
4077
|
+
// ── live styling: grouped phases (state-gated) ──────────────────────────
|
|
4078
|
+
// Open and close of one component share the SAME element(s), so a single
|
|
4079
|
+
// inline `transition` can't separate them (editing open would bend close
|
|
4080
|
+
// too). And a member often lives OUTSIDE its stateTarget (a caret in the
|
|
4081
|
+
// trigger, a backdrop portal'd to <body>), so a CSS descendant rule can't
|
|
4082
|
+
// reach it. So we GATE on state instead: watch each phase's `stateTarget`
|
|
4083
|
+
// for its destination state and apply the member's edited timing inline
|
|
4084
|
+
// ONLY while that state is active, reverting otherwise:
|
|
4085
|
+
// • OPEN (base→toState): apply while stateTarget IS in toState.
|
|
4086
|
+
// • CLOSE (fromState→base): apply while stateTarget is NOT in fromState.
|
|
4087
|
+
// This works regardless of where the member sits in the DOM and keeps the
|
|
4088
|
+
// two directions independent. A MutationObserver on each stateTarget (class
|
|
4089
|
+
// + state data-attrs, never `style`, so no feedback) re-runs the gate;
|
|
4090
|
+
// hover phases also watch pointer events. Everything reverts on teardown.
|
|
4091
|
+
const livePhaseRef=useRef(null);
|
|
4092
|
+
useEffect(()=>{
|
|
4093
|
+
if(typeof document==="undefined")return;
|
|
4094
|
+
if(livePhaseRef.current) txTeardownPhase(livePhaseRef.current);
|
|
4095
|
+
const store={observers:[],ptr:null,applied:new Map(),pending:false};
|
|
4096
|
+
livePhaseRef.current=store;
|
|
4097
|
+
const lanesToVars=(lanes)=>{const v={};for(const l of (lanes||[])){if(!l.varName)continue;
|
|
4098
|
+
if(l.scale!=null)v[l.varName]=String(l.scale);
|
|
4099
|
+
if(l.blur!=null)v[l.varName]=l.blur+"px";}return v;};
|
|
4100
|
+
const inUI=el=>el&&el.closest&&el.closest("[data-timeline-panel],[data-tl-ui]");
|
|
4101
|
+
const safeMatches=(el,sel)=>{ if(!sel)return false; try{return el.matches(sel);}catch{return false;} };
|
|
4102
|
+
const fresh=sel=>{ if(!sel)return []; try{return Array.from(document.querySelectorAll(sel)).filter(e=>!inUI(e));}catch{return [];} };
|
|
4103
|
+
// Specs hold SELECTORS, not resolved elements: the stateTarget panel and
|
|
4104
|
+
// members frequently unmount/remount on toggle, so we re-resolve them on
|
|
4105
|
+
// every recompute to stay correct across that churn.
|
|
4106
|
+
const specs=[]; const attrNames=new Set(["class"]); let anyHover=false;
|
|
4107
|
+
for(const item of entries){
|
|
4108
|
+
if(item.kind!=="phase")continue;
|
|
4109
|
+
const ov=registry.getPropOverrides(item.id);
|
|
4110
|
+
if(!ov||!Object.keys(ov).length)continue; // only edited phases go live
|
|
4111
|
+
const toSuf=stateSuffix(item.toState), fromSuf=stateSuffix(item.fromState);
|
|
4112
|
+
// collect attribute names referenced by the state tokens so the observer
|
|
4113
|
+
// fires on e.g. data-open / aria-expanded toggles, not just class.
|
|
4114
|
+
for(const suf of [toSuf,fromSuf]){const am=suf&&suf.match(/^\[([\w-]+)/);if(am)attrNames.add(am[1]);}
|
|
4115
|
+
if(/:hover/.test(toSuf)||/:hover/.test(fromSuf))anyHover=true;
|
|
4116
|
+
const members=[];
|
|
4117
|
+
for(const m of (item.members||[])){
|
|
4118
|
+
if(!m.selector||!m.lanes||!m.lanes.length)continue;
|
|
4119
|
+
members.push({selector:m.selector,css:transitionLanesToCss(m.lanes),vars:lanesToVars(m.lanes)});
|
|
4120
|
+
}
|
|
4121
|
+
if(!members.length)continue;
|
|
4122
|
+
specs.push({stateTarget:item.stateTarget||null,toSuf,fromSuf,members});
|
|
4123
|
+
}
|
|
4124
|
+
if(!specs.length) return ()=>txTeardownPhase(store);
|
|
4125
|
+
const varsKey=v=>Object.keys(v).sort().map(k=>k+":"+v[k]).join(";");
|
|
4126
|
+
const recompute=()=>{
|
|
4127
|
+
const desired=new Map(); // el → {css,vars}
|
|
4128
|
+
for(const sp of specs){
|
|
4129
|
+
const stEls=fresh(sp.stateTarget);
|
|
4130
|
+
// is THIS phase's destination state active on the stateTarget RIGHT NOW?
|
|
4131
|
+
let active;
|
|
4132
|
+
if(sp.toSuf) active=stEls.length?stEls.some(e=>safeMatches(e,sp.toSuf)):true; // open
|
|
4133
|
+
else if(sp.fromSuf) active=stEls.length?stEls.some(e=>!safeMatches(e,sp.fromSuf)):true; // close = not in fromState
|
|
4134
|
+
else active=true; // no state info → always reflect (degrades to old behavior)
|
|
4135
|
+
if(!active)continue;
|
|
4136
|
+
for(const mm of sp.members) for(const el of txMemberEls(mm.selector,sp.stateTarget)){ if(inUI(el))continue; desired.set(el,{css:mm.css,vars:mm.vars}); }
|
|
4137
|
+
}
|
|
4138
|
+
for(const [el,d] of desired){
|
|
4139
|
+
const vars=d.vars||{},vk=varsKey(vars),cur=store.applied.get(el);
|
|
4140
|
+
if(cur&&cur.css===d.css&&cur.vk===vk)continue;
|
|
4141
|
+
if(!cur) store.applied.set(el,{prevInline:el.style.transition||"",prevVars:Object.fromEntries(Object.keys(vars).map(n=>[n,el.style.getPropertyValue(n)])),css:d.css,vk});
|
|
4142
|
+
else store.applied.set(el,{prevInline:cur.prevInline,prevVars:cur.prevVars,css:d.css,vk});
|
|
4143
|
+
try{el.style.transition=d.css;}catch{}
|
|
4144
|
+
for(const n of Object.keys(vars)){try{el.style.setProperty(n,vars[n]);}catch{}}
|
|
4145
|
+
}
|
|
4146
|
+
for(const [el,rec] of Array.from(store.applied)){
|
|
4147
|
+
if(desired.has(el))continue;
|
|
4148
|
+
try{el.style.transition=rec.prevInline;}catch{}
|
|
4149
|
+
for(const n of Object.keys(rec.prevVars||{})){try{const pv=rec.prevVars[n];if(pv)el.style.setProperty(n,pv);else el.style.removeProperty(n);}catch{}}
|
|
4150
|
+
store.applied.delete(el);
|
|
4151
|
+
}
|
|
4152
|
+
};
|
|
4153
|
+
// Coalesce via microtask (NOT rAF): the gate must update the moment the
|
|
4154
|
+
// state class/attr flips — before the browser styles the next frame — so
|
|
4155
|
+
// the very first interaction animates with the edited timing, and reverts
|
|
4156
|
+
// are instant even when the tab is backgrounded (rAF pauses there).
|
|
4157
|
+
const queueMt=typeof queueMicrotask==="function"?queueMicrotask:(f)=>Promise.resolve().then(f);
|
|
4158
|
+
const schedule=()=>{ if(store.pending)return; store.pending=true; queueMt(()=>{store.pending=false;recompute();}); };
|
|
4159
|
+
// ONE observer on the document subtree: state lives on elements that may
|
|
4160
|
+
// mount/unmount (panels) or just toggle a class/attr, so we watch class +
|
|
4161
|
+
// the state attrs across the tree, plus childList for remounts. Never
|
|
4162
|
+
// `style` → our own inline writes can't feed back. rAF-coalesced.
|
|
4163
|
+
try{ const mo=new MutationObserver(schedule); mo.observe(document.body,{attributes:true,attributeFilter:Array.from(attrNames),subtree:true,childList:true}); store.observers.push(mo); }catch{}
|
|
4164
|
+
if(anyHover){ const onPtr=()=>schedule(); document.addEventListener("pointerover",onPtr,true); document.addEventListener("pointerout",onPtr,true); store.ptr=onPtr; }
|
|
4165
|
+
recompute(); // initial pass
|
|
4166
|
+
return ()=>txTeardownPhase(store);
|
|
4167
|
+
},[entries,registry]);
|
|
4022
4168
|
const startResize=useCallback(e=>{
|
|
4023
4169
|
e.preventDefault();
|
|
4024
4170
|
const startY=e.clientY; const startH=panelHeight;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "transitions-refine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.18",
|
|
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": {
|