transitions-refine 0.3.27 → 0.3.29

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
@@ -2633,9 +2633,12 @@
2633
2633
  if(tm){const sc=tm[1].match(/scale\(\s*([^),]+)/i);
2634
2634
  if(sc){const inner=sc[1].trim();const vm=inner.match(/var\(\s*(--[\w-]+)/);
2635
2635
  const num=vm?parseFloat(cs.getPropertyValue(vm[1])):parseFloat(inner);
2636
- // pick the value furthest from the resting 1 (the real pre-scale).
2637
- if(Number.isFinite(num)&&Math.abs(num-1)>1e-4&&(!bestScale||Math.abs(num-1)>Math.abs(bestScale.scale-1)))
2638
- bestScale={scale:num,...(vm?{varName:vm[1]}:{})};}
2636
+ // pick the value furthest from the resting 1 (the real pre-scale);
2637
+ // a var-backed scale at rest (1) still reports its var — the hook
2638
+ // for ADDING a start-scale the source neutralized (e.g. icon swap).
2639
+ if(Number.isFinite(num)&&(Math.abs(num-1)>1e-4||vm)){
2640
+ const better=!bestScale||Math.abs(num-1)>Math.abs(bestScale.scale-1)||(bestScale.varName==null&&vm);
2641
+ if(better)bestScale={scale:num,...(vm?{varName:vm[1]}:{})};}}
2639
2642
  const tr=tm[1].match(/translate(?:X|Y|3d)?\(\s*([^),]+)/i);
2640
2643
  if(tr){const inner=tr[1].trim();const vm=inner.match(/var\(\s*(--[\w-]+)/);
2641
2644
  const vn=vm?rootVar(vm[1]):null;
@@ -4869,11 +4872,55 @@
4869
4872
  // A replace suggestion may carry `patches` (one per related phase, e.g.
4870
4873
  // open + close) — a recipe swap is one motion, so applying it writes BOTH.
4871
4874
  // Otherwise fall back to the single `patch` on the active phase.
4872
- const patches=Array.isArray(s.patches)&&s.patches.length
4875
+ let patches=Array.isArray(s.patches)&&s.patches.length
4873
4876
  ? s.patches
4874
4877
  : (s.patch&&s.patch.property?[{...s.patch,phase:(active&&active.phase)||null}]:[]);
4875
4878
  if(!patches.length)return;
4876
4879
  const groupId=active&&active.groupId;
4880
+ // Same-slot swap recipes (icon/text swaps) use two stacked members in one
4881
+ // group. The agent can sometimes return the recipe's blur/scale value for
4882
+ // only the currently selected member; expand those value patches to every
4883
+ // sibling member so replace applies the WHOLE group, not just the exiting
4884
+ // icon/text.
4885
+ const isSameSlotSwap=s.kind==="replace"&&/icon[-\s]?swap|text states swap|text[-\s]?swap|09-icon-swap|04-text-states-swap/i.test([s.title,s.reference,s.reason].filter(Boolean).join(" "));
4886
+ if(isSameSlotSwap&&groupId){
4887
+ 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
+ const valuePatches=patches.filter(p=>p.member&&(p.blur!=null||p.scale!=null)&&(p.property==="filter"||p.property==="transform"));
4889
+ for(const p of valuePatches){
4890
+ const target=entries.find(e=>e.kind==="phase"&&e.groupId===groupId&&(!p.phase||e.phase===p.phase))||active;
4891
+ const members=new Map();
4892
+ for(const t of (target&&target.effectiveTimings)||[]){
4893
+ const m=t.member||t.memberId;if(m&&!members.has(m))members.set(m,m);
4894
+ }
4895
+ for(const member of members.values()){
4896
+ const k=[p.phase||"",p.property||"",member,p.blur!=null?"blur":"",p.scale!=null?"scale":"",p.translate!=null?"translate":""].join("|");
4897
+ if(has.has(k))continue;
4898
+ has.add(k); extra.push({...p,member});
4899
+ }
4900
+ }
4901
+ if(extra.length)patches=patches.concat(extra);
4902
+ }
4903
+ // Recipe timing per phase: a replace emits a phase-level timing patch
4904
+ // (property:"all", duration+easing) plus lane-level value patches. An
4905
+ // ADDED lane (e.g. a blur/filter lane the source never had) must ride the
4906
+ // recipe's phase timing — NOT the source lane's leftover timing — so the
4907
+ // whole recipe moves in lockstep (e.g. filter deblurs exactly as the page
4908
+ // finishes sliding, instead of lagging on the old duration). Collect the
4909
+ // phase timing up front so synthesis is order-independent.
4910
+ const phaseTiming=new Map(); // phaseKey -> {durationMs,delayMs,easing}
4911
+ for(const p of patches){
4912
+ if(p.durationMs==null&&p.easing==null&&p.delayMs==null)continue;
4913
+ const key=p.phase||"";
4914
+ const cur=phaseTiming.get(key)||{};
4915
+ // a coarse property:"all" patch describes the whole phase; prefer it.
4916
+ if(p.property==="all"||!phaseTiming.has(key)){
4917
+ phaseTiming.set(key,{
4918
+ durationMs:p.durationMs??cur.durationMs,
4919
+ delayMs:p.delayMs??cur.delayMs,
4920
+ easing:p.easing??cur.easing,
4921
+ });
4922
+ }
4923
+ }
4877
4924
  for(const p of patches){
4878
4925
  if(!p.property)continue;
4879
4926
  const timingOverride={};
@@ -4910,20 +4957,31 @@
4910
4957
  // (readElValueHint reports var-backed values even at their resting 0),
4911
4958
  // so the patch doesn't need to know the page's CSS custom properties.
4912
4959
  const sibling=(prop)=>memberLanes.find(t=>t.property===prop)||memberLanes[0]||et[0]||null;
4960
+ // Prefer this patch's own timing, then the recipe's phase timing, then
4961
+ // the sibling lane's timing — so an added lane matches the recipe, not
4962
+ // the source's stale duration/easing.
4963
+ const pt=phaseTiming.get(p.phase||"")||{};
4964
+ // recipe timing to stamp on a value lane (blur/scale/translate) so it
4965
+ // rides the recipe duration/easing, not the source's leftover timing.
4966
+ const recipeTiming=()=>{const t={};if(pt.durationMs!=null)t.durationMs=pt.durationMs;if(pt.delayMs!=null)t.delayMs=pt.delayMs;if(pt.easing!=null)t.easing=pt.easing;return t;};
4913
4967
  const synthTiming=(base)=>({
4914
- durationMs:p.durationMs??(base&&base.durationMs)??250,
4915
- delayMs:p.delayMs??(base&&base.delayMs)??0,
4916
- easing:p.easing??(base&&base.easing)??"ease",
4968
+ durationMs:p.durationMs??pt.durationMs??(base&&base.durationMs)??250,
4969
+ delayMs:p.delayMs??pt.delayMs??(base&&base.delayMs)??0,
4970
+ easing:p.easing??pt.easing??(base&&base.easing)??"ease",
4917
4971
  });
4918
4972
  const applyValue=(prop,mk)=>{
4919
4973
  // mk(lane, hint) → override fields for one lane, or null to skip.
4920
4974
  let vLanes=lanes.filter(t=>t.property===prop);
4921
4975
  if(!vLanes.length&&p.property==="all")vLanes=et.filter(t=>t.property===prop&&(!p.member||t.member===p.member||t.memberId===p.member));
4922
4976
  if(vLanes.length){
4977
+ const rt=recipeTiming();
4923
4978
  for(const lane of vLanes){
4924
4979
  const hint=laneValueHint(lane.selector,prop);
4925
4980
  const ov=mk(lane,hint);
4926
- if(ov)registry.setPropOverride(target.id,lane.laneId,ov);
4981
+ // an EXISTING lane the recipe reuses (e.g. Page side-by-side's
4982
+ // filter lane at the source's 300ms) must ALSO adopt the recipe's
4983
+ // phase duration/easing, so the value settles in lockstep.
4984
+ if(ov)registry.setPropOverride(target.id,lane.laneId,{...rt,...ov});
4927
4985
  }
4928
4986
  return;
4929
4987
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "transitions-refine",
3
- "version": "0.3.27",
3
+ "version": "0.3.29",
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
@@ -268,13 +268,18 @@ const RECIPES_BLOCK = [
268
268
  "Recipe non-resting values — the pre-blur/pre-scale/slide-distance the recipe animates FROM (settling to blur 0 / scale 1 / translate 0). When you pick one of these recipes you MUST also emit a lane-level value patch for EACH of its values, EVEN IF the input has no such lane (add the lane so the element gains the recipe's blur/scale/distance):",
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
- " - Icon swap → filter blur 2px (Small)",
271
+ " - Icon swap → filter blur 2px (Small) AND transform scale 0.25 (start-scale) on the swapping icon",
272
272
  " - Skeleton loader and reveal → filter blur 2px (Small)",
273
273
  " - Texts reveal → filter blur 3px (Medium)",
274
274
  " - Success check → filter blur 8px (Large)",
275
275
  " - Modal open/close → transform scale 0.96 (Large)",
276
276
  " - Menu dropdown → transform scale 0.97 (Medium) open, 0.99 (Tiny) close",
277
277
  " - Tooltip open/close → transform scale 0.98 (Small)",
278
+ "Recipe timing & easing — SET the transition to the recipe's documented values; do NOT keep the source's duration/easing when they differ from the recipe (a value being an on-grid token is NOT a reason to keep it if the recipe uses a different one):",
279
+ " - Easing: most surface/slide/reveal recipes use cubic-bezier(0.22, 1, 0.36, 1) (Smooth ease out) — page side-by-side, panel reveal, menu dropdown, modal, tooltip, accordion, tabs sliding, card resize, texts reveal, success check. Cross-fade swaps use ease-in-out — icon swap, text states swap. Overshoot curves stay as documented — notification badge / number pop-in cubic-bezier(0.34, 1.36, 0.64, 1), avatar return cubic-bezier(0.34, 3.85, 0.64, 1).",
280
+ " - SYMMETRIC recipes (SAME duration + easing BOTH directions — do NOT split open/close): page side-by-side (250ms), tabs sliding (250ms), accordion (250ms), icon swap (250ms), text states swap (150ms). Emit identical durationMs/easing for every phase.",
281
+ " - WHOLE-GROUP swaps: icon swap and text states swap are two stacked members in the same slot. A replace suggestion MUST patch BOTH members in EVERY phase, not just the currently selected/exiting member. Emit blur/scale (icon) or blur/translate (text) lane patches once per member per phase, copying each member name from `phases[].timings`.",
282
+ " - ASYMMETRIC recipes (open slower, close faster): menu dropdown / modal / tooltip / panel reveal → open 250ms, close 150ms.",
278
283
  "Tie-break: prefer the lower-overhead recipe (card resize over panel reveal, dropdown over modal). If no recipe genuinely fits, return an empty suggestions array.",
279
284
  ].join("\n");
280
285
 
@@ -317,7 +322,7 @@ function buildPrompt(job) {
317
322
  );
318
323
  if (multiPhase) {
319
324
  lines.push(
320
- "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 uses transform pre-scale, filter pre-blur or a slide distance (see \"Recipe non-resting values\" above), add EXTRA per-lane value patches for those lanes (not property:\"all\"), e.g. `{\"phase\":\"open\",\"property\":\"transform\",\"member\":\"Panel\",\"varName\":\"--dropdown-pre-scale\",\"scale\":0.97}`, `{\"phase\":\"open\",\"property\":\"filter\",\"member\":\"Panel\",\"varName\":\"--panel-blur\",\"blur\":2}` or `{\"phase\":\"forward\",\"property\":\"transform\",\"member\":\"Page panel\",\"translateVarName\":\"--page-slide-distance\",\"translate\":8}`. Copy each lane's `member` + `varName`/`translateVarName` from the input timings when that lane exists; when the recipe adds a lane the input LACKS (e.g. Page side-by-side prescribes a 3px blur but the captured page has no `filter` lane), STILL emit the value patch — reuse the sibling lane's `member` so it targets the right element and omit the var name (Apply discovers the page's backing variable itself). 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`.",
325
+ "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 uses transform pre-scale, filter pre-blur or a slide distance (see \"Recipe non-resting values\" above), add EXTRA per-lane value patches for those lanes (not property:\"all\"), e.g. `{\"phase\":\"open\",\"property\":\"transform\",\"member\":\"Panel\",\"varName\":\"--dropdown-pre-scale\",\"scale\":0.97}`, `{\"phase\":\"open\",\"property\":\"filter\",\"member\":\"Panel\",\"varName\":\"--panel-blur\",\"blur\":2}` or `{\"phase\":\"forward\",\"property\":\"transform\",\"member\":\"Page panel\",\"translateVarName\":\"--page-slide-distance\",\"translate\":8}`. Copy each lane's `member` + `varName`/`translateVarName` from the input timings when that lane exists; when the recipe adds a lane the input LACKS (e.g. Page side-by-side prescribes a 3px blur but the captured page has no `filter` lane), STILL emit the value patch — reuse the sibling lane's `member` so it targets the right element and omit the var name (Apply discovers the page's backing variable itself). Set each phase's `durationMs` AND `easing` from \"Recipe timing & easing\" above (SYMMETRIC recipes use the SAME duration+easing for every phase — do NOT split open/close; ASYMMETRIC recipes use open 250ms / close 150ms). The added value lanes (blur/translate) automatically inherit their phase's timing on Apply, so you don't need to repeat duration/easing on those lane-level patches. Also include a single `patch` for live preview (first timing patch is fine). Apply will write every phase from `patches`.",
321
326
  "",
322
327
  );
323
328
  }
@@ -327,7 +332,7 @@ function buildPrompt(job) {
327
332
  "refineType is \"replace\": suggest a WHOLE-TRANSITION replacement ONLY — do NOT propose motion-token tweaks (no kind \"duration\"/\"delay\"/\"easing\").",
328
333
  multiPhase
329
334
  ? "Pick the SINGLE best-fit recipe from the list above. Emit ONE suggestion with kind \"replace\": include the `patches` array (one entry per related phase, as described above) AND a single `patch` (the first phase) for the live preview, add a `reference` field with the reference filename, and name the recipe in `title`/`reason`. If no recipe genuinely fits the usage, return an empty suggestions array."
330
- : "Pick the SINGLE best-fit recipe from the list above. Emit ONE suggestion with kind \"replace\": set its `patch` to the motion-token duration/easing for the recipe's phase on the property that already transitions (or \"all\") so Apply works live, add a `reference` field with the reference filename, and name the recipe in `title`/`reason`. If the recipe prescribes a non-resting pre-blur/pre-scale/slide-distance (see \"Recipe non-resting values\" above), put those values on the `patch` too (add `blur` for a filter-blur recipe like Page side-by-side → 3px, `scale` for a scale recipe, `translate` for a slide distance like Page side-by-side → 8px) so the sliding/scaling element gains them even when the captured transition had no such lane. If no recipe genuinely fits the usage, return an empty suggestions array.",
335
+ : "Pick the SINGLE best-fit recipe from the list above. Emit ONE suggestion with kind \"replace\": set its `patch` to the recipe's documented duration AND easing (see \"Recipe timing & easing\" — do NOT keep the source's easing when the recipe differs) on the property that already transitions (or \"all\") so Apply works live, add a `reference` field with the reference filename, and name the recipe in `title`/`reason`. If the recipe prescribes a non-resting pre-blur/pre-scale/slide-distance (see \"Recipe non-resting values\" above), put those values on the `patch` too (add `blur` for a filter-blur recipe like Page side-by-side → 3px, `scale` for a scale recipe, `translate` for a slide distance like Page side-by-side → 8px) so the sliding/scaling element gains them even when the captured transition had no such lane. If no recipe genuinely fits the usage, return an empty suggestions array.",
331
336
  "Answer in ONE response — do NOT read or search files.",
332
337
  );
333
338
  } else if (refineType === "both") {
@@ -337,7 +342,7 @@ function buildPrompt(job) {
337
342
  MISSING_BLUR_NOTE,
338
343
  multiPhase
339
344
  ? "(2) Whole-transition replacement (kind \"replace\"): ALWAYS evaluate one — pick the SINGLE best-fit recipe. Emit at most ONE \"replace\" suggestion with the `patches` array (one entry per related phase) AND a single `patch` (the first phase), a `reference` field, and the recipe named in `title`/`reason`. If no recipe genuinely fits, omit the replace suggestion."
340
- : "(2) Whole-transition replacement (kind \"replace\"): ALWAYS evaluate one — pick the SINGLE best-fit recipe from the list above. Emit at most ONE \"replace\" suggestion: set its `patch` to the motion-token duration/easing for the recipe's phase on the property that already transitions (or \"all\"); if the recipe prescribes a non-resting pre-blur/pre-scale/slide-distance (see \"Recipe non-resting values\" above) put those `blur`/`scale`/`translate` values on the `patch` too so e.g. Page side-by-side gains its 3px blur and 8px distance. Add a `reference` field with the reference filename, and name the recipe in `title`/`reason`. If no recipe genuinely fits, omit the replace suggestion.",
345
+ : "(2) Whole-transition replacement (kind \"replace\"): ALWAYS evaluate one — pick the SINGLE best-fit recipe from the list above. Emit at most ONE \"replace\" suggestion: set its `patch` to the recipe's documented duration AND easing (see \"Recipe timing & easing\" — do NOT keep the source's easing when the recipe differs) on the property that already transitions (or \"all\"); if the recipe prescribes a non-resting pre-blur/pre-scale/slide-distance (see \"Recipe non-resting values\" above) put those `blur`/`scale`/`translate` values on the `patch` too so e.g. Page side-by-side gains its 3px blur and 8px distance. Add a `reference` field with the reference filename, and name the recipe in `title`/`reason`. If no recipe genuinely fits, omit the replace suggestion.",
341
346
  "Answer in ONE response — do NOT read or search files.",
342
347
  );
343
348
  } else {