transitions-refine 0.3.30 → 0.3.32
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 +141 -83
- package/package.json +1 -1
- package/server/relay.mjs +42 -0
package/demo.html
CHANGED
|
@@ -1266,7 +1266,7 @@
|
|
|
1266
1266
|
background: #fff; color: var(--c-text);
|
|
1267
1267
|
font-family: "Inter", system-ui, -apple-system, sans-serif;
|
|
1268
1268
|
}
|
|
1269
|
-
/* header: title block (left) +
|
|
1269
|
+
/* header: title block (left) + scan-scope dropdown (right, Replace tab only) */
|
|
1270
1270
|
/* match the timeline top bar exactly: 7.5px/14px padding, centered, 52px tall (36px row + 15 + 1px border) */
|
|
1271
1271
|
.tl-refine-head { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between;
|
|
1272
1272
|
gap: 8px; box-sizing: border-box; padding: 7.5px 7.5px 7.5px 14px;
|
|
@@ -1635,7 +1635,7 @@
|
|
|
1635
1635
|
filter: blur(var(--text-swap-blur)); opacity: 0; }
|
|
1636
1636
|
.t-text-swap.is-enter-start { transform: translateY(var(--text-swap-translate-y));
|
|
1637
1637
|
filter: blur(var(--text-swap-blur)); opacity: 0; transition: none; }
|
|
1638
|
-
/*
|
|
1638
|
+
/* scope dropdown rows (Selected transition / All transitions) */
|
|
1639
1639
|
.tl-mode-row { display: flex; align-items: center; gap: 12px; width: 100%; padding: 8px 12px;
|
|
1640
1640
|
border: none; background: transparent; border-radius: 8px; cursor: pointer; text-align: left;
|
|
1641
1641
|
font: inherit; transition: background 0.12s ease; }
|
|
@@ -2644,8 +2644,12 @@
|
|
|
2644
2644
|
const vn=vm?rootVar(vm[1]):null;
|
|
2645
2645
|
// distance is a magnitude — direction lives in the CSS (± calc).
|
|
2646
2646
|
const num=Math.abs(vn?parseFloat(cs.getPropertyValue(vn)):parseFloat(inner));
|
|
2647
|
-
|
|
2648
|
-
|
|
2647
|
+
// a var-backed translate at rest (0) still reports its var — the
|
|
2648
|
+
// hook for ADDING a slide distance the source neutralized (e.g. a
|
|
2649
|
+
// generic text swap with --text-swap-translate-y: 0).
|
|
2650
|
+
if(Number.isFinite(num)&&(num>1e-4||vn)){
|
|
2651
|
+
const better=!bestTranslate||num>bestTranslate.translate||(bestTranslate.translateVarName==null&&vn);
|
|
2652
|
+
if(better)bestTranslate={translate:num,...(vn?{translateVarName:vn}:{})};}}}
|
|
2649
2653
|
}else{
|
|
2650
2654
|
const fm=txt.match(/filter\s*:\s*([^;}]+)/i);
|
|
2651
2655
|
if(fm){const bl=fm[1].match(/blur\(\s*([^)]+)\)/i);
|
|
@@ -3105,9 +3109,11 @@
|
|
|
3105
3109
|
return r.json();
|
|
3106
3110
|
}
|
|
3107
3111
|
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3112
|
+
// Scan SCOPE (Replace tab only): scan just the selected transition, or every
|
|
3113
|
+
// detected transition and propose a replacement for each good recipe match.
|
|
3114
|
+
const REFINE_SCOPES=[
|
|
3115
|
+
{key:"selected",label:"Selected transition",desc:"Scan only the transition you have selected."},
|
|
3116
|
+
{key:"all",label:"All transitions",desc:"Scan every detected transition and suggest a replacement for each good recipe match."},
|
|
3111
3117
|
];
|
|
3112
3118
|
const REFINE_TYPES=[
|
|
3113
3119
|
{key:"small",label:"Small refinements",desc:"The agent will scan the transition and suggest small refinements by adjusting motion tokens using the Transitions.dev skill."},
|
|
@@ -3264,7 +3270,12 @@
|
|
|
3264
3270
|
// top row: member chip + property, or a single heading for replace cards
|
|
3265
3271
|
h("div",{className:"tl-sug-head"},
|
|
3266
3272
|
isReplace
|
|
3267
|
-
? h(
|
|
3273
|
+
? h(React.Fragment,null,
|
|
3274
|
+
// "All transitions" scope tags each replace card with the
|
|
3275
|
+
// transition/component it targets so a multi-suggestion list
|
|
3276
|
+
// is legible; single-scope replaces omit it.
|
|
3277
|
+
s._targetLabel&&h("span",{className:"tl-sug-member"},s._targetLabel),
|
|
3278
|
+
h("span",{className:"tl-sug-prop"},s.title||"Transition replacement"))
|
|
3268
3279
|
: h(React.Fragment,null,
|
|
3269
3280
|
member&&h("span",{className:"tl-sug-member"},member),
|
|
3270
3281
|
prop&&h("span",{className:"tl-sug-prop"},prop))),
|
|
@@ -3329,14 +3340,15 @@
|
|
|
3329
3340
|
onReconnect&&h("button",{className:"tl-gate-recheck",disabled:rechecking,onClick:recheck},
|
|
3330
3341
|
rechecking?(relayDown?"Retrying…":"Reconnecting…"):(relayDown?"Retry connection":"Reconnect agent")));
|
|
3331
3342
|
}
|
|
3332
|
-
function RefinePanel({open,onClose,phase,label,refineType,onType,suggestions,summary,error,appliedIds,onApply,onApplyAll,
|
|
3343
|
+
function RefinePanel({open,onClose,phase,label,refineType,onType,suggestions,summary,error,appliedIds,onApply,onApplyAll,scope,onScope,llmAvailable,cliInstalled,relayDown,agentReason,onReconnect,onStart,lanes,resolvedTheme}){
|
|
3333
3344
|
// mount-on-open; keep mounted through the panel-reveal slide-out, then unmount
|
|
3334
3345
|
const[render,setRender]=useState(open);
|
|
3335
3346
|
const[panelOpen,setPanelOpen]=useState(false);
|
|
3336
3347
|
const[slidePhase,setSlidePhase]=useState("opening");
|
|
3337
|
-
|
|
3348
|
+
// scope selector dropdown (Replace tab only)
|
|
3349
|
+
const[scopeOpen,setScopeOpen]=useState(false);
|
|
3338
3350
|
const[statusIx,setStatusIx]=useState(0);
|
|
3339
|
-
const
|
|
3351
|
+
const scopeRef=useRef(null);
|
|
3340
3352
|
// The results reveal should play once per scan, not on every tab switch.
|
|
3341
3353
|
// This persists across RefineResults remounts; reset it when a new scan
|
|
3342
3354
|
// starts (or the panel returns to idle) so fresh results animate again.
|
|
@@ -3361,10 +3373,10 @@
|
|
|
3361
3373
|
// close on Escape — dismiss the mode dropdown first if it's open
|
|
3362
3374
|
useEffect(()=>{
|
|
3363
3375
|
if(!open)return;
|
|
3364
|
-
const onKey=e=>{if(e.key!=="Escape")return;if(
|
|
3376
|
+
const onKey=e=>{if(e.key!=="Escape")return;if(scopeOpen){setScopeOpen(false);return;}onClose();};
|
|
3365
3377
|
window.addEventListener("keydown",onKey);
|
|
3366
3378
|
return()=>window.removeEventListener("keydown",onKey);
|
|
3367
|
-
},[open,onClose,
|
|
3379
|
+
},[open,onClose,scopeOpen]);
|
|
3368
3380
|
// cycle the in-progress status copy while scanning (matches the design's
|
|
3369
3381
|
// "Scanning… → Reading the Transitions.dev skill → Suggesting edits").
|
|
3370
3382
|
useEffect(()=>{
|
|
@@ -3398,15 +3410,11 @@
|
|
|
3398
3410
|
// Replace transition → kind "replace"; Small refinements → token tweaks.
|
|
3399
3411
|
const visible = suggestions.filter(s=> refineType==="replace" ? s.kind==="replace" : s.kind!=="replace");
|
|
3400
3412
|
const pending = visible.filter(s=>!appliedIds[s.id]);
|
|
3401
|
-
const agentMode = mode==="llm";
|
|
3402
3413
|
// null (unknown / still probing) is treated as ready so the panel doesn't
|
|
3403
|
-
// flash the "unavailable" copy before /health resolves.
|
|
3404
|
-
|
|
3405
|
-
const
|
|
3406
|
-
|
|
3407
|
-
// Agent (llm) keeps the type's agent-oriented desc; Deterministic has no
|
|
3408
|
-
// agent, so it describes the math-snap behavior (and that "replace" needs
|
|
3409
|
-
// Agent mode).
|
|
3414
|
+
// flash the "unavailable" copy before /health resolves. All scanning goes
|
|
3415
|
+
// through the agent now, so readiness is purely "is a live agent wired".
|
|
3416
|
+
const agentReady = llmAvailable!==false;
|
|
3417
|
+
const scopeLabel = (REFINE_SCOPES.find(s=>s.key===scope)||REFINE_SCOPES[0]).label;
|
|
3410
3418
|
// One persistent control for the whole foot: in idle/done/error it's the
|
|
3411
3419
|
// pill button; while scanning it carries `.is-scanning` and the same DOM
|
|
3412
3420
|
// node morphs (card resize) into the loading rectangle with the border-beam.
|
|
@@ -3463,9 +3471,7 @@
|
|
|
3463
3471
|
h("div",{className:"tl-refine-unavail-title"},"Connect with your agent"),
|
|
3464
3472
|
h(AgentConnectBody,{cliMissing:cliInstalled===false,relayDown,reason:agentReason,onReconnect}));
|
|
3465
3473
|
}
|
|
3466
|
-
const idleDescR =
|
|
3467
|
-
? "Deterministic mode can't pick a recipe to replace the transition — switch to Agent mode for replacements."
|
|
3468
|
-
: "Deterministic mode snaps your transition's timing to the nearest Transitions.dev motion tokens — mathematically, with no agent or credits.");
|
|
3474
|
+
const idleDescR = (REFINE_TYPES.find(t=>t.key===rt)||REFINE_TYPES[0]).desc;
|
|
3469
3475
|
return h("div",{className:"tl-refine-center"},
|
|
3470
3476
|
h("p",{className:"tl-refine-idle-text"},idleDescR));
|
|
3471
3477
|
};
|
|
@@ -3530,19 +3536,22 @@
|
|
|
3530
3536
|
h("h3",null,"Refine"),
|
|
3531
3537
|
h("p",null,label||"Transitions review")),
|
|
3532
3538
|
h("div",{className:"tl-refine-actions"},
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3539
|
+
// Scan-scope selector — Replace tab only (Small refinements always
|
|
3540
|
+
// scan just the selected transition). Same control the Agent/
|
|
3541
|
+
// Deterministic mode dropdown used to be.
|
|
3542
|
+
refineType==="replace"&&h("button",{ref:scopeRef,className:cx("tl-refine-mode",scopeOpen&&"is-open"),
|
|
3543
|
+
"aria-haspopup":"menu","aria-expanded":scopeOpen?"true":"false",onClick:()=>setScopeOpen(v=>!v)},
|
|
3544
|
+
h("span",null,scopeLabel),
|
|
3536
3545
|
h(Ic,{name:"chevron",size:16})),
|
|
3537
3546
|
h("button",{className:"tl-refine-close","aria-label":"Close refine panel",onClick:onClose},
|
|
3538
3547
|
h(Ic,{name:"close",size:16})))),
|
|
3539
|
-
h(Dropdown,{open:
|
|
3540
|
-
|
|
3541
|
-
onClick:()=>{if(
|
|
3548
|
+
refineType==="replace"&&h(Dropdown,{open:scopeOpen,onClose:()=>setScopeOpen(false),triggerRef:scopeRef,width:276,align:"right"},
|
|
3549
|
+
REFINE_SCOPES.map(sc=>h("button",{key:sc.key,className:"tl-mode-row",
|
|
3550
|
+
onClick:()=>{if(sc.key!==scope)onScope(sc.key);setScopeOpen(false);}},
|
|
3542
3551
|
h("div",{className:"tl-mode-row-main"},
|
|
3543
|
-
h("div",{className:"tl-mode-row-title"},
|
|
3544
|
-
h("div",{className:"tl-mode-row-desc"},
|
|
3545
|
-
(
|
|
3552
|
+
h("div",{className:"tl-mode-row-title"},sc.label),
|
|
3553
|
+
h("div",{className:"tl-mode-row-desc"},sc.desc)),
|
|
3554
|
+
(scope===sc.key)&&h("span",{className:"tl-mode-row-check"},h(Ic,{name:"accept",size:16}))))),
|
|
3546
3555
|
h("div",{className:"tl-refine-tabs",role:"tablist","aria-label":"Refinement type"},
|
|
3547
3556
|
REFINE_TYPES.map(t=>h("button",{key:t.key,className:"tl-refine-tab",role:"tab",
|
|
3548
3557
|
"aria-selected":refineType===t.key?"true":"false",
|
|
@@ -4621,10 +4630,7 @@
|
|
|
4621
4630
|
const[refineSuggestions,setRefineSuggestions]=useState([]);
|
|
4622
4631
|
const[refineSummary,setRefineSummary]=useState(null);
|
|
4623
4632
|
const[refineError,setRefineError]=useState(null);
|
|
4624
|
-
// deterministic scans answer almost instantly, so enforce a 2s floor on the
|
|
4625
|
-
// "scanning" phase for that mode only (LLM scans stay immediate).
|
|
4626
4633
|
const scanStartedAtRef=useRef(0);
|
|
4627
|
-
const scanModeRef=useRef("llm");
|
|
4628
4634
|
// ── accept (write to source) ──
|
|
4629
4635
|
const[acceptState,setAcceptState]=useState("idle"); // idle | saving | done | error
|
|
4630
4636
|
const[acceptError,setAcceptError]=useState(null);
|
|
@@ -4655,7 +4661,12 @@
|
|
|
4655
4661
|
const GROUP_SCAN_MAX_RETRIES=2;
|
|
4656
4662
|
const[refineLabel,setRefineLabel]=useState(null);
|
|
4657
4663
|
const[appliedIds,setAppliedIds]=useState({});
|
|
4658
|
-
|
|
4664
|
+
// scan SCOPE for the Replace tab — "selected" (only the active transition)
|
|
4665
|
+
// or "all" (every detected transition). Persisted like the tab/theme prefs;
|
|
4666
|
+
// defaults to "selected". Small refinements always scan just the selection.
|
|
4667
|
+
const[refineScope,setRefineScope]=useState(()=>{
|
|
4668
|
+
try{const v=localStorage.getItem("tl-refine-scope-pref");return v==="all"?"all":"selected";}catch{return "selected";}
|
|
4669
|
+
});
|
|
4659
4670
|
// small | replace — last selected tab persists (same pattern as tl-theme-pref)
|
|
4660
4671
|
const[refineType,setRefineType]=useState(()=>{
|
|
4661
4672
|
try{const v=localStorage.getItem("tl-refine-tab-pref");return v==="small"||v==="replace"?v:"small";}catch{return "small";}
|
|
@@ -4750,10 +4761,9 @@
|
|
|
4750
4761
|
},[active,refineOpen,refreshHealth]);
|
|
4751
4762
|
// "Start scanning" resolves availability first, then posts the job.
|
|
4752
4763
|
// per-(transition,type) result cache so switching tabs reuses a prior scan
|
|
4753
|
-
// instead of re-asking the agent.
|
|
4754
|
-
//
|
|
4755
|
-
//
|
|
4756
|
-
// a single instant "both" pass that feeds both tabs.
|
|
4764
|
+
// instead of re-asking the agent. Each scan is scoped to the active tab
|
|
4765
|
+
// (Small = token tweaks, Replace = recipe) so the common Small view skips
|
|
4766
|
+
// the recipe selection + reference-file read.
|
|
4757
4767
|
const refineCacheRef=useRef(new Map()); // `${id}::${type}` -> {suggestions,summary}
|
|
4758
4768
|
const refineScanTypeRef=useRef("small"); // which type the in-flight job targets
|
|
4759
4769
|
// results are not comparable across transitions — drop the cache when the
|
|
@@ -4772,64 +4782,96 @@
|
|
|
4772
4782
|
const startScan=useCallback(async(typeArg,opts)=>{
|
|
4773
4783
|
if(!active)return;
|
|
4774
4784
|
opts=opts||{};
|
|
4775
|
-
//
|
|
4776
|
-
//
|
|
4777
|
-
// available so the first click isn't blocked
|
|
4778
|
-
// the POST below (→ error state).
|
|
4779
|
-
const
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
scanModeRef.current=mode;
|
|
4783
|
-
// deterministic → one "both" pass (instant, free). llm → scope to the
|
|
4784
|
-
// active tab so Small skips recipe work; Replace asks for it on demand.
|
|
4785
|
-
// NOTE: this is also wired as a button onClick, so typeArg may be a click
|
|
4786
|
-
// event — only honour it when it's an explicit "small"/"replace" string.
|
|
4785
|
+
// All scanning goes through the agent now (the deterministic path is gone).
|
|
4786
|
+
// Availability is resolved from the continuously-polled /health; unknown
|
|
4787
|
+
// (null) counts as available so the first click isn't blocked, and a dead
|
|
4788
|
+
// relay surfaces via the POST below (→ error state).
|
|
4789
|
+
const mode="llm";
|
|
4790
|
+
// NOTE: also wired as a button onClick, so typeArg may be a click event —
|
|
4791
|
+
// only honour it when it's an explicit "small"/"replace" string.
|
|
4787
4792
|
const reqType=(typeArg==="small"||typeArg==="replace")?typeArg:refineType;
|
|
4788
|
-
const type=
|
|
4793
|
+
const type=reqType;
|
|
4794
|
+
// Scope only applies to the Replace tab — Small always scans the selection.
|
|
4795
|
+
const scope=(reqType==="replace")?refineScope:"selected";
|
|
4789
4796
|
refineScanTypeRef.current=type;
|
|
4790
4797
|
scanStartedAtRef.current=Date.now();
|
|
4791
4798
|
setRefinePhase("scanning");setRefineLog([]);setRefineError(null);
|
|
4792
|
-
setRefineLabel(active.label);setRefineJobId(null);
|
|
4799
|
+
setRefineLabel(scope==="all"?"All transitions":active.label);setRefineJobId(null);
|
|
4793
4800
|
if(!opts.keepApplied)setAppliedIds({});
|
|
4794
4801
|
// re-scan only this type; keep the other tab's cached result for the merge.
|
|
4795
4802
|
refineCacheRef.current.delete(active.id+"::"+type);
|
|
4803
|
+
// attach the lane's non-resting scale/blur/translate value (+ backing var)
|
|
4804
|
+
// so the agent can refine transform/filter VALUES too.
|
|
4805
|
+
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,
|
|
4806
|
+
...(v.scale!=null?{scale:v.scale}:{}),...(v.blur!=null?{blur:v.blur}:{}),...(v.varName?{varName:v.varName}:{}),
|
|
4807
|
+
...(v.translate!=null?{translate:v.translate}:{}),...(v.translateVarName?{translateVarName:v.translateVarName}:{})};};
|
|
4796
4808
|
try{
|
|
4809
|
+
if(scope==="all"){
|
|
4810
|
+
// Bundle EVERY detected transition — one per agent group (with its
|
|
4811
|
+
// related open/close phases) plus each ungrouped flat entry — so the
|
|
4812
|
+
// agent can return a LIST of replace suggestions, one per transition
|
|
4813
|
+
// it finds a good recipe for. Each suggestion echoes its transitionId
|
|
4814
|
+
// so Apply targets the right transition.
|
|
4815
|
+
const seenGroup=new Set();
|
|
4816
|
+
const transitions=[];
|
|
4817
|
+
for(const e of entries){
|
|
4818
|
+
if(e.kind==="phase"){
|
|
4819
|
+
if(!e.groupId||seenGroup.has(e.groupId))continue;
|
|
4820
|
+
seenGroup.add(e.groupId);
|
|
4821
|
+
const gp=entries.filter(x=>x.kind==="phase"&&x.groupId===e.groupId);
|
|
4822
|
+
const rep=gp[0]||e;
|
|
4823
|
+
const ret=rep.effectiveTimings||[];
|
|
4824
|
+
const sel=(ret[0]&&ret[0].selector)||null;
|
|
4825
|
+
const ph=gp.length>1?gp.map(x=>({phase:x.phase||x.phaseLabel||"phase",label:x.phaseLabel||x.phase||"Phase",
|
|
4826
|
+
timings:(x.effectiveTimings||[]).map(withVal)})):undefined;
|
|
4827
|
+
transitions.push({transitionId:rep.id,label:rep.groupLabel||rep.label,selector:sel,timings:ret.map(withVal),phases:ph});
|
|
4828
|
+
}else{
|
|
4829
|
+
const fet=e.effectiveTimings||[];
|
|
4830
|
+
const sel=(e.bindings&&e.bindings.selector)||(fet[0]&&fet[0].selector)||null;
|
|
4831
|
+
transitions.push({transitionId:e.id,label:e.label,selector:sel,timings:fet.map(withVal)});
|
|
4832
|
+
}
|
|
4833
|
+
}
|
|
4834
|
+
const{id}=await relayCreateJob({scope:"all",label:"All transitions",mode,refineType:"replace",transitions});
|
|
4835
|
+
setRefineJobId(id);
|
|
4836
|
+
return;
|
|
4837
|
+
}
|
|
4797
4838
|
const et=active.effectiveTimings||[];
|
|
4798
|
-
// attach the lane's non-resting scale/blur value (+ backing var) so the
|
|
4799
|
-
// agent + deterministic engine can refine transform/filter VALUES too.
|
|
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,
|
|
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}:{})};};
|
|
4803
4839
|
const timings=et.map(withVal);
|
|
4804
4840
|
const selector=(active.bindings&&active.bindings.selector)||(et[0]&&et[0].selector)||null;
|
|
4805
|
-
// A recipe swap (replace
|
|
4841
|
+
// A recipe swap (replace) on a grouped phase should update its related
|
|
4806
4842
|
// phases (open + close) together — they're one motion. Send the whole
|
|
4807
4843
|
// group's phases so the agent can return a per-phase `patches` array.
|
|
4808
4844
|
let phases;
|
|
4809
|
-
if(
|
|
4845
|
+
if(type==="replace"&&active.kind==="phase"&&active.groupId){
|
|
4810
4846
|
const gp=entries.filter(e=>e.kind==="phase"&&e.groupId===active.groupId);
|
|
4811
4847
|
if(gp.length>1)phases=gp.map(e=>({phase:e.phase||e.phaseLabel||"phase",label:e.phaseLabel||e.phase||"Phase",
|
|
4812
4848
|
timings:(e.effectiveTimings||[]).map(withVal)}));
|
|
4813
4849
|
}
|
|
4814
4850
|
const{id}=await relayCreateJob({transitionId:active.id,label:active.label,selector,
|
|
4815
|
-
phase:active.phase||null,group:active.groupLabel||null,timings,mode,refineType:type,phases});
|
|
4851
|
+
phase:active.phase||null,group:active.groupLabel||null,timings,mode,refineType:type,scope:"selected",phases});
|
|
4816
4852
|
setRefineJobId(id);
|
|
4817
4853
|
}catch(e){
|
|
4818
4854
|
setRefinePhase("error");
|
|
4819
4855
|
setRefineError(h(React.Fragment,null,"Couldn't reach the refine relay. Start it with ",h("code",{className:"tl-code"},"npx transitions-refine live"),"."));
|
|
4820
4856
|
}
|
|
4821
|
-
},[active,
|
|
4822
|
-
|
|
4823
|
-
//
|
|
4824
|
-
//
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4857
|
+
},[active,refineScope,refineType,llmAvailable,entries]);
|
|
4858
|
+
// Switching scope changes the Replace scan's shape (one transition vs all),
|
|
4859
|
+
// so drop cached results and fall back to idle — the next scan uses the new
|
|
4860
|
+
// scope. Persist the choice like the tab/theme prefs.
|
|
4861
|
+
const changeRefineScope=useCallback((sc)=>{
|
|
4862
|
+
setRefineScope(sc);
|
|
4863
|
+
try{localStorage.setItem("tl-refine-scope-pref",sc);}catch{}
|
|
4864
|
+
refineCacheRef.current.clear();
|
|
4865
|
+
setRefinePhase(p=>(p==="done"||p==="error")?"idle":p);
|
|
4866
|
+
},[]);
|
|
4867
|
+
// Switching tabs reuses a cached scan when present; otherwise the tab drops
|
|
4868
|
+
// back to its idle state and waits for an explicit Start-scanning click —
|
|
4869
|
+
// switching never auto-runs a scan. Before any scan (idle/error), tabs just
|
|
4870
|
+
// preview copy and never auto-scan.
|
|
4828
4871
|
const changeRefineType=useCallback((t)=>{
|
|
4829
4872
|
setRefineType(t);
|
|
4830
4873
|
try{localStorage.setItem("tl-refine-tab-pref",t);}catch{}
|
|
4831
4874
|
if(!active)return;
|
|
4832
|
-
if(scanModeRef.current==="deterministic")return;
|
|
4833
4875
|
if(refinePhase!=="done"&&refinePhase!=="scanning")return;
|
|
4834
4876
|
if(refineCacheRef.current.has(active.id+"::"+t)){applyMerged(active.id);setRefinePhase("done");return;}
|
|
4835
4877
|
// No cached result for this tab → show its idle scan button and wait for
|
|
@@ -4852,13 +4894,23 @@
|
|
|
4852
4894
|
// (so the other tab's cached suggestions stay visible).
|
|
4853
4895
|
const type=refineScanTypeRef.current;
|
|
4854
4896
|
const id=active?active.id:"";
|
|
4855
|
-
|
|
4856
|
-
|
|
4897
|
+
// "All transitions" scope returns MANY replace suggestions, each
|
|
4898
|
+
// carrying the `transitionId` it targets. Tag each with the target
|
|
4899
|
+
// transition's label (for the card) and namespace its id by that
|
|
4900
|
+
// transitionId so multiple replaces stay uniquely keyed for Apply.
|
|
4901
|
+
const rawSugs=(job.result&&job.result.suggestions)||[];
|
|
4902
|
+
const sugs=rawSugs.map((s,i)=>{
|
|
4903
|
+
if(s&&s.transitionId){
|
|
4904
|
+
const ent=entries.find(e=>e.id===s.transitionId);
|
|
4905
|
+
return {...s,id:s.transitionId+"::"+(s.id||s.kind||("sug"+i)),
|
|
4906
|
+
_targetLabel:ent?(ent.groupLabel||ent.label):null};
|
|
4907
|
+
}
|
|
4908
|
+
return s;
|
|
4909
|
+
});
|
|
4910
|
+
refineCacheRef.current.set(id+"::"+type,{suggestions:sugs,summary:(job.result&&job.result.summary)||null});
|
|
4911
|
+
if(active)applyMerged(active.id);else{setRefineSuggestions(sugs);setRefineSummary(job.result&&job.result.summary);}
|
|
4857
4912
|
setRefinePhase("done");};
|
|
4858
|
-
|
|
4859
|
-
const minMs=scanModeRef.current==="deterministic"?2000:0;
|
|
4860
|
-
const wait=Math.max(0,minMs-(Date.now()-scanStartedAtRef.current));
|
|
4861
|
-
if(wait>0){to=setTimeout(finish,wait);}else{finish();}
|
|
4913
|
+
finish();
|
|
4862
4914
|
return;
|
|
4863
4915
|
}
|
|
4864
4916
|
if(job.status==="error"){setRefineError(job.error||"The agent reported an error.");setRefinePhase("error");return;}
|
|
@@ -4869,14 +4921,20 @@
|
|
|
4869
4921
|
return()=>{live=false;if(to)clearTimeout(to);};
|
|
4870
4922
|
},[refineJobId,refinePhase]);
|
|
4871
4923
|
const applySuggestion=useCallback((s)=>{
|
|
4924
|
+
// Resolve which transition this suggestion targets. In "All transitions"
|
|
4925
|
+
// scope every suggestion carries its own `transitionId`; otherwise it
|
|
4926
|
+
// applies to the currently-active transition. Everything below writes to
|
|
4927
|
+
// `tgt` (and its sibling phases), so multi-transition Apply just works.
|
|
4928
|
+
const tgt=(s&&s.transitionId?entries.find(e=>e.id===s.transitionId):null)||active;
|
|
4929
|
+
if(!tgt)return;
|
|
4872
4930
|
// A replace suggestion may carry `patches` (one per related phase, e.g.
|
|
4873
4931
|
// open + close) — a recipe swap is one motion, so applying it writes BOTH.
|
|
4874
|
-
// Otherwise fall back to the single `patch` on the
|
|
4932
|
+
// Otherwise fall back to the single `patch` on the target phase.
|
|
4875
4933
|
let patches=Array.isArray(s.patches)&&s.patches.length
|
|
4876
4934
|
? s.patches
|
|
4877
|
-
: (s.patch&&s.patch.property?[{...s.patch,phase:(
|
|
4935
|
+
: (s.patch&&s.patch.property?[{...s.patch,phase:(tgt&&tgt.phase)||null}]:[]);
|
|
4878
4936
|
if(!patches.length)return;
|
|
4879
|
-
const groupId=
|
|
4937
|
+
const groupId=tgt&&tgt.groupId;
|
|
4880
4938
|
// Same-slot swap recipes (icon/text swaps) use two stacked members in one
|
|
4881
4939
|
// group. The agent can sometimes return the recipe's blur/scale value for
|
|
4882
4940
|
// only the currently selected member; expand those value patches to every
|
|
@@ -4887,7 +4945,7 @@
|
|
|
4887
4945
|
const extra=[]; const has=new Set(patches.map(p=>[p.phase||"",p.property||"",p.member||"",p.blur!=null?"blur":"",p.scale!=null?"scale":"",p.translate!=null?"translate":""].join("|")));
|
|
4888
4946
|
const valuePatches=patches.filter(p=>p.member&&(p.blur!=null||p.scale!=null)&&(p.property==="filter"||p.property==="transform"));
|
|
4889
4947
|
for(const p of valuePatches){
|
|
4890
|
-
const target=entries.find(e=>e.kind==="phase"&&e.groupId===groupId&&(!p.phase||e.phase===p.phase))||
|
|
4948
|
+
const target=entries.find(e=>e.kind==="phase"&&e.groupId===groupId&&(!p.phase||e.phase===p.phase))||tgt;
|
|
4891
4949
|
const members=new Map();
|
|
4892
4950
|
for(const t of (target&&target.effectiveTimings)||[]){
|
|
4893
4951
|
const m=t.member||t.memberId;if(m&&!members.has(m))members.set(m,m);
|
|
@@ -4929,7 +4987,7 @@
|
|
|
4929
4987
|
if(p.easing!=null)timingOverride.easing=p.easing;
|
|
4930
4988
|
// resolve the phase entry this patch targets (its sibling open/close
|
|
4931
4989
|
// lives in the same group); default to the active entry.
|
|
4932
|
-
let target=
|
|
4990
|
+
let target=tgt;
|
|
4933
4991
|
if(p.phase&&groupId){
|
|
4934
4992
|
const found=entries.find(e=>e.kind==="phase"&&e.groupId===groupId&&e.phase===p.phase);
|
|
4935
4993
|
if(found)target=found;
|
|
@@ -5615,7 +5673,7 @@
|
|
|
5615
5673
|
h(RefinePanel,{open:refineOpen,onClose:()=>setRefineOpen(false),phase:refinePhase,label:refineLabel,
|
|
5616
5674
|
refineType,onType:changeRefineType,suggestions:refineSuggestions,summary:refineSummary,error:refineError,
|
|
5617
5675
|
appliedIds,onApply:applySuggestion,onApplyAll:applyAllSuggestions,
|
|
5618
|
-
|
|
5676
|
+
scope:refineScope,onScope:changeRefineScope,llmAvailable,cliInstalled,relayDown,agentReason,onReconnect:reconnectAgent,onStart:startScan,
|
|
5619
5677
|
lanes:active?.effectiveTimings||[],resolvedTheme}))
|
|
5620
5678
|
: h("div",{className:"tl-panel-main"},
|
|
5621
5679
|
gate==="blocked" && h("div",{className:"tl-gate"},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "transitions-refine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.32",
|
|
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
|
@@ -269,6 +269,7 @@ const RECIPES_BLOCK = [
|
|
|
269
269
|
" - Panel reveal → filter blur 2px (Small)",
|
|
270
270
|
" - Page side-by-side → filter blur 3px (Medium) AND transform translate distance 8px, both on the sliding page",
|
|
271
271
|
" - Icon swap → filter blur 2px (Small) AND transform scale 0.25 (start-scale) on the swapping icon",
|
|
272
|
+
" - Text states swap → filter blur 2px (Small) AND transform translateY distance 4px, on the swapping text (exit and enter both settle to blur 0 / translate 0)",
|
|
272
273
|
" - Skeleton loader and reveal → filter blur 2px (Small)",
|
|
273
274
|
" - Texts reveal → filter blur 3px (Medium)",
|
|
274
275
|
" - Success check → filter blur 8px (Large)",
|
|
@@ -292,6 +293,47 @@ const MISSING_BLUR_NOTE =
|
|
|
292
293
|
|
|
293
294
|
function buildPrompt(job) {
|
|
294
295
|
const r = job.request || {};
|
|
296
|
+
// "All transitions" replace scope: the panel sends EVERY detected transition
|
|
297
|
+
// and wants a LIST of whole-transition replacements — one per transition the
|
|
298
|
+
// agent finds a good transitions.dev recipe for. Each suggestion echoes its
|
|
299
|
+
// `transitionId` so the panel applies it to the right transition, and carries
|
|
300
|
+
// the SAME patch/patches contract as a single replace so Apply is unchanged.
|
|
301
|
+
const transitions = Array.isArray(r.transitions) ? r.transitions.filter((t) => t && t.transitionId) : [];
|
|
302
|
+
if (r.scope === "all" && transitions.length) {
|
|
303
|
+
return [
|
|
304
|
+
"You are refining MULTIPLE CSS transitions against the transitions.dev library and motion tokens. Evaluate EVERY transition below; for each one where a transitions.dev recipe is a genuinely good fit, emit ONE whole-transition replacement. Transitions with no good recipe match are simply OMITTED — never force a swap.",
|
|
305
|
+
MOTION_TOKENS_BLOCK,
|
|
306
|
+
"",
|
|
307
|
+
"Transitions (JSON array). Each has a stable `transitionId` you MUST echo VERBATIM in that transition's suggestion, its current `timings`, and — when it has related states — a `phases` array (e.g. open + close) that is ONE motion.",
|
|
308
|
+
JSON.stringify(
|
|
309
|
+
transitions.map((t) => ({
|
|
310
|
+
transitionId: t.transitionId,
|
|
311
|
+
label: t.label,
|
|
312
|
+
selector: t.selector,
|
|
313
|
+
timings: t.timings,
|
|
314
|
+
phases: Array.isArray(t.phases) && t.phases.length ? t.phases : undefined,
|
|
315
|
+
})),
|
|
316
|
+
null,
|
|
317
|
+
2
|
|
318
|
+
),
|
|
319
|
+
"",
|
|
320
|
+
"For EACH transition infer its USAGE (modal close, dropdown open, tooltip, badge, resize, page slide, icon/text swap…) from its label/selector. Match on usage intent, not the nearest number. A lane may carry `scale`/`translate` (transform) or `blur` (filter) plus a `varName`/`translateVarName` — check those against the Scales/Blur/recipe values too and pass any var names straight through in the patch.",
|
|
321
|
+
"",
|
|
322
|
+
"To pick a whole-transition replacement, match the inferred USAGE against the recipe list below (the skill's decision rules are summarised here — do NOT read SKILL.md or any reference file). Put the recipe's reference filename in `reference`.",
|
|
323
|
+
"",
|
|
324
|
+
RECIPES_BLOCK,
|
|
325
|
+
"",
|
|
326
|
+
"PER-TRANSITION OUTPUT — for each transition you replace, emit ONE suggestion with kind \"replace\" and `transitionId` set to that transition's id:",
|
|
327
|
+
" - If the transition has a `phases` array, emit a `patches` array with ONE phase-level timing entry per phase (`{\"phase\":<phase>,\"property\":\"all\",\"durationMs\":...,\"easing\":...}`) PLUS extra per-lane value patches for any recipe non-resting scale/blur/translate (`property:\"transform\"`/`\"filter\"` with the lane `member` and `varName`/`translateVarName` copied from that phase's input timings; when the recipe adds a lane the input lacks, still emit it and reuse a sibling lane's `member`, omitting the var name). Also include a single `patch` (the first phase) for the live preview. Set each phase's durationMs AND easing from \"Recipe timing & easing\" (SYMMETRIC recipes use the SAME duration+easing for every phase; ASYMMETRIC use open 250ms / close 150ms).",
|
|
328
|
+
" - Otherwise (single phase): set its `patch` to the recipe's documented duration AND easing on the property that already transitions (or \"all\"); if the recipe prescribes a non-resting pre-blur/pre-scale/slide-distance, put those `blur`/`scale`/`translate` values on the `patch` too so the element gains them even when the captured transition had no such lane.",
|
|
329
|
+
"Copy each suggestion's and patch's `member` VERBATIM from the input lane it came from — REQUIRED whenever an input lane carries a member (it is the only way to tell which lane a transform/filter tweak targets).",
|
|
330
|
+
"Return ALL such suggestions in ONE array (order doesn't matter). If NO transition has a good recipe match, return an empty suggestions array.",
|
|
331
|
+
"Answer in ONE response — do NOT read or search files.",
|
|
332
|
+
"",
|
|
333
|
+
"Output ONLY a JSON object — no prose, no markdown fences — shaped exactly like:",
|
|
334
|
+
'{"summary":"…","suggestions":[{"id":"dropdown-replace","transitionId":"<echo an id from the input>","kind":"replace","title":"Menu dropdown","reference":"05-menu-dropdown.md","patch":{"property":"all","member":"Panel","durationMs":250,"easing":"cubic-bezier(0.22, 1, 0.36, 1)","scale":0.97},"patches":[{"phase":"open","property":"all","durationMs":250,"easing":"cubic-bezier(0.22, 1, 0.36, 1)"},{"phase":"open","property":"transform","member":"Panel","scale":0.97},{"phase":"close","property":"all","durationMs":150,"easing":"cubic-bezier(0.22, 1, 0.36, 1)"}],"reason":"…"}]}',
|
|
335
|
+
].join("\n");
|
|
336
|
+
}
|
|
295
337
|
const rawType = r.refineType || "small";
|
|
296
338
|
const refineType = rawType === "replace" ? "replace" : rawType === "both" ? "both" : "small";
|
|
297
339
|
const needsRecipe = refineType === "replace" || refineType === "both";
|