transitions-refine 0.3.28 → 0.3.30

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
@@ -4872,11 +4872,34 @@
4872
4872
  // A replace suggestion may carry `patches` (one per related phase, e.g.
4873
4873
  // open + close) — a recipe swap is one motion, so applying it writes BOTH.
4874
4874
  // Otherwise fall back to the single `patch` on the active phase.
4875
- const patches=Array.isArray(s.patches)&&s.patches.length
4875
+ let patches=Array.isArray(s.patches)&&s.patches.length
4876
4876
  ? s.patches
4877
4877
  : (s.patch&&s.patch.property?[{...s.patch,phase:(active&&active.phase)||null}]:[]);
4878
4878
  if(!patches.length)return;
4879
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
+ }
4880
4903
  // Recipe timing per phase: a replace emits a phase-level timing patch
4881
4904
  // (property:"all", duration+easing) plus lane-level value patches. An
4882
4905
  // ADDED lane (e.g. a blur/filter lane the source never had) must ride the
@@ -5145,10 +5168,32 @@
5145
5168
  if(sp.toSuf) active=stEls.length?stEls.some(e=>safeMatches(e,sp.toSuf)):true; // open
5146
5169
  else if(sp.fromSuf) active=stEls.length?stEls.some(e=>!safeMatches(e,sp.fromSuf)):true; // close = not in fromState
5147
5170
  else active=true; // no state info → always reflect (degrades to old behavior)
5171
+ const spVars={}; // union of this phase's value vars (scale/blur/translate)
5172
+ const memberEls=[]; // {el,css} of resolved members this phase
5148
5173
  for(const mm of sp.members) for(const el of txMemberEls(mm.selector,sp.stateTarget)){
5149
5174
  if(inUI(el))continue;
5150
- if(mm.vars&&Object.keys(mm.vars).length){ const ex=desiredVars.get(el)||{}; desiredVars.set(el,{...ex,...mm.vars}); }
5175
+ if(mm.vars&&Object.keys(mm.vars).length){ const ex=desiredVars.get(el)||{}; desiredVars.set(el,{...ex,...mm.vars}); Object.assign(spVars,mm.vars); }
5151
5176
  if(active) desiredCss.set(el,mm.css);
5177
+ memberEls.push({el,css:mm.css});
5178
+ }
5179
+ // SHARED-VAR recipes (icon/text swap, page slide, …) drive the non-
5180
+ // resting look through a custom property read by SEVERAL stacked
5181
+ // members from a COMMON ANCESTOR — and the "from" member is often not
5182
+ // even a captured lane (e.g. only opacity animates at rest, so the
5183
+ // entering icon has no filter/transform lane). Writing the var inline
5184
+ // on one member can't reach its siblings, and only the captured
5185
+ // member gets the recipe timing. So when a phase carries value vars:
5186
+ // (1) set the vars on the stateTarget container — every descendant
5187
+ // member inherits them, and each state's own CSS rule
5188
+ // (active → blur(0)/scale(1); inactive → blur(var)/scale(var))
5189
+ // gates where they actually show; and
5190
+ // (2) give the same recipe timing to the member's same-slot stacked
5191
+ // siblings (element children of the same parent sharing a class)
5192
+ // so the ENTERING member animates the new filter/transform lanes
5193
+ // instead of snapping.
5194
+ if(Object.keys(spVars).length){
5195
+ for(const el of stEls){ if(inUI(el))continue; const ex=desiredVars.get(el)||{}; desiredVars.set(el,{...ex,...spVars}); }
5196
+ if(active) for(const{el,css}of memberEls){ const parent=el.parentElement; if(!parent)continue; const cls=[...(el.classList||[])]; for(const sib of Array.from(parent.children)){ if(sib===el||inUI(sib)||!sib.classList)continue; if(cls.some(c=>sib.classList.contains(c))&&!desiredCss.has(sib))desiredCss.set(sib,css); } }
5152
5197
  }
5153
5198
  }
5154
5199
  const allEls=new Set([...desiredCss.keys(),...desiredVars.keys()]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "transitions-refine",
3
- "version": "0.3.28",
3
+ "version": "0.3.30",
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
@@ -278,6 +278,7 @@ const RECIPES_BLOCK = [
278
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
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
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`.",
281
282
  " - ASYMMETRIC recipes (open slower, close faster): menu dropdown / modal / tooltip / panel reveal → open 250ms, close 150ms.",
282
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.",
283
284
  ].join("\n");