transitions-refine 0.3.24 → 0.3.25

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 CHANGED
@@ -2891,10 +2891,16 @@
2891
2891
  // Token-faithful tweaks for one lane list (mirrors refineTimings()).
2892
2892
  function _txTokenTweaks(timings, ctx){
2893
2893
  const out=[]; const c=ctx||{};
2894
+ const blurByMember=new Map(); // memberKey -> {member,hint,sawBlur,varName}
2894
2895
  for(const t of (timings||[])){
2895
2896
  const prop=t.property||"all";
2896
2897
  const member=t.member||null;
2897
2898
  const hint=[c.label,c.selector,c.phase,t.property,t.member].filter(Boolean).join(" ");
2899
+ const memberKey=member||"__all__";
2900
+ if(!blurByMember.has(memberKey))blurByMember.set(memberKey,{member,hint,sawBlur:false,varName:null});
2901
+ const bm=blurByMember.get(memberKey);
2902
+ bm.hint=[bm.hint,hint].filter(Boolean).join(" ");
2903
+ if(t.varName&&(!bm.varName||prop==="filter"))bm.varName=t.varName;
2898
2904
  const mp=(rest)=> member ? {member, ...rest} : rest;
2899
2905
  if(Number.isFinite(t.durationMs)){const {token,delta}=_txNearest(_TX_DUR_TOKENS,t.durationMs);
2900
2906
  if(delta>10)out.push({id:"mock-"+prop+"-duration",kind:"duration",property:prop,member,
@@ -2911,11 +2917,23 @@
2911
2917
  patch:mp({property:prop,scale:token.v,...(t.varName?{varName:t.varName}:{})}),
2912
2918
  reason:token.name+" ("+token.v+") is the transitions.dev scale token for "+token.usage+". "+t.scale+" is "+(usage?"off the usage token":"off-grid")+"."});}
2913
2919
  if(Number.isFinite(t.blur)&&t.blur>1e-4){const usage=_txBlurUsage(hint);const {token:near,delta}=_txNearest(_TX_BLUR_TOKENS,t.blur);const token=usage||near;
2920
+ bm.sawBlur=true;
2914
2921
  if(token&&(usage||delta>0.5)&&Math.abs(token.v-t.blur)>1e-4)out.push({id:"mock-"+prop+"-blur",kind:"blur",property:prop,member,
2915
2922
  title:"Blur → "+token.name,from:t.blur+"px",to:token.v+"px",
2916
2923
  patch:mp({property:prop,blur:token.v,...(t.varName?{varName:t.varName}:{})}),
2917
2924
  reason:token.name+" ("+token.v+"px) is the transitions.dev blur token for "+token.usage+". "+t.blur+"px is "+(usage?"off the usage token":"off-grid")+"."});}
2918
2925
  }
2926
+ // Missing-blur detection (demo parity with server token engine).
2927
+ for(const [memberKey,info] of blurByMember){
2928
+ if(info.sawBlur)continue;
2929
+ const token=_txBlurUsage(info.hint);
2930
+ if(!token)continue;
2931
+ const member=info.member||null;
2932
+ out.push({id:"mock-"+memberKey+"-blur-add",kind:"blur",property:"filter",member,
2933
+ title:"Blur → "+token.name,from:"0px",to:token.v+"px",
2934
+ patch:{property:"filter",blur:token.v,...(member?{member}:{}),...(info.varName?{varName:info.varName}:{})},
2935
+ reason:token.name+" ("+token.v+"px) is the transitions.dev blur token for "+token.usage+". This transition currently has no blur."});
2936
+ }
2919
2937
  return out;
2920
2938
  }
2921
2939
  // refine → token-faithful suggestions (same verdict the shipped agent reaches)
@@ -2938,15 +2956,20 @@
2938
2956
  // property and needs the var name, which only the scanned timings know.
2939
2957
  const reqPhases = (request && Array.isArray(request.phases)) ? request.phases.filter(p=>p&&p.phase) : [];
2940
2958
  const recipeScale = ph => /close/i.test(ph)?0.99:0.97;
2959
+ const recipeBlur = ph => /close/i.test(ph)?2:2;
2941
2960
  const recipeDur = ph => /close/i.test(ph)?150:250;
2942
2961
  // one scale patch per transform/filter lane that has a backing var, so the
2943
2962
  // applied override carries {scale, varName} and the live var gets written.
2944
2963
  const scalePatchesFor = (phase, timings) => (timings||[])
2945
2964
  .filter(tt=>tt.varName && tt.scale!=null)
2946
2965
  .map(tt=>({phase, property:tt.property, member:tt.member||null, varName:tt.varName, scale:recipeScale(phase)}));
2966
+ const blurPatchesFor = (phase, timings) => (timings||[])
2967
+ .filter(tt=>tt.varName && tt.blur!=null)
2968
+ .map(tt=>({phase, property:tt.property, member:tt.member||null, varName:tt.varName, blur:recipeBlur(phase)}));
2947
2969
  const phasePatches = (phase, timings) => [
2948
2970
  {phase, property:"all", durationMs:recipeDur(phase), easing:_TX_EASE},
2949
- ...scalePatchesFor(phase, timings)];
2971
+ ...scalePatchesFor(phase, timings),
2972
+ ...blurPatchesFor(phase, timings)];
2950
2973
  let replacePatches = reqPhases.length>1
2951
2974
  ? reqPhases.flatMap(p=>phasePatches(p.phase, p.timings))
2952
2975
  : phasePatches((request&&request.phase)||null, t);
@@ -4797,15 +4820,10 @@
4797
4820
  const groupId=active&&active.groupId;
4798
4821
  for(const p of patches){
4799
4822
  if(!p.property)continue;
4800
- const o={};
4801
- if(p.durationMs!=null)o.durationMs=p.durationMs;
4802
- if(p.delayMs!=null)o.delayMs=p.delayMs;
4803
- if(p.easing!=null)o.easing=p.easing;
4804
- // value edits: keep the new value, the backing var, and the from-value
4805
- // (parsed off the suggestion) so computeChanges can write source on Accept.
4806
- if(p.scale!=null){o.scale=p.scale;o.scaleFrom=(s.from!=null?parseFloat(s.from):undefined);}
4807
- if(p.blur!=null){o.blur=p.blur;o.blurFrom=(s.from!=null?parseFloat(s.from):undefined);}
4808
- if(p.varName!=null)o.varName=p.varName;
4823
+ const timingOverride={};
4824
+ if(p.durationMs!=null)timingOverride.durationMs=p.durationMs;
4825
+ if(p.delayMs!=null)timingOverride.delayMs=p.delayMs;
4826
+ if(p.easing!=null)timingOverride.easing=p.easing;
4809
4827
  // resolve the phase entry this patch targets (its sibling open/close
4810
4828
  // lives in the same group); default to the active entry.
4811
4829
  let target=active;
@@ -4820,7 +4838,35 @@
4820
4838
  let lanes=p.property==="all"?et:et.filter(t=>t.property===p.property);
4821
4839
  if(p.member)lanes=lanes.filter(t=>(t.member===p.member||t.memberId===p.member));
4822
4840
  const ids=lanes.length?lanes.map(t=>t.laneId):[p.property];
4823
- for(const id of ids)registry.setPropOverride(target.id,id,o);
4841
+ if(Object.keys(timingOverride).length){
4842
+ for(const id of ids)registry.setPropOverride(target.id,id,timingOverride);
4843
+ }
4844
+
4845
+ // Value edits (scale/blur) must land on concrete lanes so live preview
4846
+ // can write the right backing var. For coarse recipe patches that use
4847
+ // property:"all" and omit varName, fan out by lane type.
4848
+ if(p.scale!=null){
4849
+ let scaleLanes=lanes.filter(t=>t.property==="transform"||t.scale!=null);
4850
+ if(!scaleLanes.length&&p.property==="all")scaleLanes=et.filter(t=>t.property==="transform"||t.scale!=null);
4851
+ const scaleFrom=(s.from!=null?parseFloat(s.from):undefined);
4852
+ for(const lane of scaleLanes){
4853
+ const ov={scale:p.scale,scaleFrom};
4854
+ const vn=p.varName??lane.varName;
4855
+ if(vn!=null)ov.varName=vn;
4856
+ registry.setPropOverride(target.id,lane.laneId,ov);
4857
+ }
4858
+ }
4859
+ if(p.blur!=null){
4860
+ let blurLanes=lanes.filter(t=>t.property==="filter"||t.blur!=null);
4861
+ if(!blurLanes.length&&p.property==="all")blurLanes=et.filter(t=>t.property==="filter"||t.blur!=null);
4862
+ const blurFrom=(s.from!=null?parseFloat(s.from):undefined);
4863
+ for(const lane of blurLanes){
4864
+ const ov={blur:p.blur,blurFrom};
4865
+ const vn=p.varName??lane.varName;
4866
+ if(vn!=null)ov.varName=vn;
4867
+ registry.setPropOverride(target.id,lane.laneId,ov);
4868
+ }
4869
+ }
4824
4870
  }
4825
4871
  setAppliedIds(prev=>({...prev,[s.id]:true}));
4826
4872
  },[registry,active,entries]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "transitions-refine",
3
- "version": "0.3.24",
3
+ "version": "0.3.25",
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": {
@@ -133,6 +133,7 @@ export function refineTimings(timings, ctx) {
133
133
  const suggestions = [];
134
134
  if (!Array.isArray(timings)) return suggestions;
135
135
  const c = ctx || {};
136
+ const blurByMember = new Map(); // memberKey -> {member,hint,sawBlur,varName}
136
137
 
137
138
  for (const t of timings) {
138
139
  const prop = t.property || "all";
@@ -140,6 +141,13 @@ export function refineTimings(timings, ctx) {
140
141
  // with this lane's own property/member so component words (dropdown, modal,
141
142
  // page slide…) are visible to the usage matcher.
142
143
  const hint = [c.label, c.selector, c.phase, t.hint, t.property, t.member].filter(Boolean).join(" ");
144
+ const memberKey = t.member || "__all__";
145
+ if (!blurByMember.has(memberKey)) {
146
+ blurByMember.set(memberKey, { member: t.member || null, hint, sawBlur: false, varName: null });
147
+ }
148
+ const bm = blurByMember.get(memberKey);
149
+ bm.hint = [bm.hint, hint].filter(Boolean).join(" ");
150
+ if (t.varName && (!bm.varName || prop === "filter")) bm.varName = t.varName;
143
151
 
144
152
  // Duration → nearest token (skip if already on-grid or within 10ms).
145
153
  if (Number.isFinite(t.durationMs)) {
@@ -199,6 +207,7 @@ export function refineTimings(timings, ctx) {
199
207
  // Blur → a transitions.dev blur token (usage-first, nearest fallback). The
200
208
  // captured value is the non-resting "pre" blur (settles to 0); skip 0/none.
201
209
  if (Number.isFinite(t.blur) && t.blur > 1e-4) {
210
+ bm.sawBlur = true;
202
211
  const usage = pickBlurByUsage(hint);
203
212
  const { token: near, delta } = nearestBlur(t.blur);
204
213
  const token = usage || near;
@@ -218,5 +227,30 @@ export function refineTimings(timings, ctx) {
218
227
  }
219
228
  }
220
229
 
230
+ // Missing-blur detection: when usage clearly maps to a blur token but no blur
231
+ // lane is present, suggest adding one so the motion isn't opacity-only.
232
+ for (const [memberKey, info] of blurByMember) {
233
+ if (info.sawBlur) continue;
234
+ const token = pickBlurByUsage(info.hint);
235
+ if (!token) continue;
236
+ const member = info.member;
237
+ suggestions.push({
238
+ id: `${memberKey}-blur-add`,
239
+ kind: "blur",
240
+ property: "filter",
241
+ member,
242
+ title: `Blur → ${token.name}`,
243
+ from: "0px",
244
+ to: `${token.px}px`,
245
+ patch: {
246
+ property: "filter",
247
+ blur: token.px,
248
+ ...(info.varName ? { varName: info.varName } : {}),
249
+ ...(member ? { member } : {}),
250
+ },
251
+ reason: `${token.name} (${token.px}px) is the transitions.dev blur token for ${token.usage}. This transition currently has no blur.`,
252
+ });
253
+ }
254
+
221
255
  return suggestions;
222
256
  }
package/server/relay.mjs CHANGED
@@ -300,7 +300,7 @@ function buildPrompt(job) {
300
300
  );
301
301
  if (multiPhase) {
302
302
  lines.push(
303
- "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 entry per phase in `phases`, each shaped `{\"phase\":<that phase's name verbatim>,\"property\":\"all\",\"durationMs\":<recipe duration for THAT phase>,\"easing\":<recipe easing>,\"scale\":<recipe pre-scale for THAT phase>,\"blur\":<recipe pre-blur px for THAT phase>}`. Take each phase's duration from the MOTION TOKENS for that phase open is usually slower than close (e.g. 250ms open / 150ms close) — and use the same easing for both unless the recipe differs. Include `scale`/`blur` ONLY when the recipe animates transform-scale / filter-blur (omit them otherwise), using the Scales/Blur tokens for that phase. Also include a single `patch` equal to the FIRST phase's entry (it drives the live preview). Apply will write every phase from `patches`.",
303
+ "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 also uses transform pre-scale or filter pre-blur, add EXTRA per-lane value patches for those lanes (not property:\"all\") and include that lane's `member` + `varName` from input timings, e.g. `{\"phase\":\"open\",\"property\":\"transform\",\"member\":\"Panel\",\"varName\":\"--dropdown-pre-scale\",\"scale\":0.97}` and `{\"phase\":\"open\",\"property\":\"filter\",\"member\":\"Panel\",\"varName\":\"--panel-blur\",\"blur\":2}`. Take each phase duration from MOTION TOKENS (open usually slower than close, e.g. 250ms/150ms). Also include a single `patch` for live preview (first timing patch is fine). Apply will write every phase from `patches`.",
304
304
  "",
305
305
  );
306
306
  }
@@ -335,7 +335,7 @@ function buildPrompt(job) {
335
335
  '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":"…"}.',
336
336
  "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.",
337
337
  multiPhase
338
- ? 'A multi-phase replace suggestion ALSO carries a `patches` array, e.g. "patches":[{"phase":"open","property":"all","durationMs":250,"easing":"cubic-bezier(0.22, 1, 0.36, 1)","scale":0.97},{"phase":"close","property":"all","durationMs":150,"easing":"cubic-bezier(0.22, 1, 0.36, 1)","scale":0.99}]. In each `patch`/`patches` entry 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".'
338
+ ? '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".'
339
339
  : '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.',
340
340
  );
341
341
  return lines.join("\n");