@clicktap/ui 0.40.8 → 0.40.9
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/docs/migrations/2026/01/2026-01-14-external-link-detection.md +290 -0
- package/docs/migrations/2026/01/2026-01-14-use-route-hook.md +142 -0
- package/docs/migrations/2026/01/2026-01-15-input-disable-skeleton.md +82 -0
- package/docs/migrations/2026/02/2026-02-16-env-utils-from-clicktap-ui.md +94 -0
- package/docs/migrations/2026/02/2026-02-17-select-popover-portal-container-restored.md +84 -0
- package/docs/migrations/2026/03/2026-03-24-image-dimmed-hidden-onready.md +108 -0
- package/docs/migrations/2026/03/2026-03-24-modal-overlay-backdrop-dismiss.md +81 -0
- package/docs/migrations/2026/04/2026-04-17-select-address-input-onchange-key-or-item.md +182 -0
- package/docs/migrations/2026/04/2026-04-24-image-placeholder-frontend-fallback.md +85 -0
- package/docs/migrations/2026/04/2026-04-30-prune-invalid-pdp-selections.md +166 -0
- package/docs/migrations/2026/04/2026-04-30-top-down-pdp-option-dependency.md +286 -0
- package/docs/migrations/2026/05/2026-05-01-build-pdp-url-util.md +128 -0
- package/docs/migrations/2026/05/2026-05-03-auth-aware-graphql-client.md +278 -0
- package/docs/migrations/2026/05/2026-05-20-framer-motion-11-idioms.md +391 -0
- package/docs/migrations/2026/05/2026-05-21-button-loading-content.md +265 -0
- package/docs/migrations/2026/05/2026-05-21-password-check-hide-on-empty.md +50 -0
- package/docs/migrations/2026/05/2026-05-29-password-check-requirements-variant.md +63 -0
- package/docs/migrations/2026/06/2026-06-17-select-standardization.md +286 -0
- package/docs/migrations/2026/06/2026-06-18-select-hiddenselect-setvalue-crash.md +75 -0
- package/docs/migrations/2026/07/2026-07-31-ui-exports-bundler-resolution.md +50 -0
- package/docs/migrations/README.md +31 -0
- package/package.json +1 -1
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
# framer-motion 11.x idioms — opacity-only for modal entry/exit, primitives over transform strings, matched keyframe shapes
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-05-20
|
|
4
|
+
**Affects:** `@clicktap/ui/components/Dialog`, `@clicktap/ui/components/ModalOverlay`, `@clicktap/ui/components/Badge`, `@clicktap/ui/components/Loader/CircularEasing`, `@clicktap/ui/components/Loader/Pulse`, and any consumer-side `motion.*` usage on framer-motion 11.x.
|
|
5
|
+
**Classification:** COMPATIBILITY
|
|
6
|
+
**Breaking:** Yes (for consumers passing custom `animationVariants` containing multi-function `transform` strings, transform/scale primitives in modal variants, calc-with-mixed-units in animate targets, or who copy-paste the affected motion patterns into their own components — see Consumer Impact)
|
|
7
|
+
|
|
8
|
+
## Dependencies
|
|
9
|
+
|
|
10
|
+
*None.* Targets framer-motion 11.18.
|
|
11
|
+
|
|
12
|
+
## Summary
|
|
13
|
+
|
|
14
|
+
framer-motion 11.x's keyframe resolver crashes inside `mixObject` (`complex.mjs:48`) with `Cannot read properties of null (reading '0')` more often than the docs suggest. The root cause is `getMixer(null)`: `typeof null === 'object'` in JavaScript, so when the resolver hands a null keyframe value into the mixer factory it returns `mixObject`, and `mixObject` then dereferences a key on null and throws. Any time the resolver leaves a null in the keyframes array — unresolved DOM-dependent value, a key animated in one phase but missing in another, a complex/calc value that fails to parse, an un-interpolatable CSS property batched with other unresolved keyframes — it crashes here.
|
|
15
|
+
|
|
16
|
+
We hit this bug repeatedly across the UI library and the frontend app. After iterating on several "use primitives instead of transform strings" attempts that *still* crashed under realistic ancestor-AnimatePresence load, we landed on a small set of rules that hold:
|
|
17
|
+
|
|
18
|
+
1. **Modal entry/exit animations: animate `opacity` only.** Centering, scale, and any other transform-related styling stays in static CSS (Tailwind). The Dialog and ModalOverlay default variants now look like `{ opacity: 0 } ↔ { opacity: 1 }`, full stop.
|
|
19
|
+
2. **For non-modal motion (toasts, badges, page-level enter/exit), prefer framer-motion primitives (`x`, `y`, `rotate`, `scale`, `opacity`) over multi-function CSS `transform` strings.** Primitives compose into a single transform that framer-motion owns; transform strings go through the complex-value parser, which is the buggy code path.
|
|
20
|
+
3. **Every key that `animate` touches must also be present in `initial` and `exit`.** When you have a no-animation branch (`shouldAnimate ? animatedShape : skippedShape`), the skipped shape still has to include the same keys with sensible base values (usually `0`).
|
|
21
|
+
4. **Resolve unit math to pixels in JS before handing it to framer-motion.** Strings like `calc(-100% + Npx + Yrem)` are complex values; the parser can't reliably mix them across remeasure cycles.
|
|
22
|
+
5. **Anything WAAPI can't interpolate goes on a static CSS class, not in `variants`.** `backdrop-filter`, complex `clip-path`, gradient `background`, `pointer-events`, etc.
|
|
23
|
+
|
|
24
|
+
Until framer-motion 12.x lands (where this resolver is rewritten), treat these rules as the long-term convention, not a workaround.
|
|
25
|
+
|
|
26
|
+
## Problem
|
|
27
|
+
|
|
28
|
+
A timeline of what failed before we got to the right rule set:
|
|
29
|
+
|
|
30
|
+
1. **Dialog rendered off-center** — framer-motion was writing its own inline `transform` for `scale`, overwriting Tailwind `-translate-x-1/2 -translate-y-1/2` centering classes.
|
|
31
|
+
2. **`mixObject` crash on the Cybersource / Auth.Net checkout step** — saved-cards ↔ new-card-form toggle exposed two contributing causes: Dialog variants used multi-function `transform` strings, AND the `motion.div` toggling between forms had mismatched `initial` / `animate` / `exit` keys.
|
|
32
|
+
3. **`mixObject` crash on the Add Address modal** — Dialog had been "fixed" with primitives at this point, but the Badge cart-count component still animated `transform: 'scale(0/1)'` strings. Ancestor remeasure from the modal open triggered Badge's variants through the complex parser.
|
|
33
|
+
4. **Loader spinners crash similarly** — `CircularEasing` animated `transform: 'rotate(360deg)'` and `Pulse` animated `transform: ['scale(...)', ...]` array. Same complex-parser path.
|
|
34
|
+
5. **Toast position crash** — `Toast.tsx` animated `translateY: 'calc(-100% + Npx + Yrem)'` — a calc with mixed units (`%`, `px`, `rem`). The parser couldn't keep up across re-runs on layout shifts.
|
|
35
|
+
6. **Add Address modal *still* crashed** even with all the above fixed and Dialog variants on `x`/`y`/`scale` primitives. The final cause turned out to be the modal's own variants — `x: '-50%'` primitives still routed through the complex-value path under enough load. Once we dropped to opacity-only, the crash stopped.
|
|
36
|
+
|
|
37
|
+
The pattern is consistent: framer-motion 11.x's keyframe resolver gets confused when batched keyframe resolution sees any kind of non-trivial value (string with units, calc, multi-function transform, pointer-events, etc.). Opacity is reliably safe.
|
|
38
|
+
|
|
39
|
+
## Changes
|
|
40
|
+
|
|
41
|
+
| File | Change |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `libs/ui/src/components/Dialog/Dialog.tsx` | Default `animationVariants` now `{ opacity: 0 } ↔ { opacity: 1 }` — no scale, no x/y. Centering restored to static Tailwind classes (`-translate-x-1/2 -translate-y-1/2`). |
|
|
44
|
+
| `libs/ui/src/components/ModalOverlay/ModalOverlay.tsx` | Default variants reduced to opacity-only; `pointerEvents` removed from variants. Backdrop blur stays on the static CSS class (already was). |
|
|
45
|
+
| `libs/ui/src/components/Badge/Badge.tsx` | `animationVariants` switched from `transform: 'scale(0/1)'` strings to `scale: 0 / 1` primitives. |
|
|
46
|
+
| `libs/ui/src/components/Loader/CircularEasing.tsx` | Spinner `animate` switched from `transform: 'rotate(360deg)'` to `rotate: 360` primitive. |
|
|
47
|
+
| `libs/ui/src/components/Loader/Pulse.tsx` | Pulse keyframes switched from `transform: ['scale(1)', 'scale(0.1)', ...]` to `scale: [1, 0.1, ...]` numeric keyframes. |
|
|
48
|
+
| `apps/frontend/components/Toast/Toast.tsx` | Container `translateY` calc string replaced with a numeric pixel `y` value. Container-height and rem-to-pixel resolved in JS before handing to framer-motion. |
|
|
49
|
+
| `apps/frontend/components/Toast/Notification.tsx` | `initial` / `animate` / `exit` shapes aligned (all three include `opacity`, `x`, `y`). `translateX`/`translateY` aliases dropped in favor of `x`/`y` primitives. |
|
|
50
|
+
| `apps/frontend/components/Checkout/Step/Payment/Cybersource/CybersourcePaymentMethod.tsx` | The no-animate branch of `initial` previously returned `{ opacity: 1 }` without `x`. Both branches now include `x: 0`, and `exit` includes `x: 0`. |
|
|
51
|
+
| `apps/frontend/components/Checkout/Step/Payment/Authorizenet/AuthorizenetPaymentMethod.tsx` | Same shape-consistency fix as Cybersource. |
|
|
52
|
+
|
|
53
|
+
### Dialog variants — final state
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// before — multi-function transform string; off-center; crashes under load
|
|
57
|
+
variants={
|
|
58
|
+
animationVariants || {
|
|
59
|
+
hidden: { transform: 'translate(-50%, -50%) scale(0.8)', opacity: 0 },
|
|
60
|
+
visible: { transform: 'translate(-50%, -50%) scale(1)', opacity: 1 },
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
// after — opacity only; centering moved to static Tailwind
|
|
67
|
+
className={cn(
|
|
68
|
+
'p-8 outline-0 max-w-max w-screen absolute top-2/4 left-2/4',
|
|
69
|
+
'-translate-x-1/2 -translate-y-1/2', // ← centering back in CSS
|
|
70
|
+
'shadow-... rounded-lg bg-white border ...',
|
|
71
|
+
className,
|
|
72
|
+
)}
|
|
73
|
+
// ...
|
|
74
|
+
variants={
|
|
75
|
+
animationVariants || {
|
|
76
|
+
hidden: { opacity: 0 },
|
|
77
|
+
visible: { opacity: 1 },
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### ModalOverlay variants — final state
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
// before
|
|
86
|
+
variants={
|
|
87
|
+
animationVariants || {
|
|
88
|
+
hidden: {
|
|
89
|
+
opacity: 0,
|
|
90
|
+
pointerEvents: 'none', // ← non-interpolatable; risky
|
|
91
|
+
transition: { delay: 0.08 },
|
|
92
|
+
},
|
|
93
|
+
visible: { opacity: 1, pointerEvents: 'auto' },
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
// after — opacity only; pointerEvents handled via static class on the overlay
|
|
100
|
+
variants={
|
|
101
|
+
animationVariants || {
|
|
102
|
+
hidden: { opacity: 0, transition: { delay: 0.08 } },
|
|
103
|
+
visible: { opacity: 1 },
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Badge variants — primitive swap
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
// before
|
|
112
|
+
const animationVariants = {
|
|
113
|
+
hidden: { opacity: 0, transform: 'scale(0)' },
|
|
114
|
+
show: { opacity: 1, transform: 'scale(1)' },
|
|
115
|
+
};
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
// after
|
|
120
|
+
const animationVariants = {
|
|
121
|
+
hidden: { opacity: 0, scale: 0 },
|
|
122
|
+
show: { opacity: 1, scale: 1 },
|
|
123
|
+
};
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Loader spinners — primitive swap
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
// CircularEasing — before
|
|
130
|
+
<motion.svg animate={{ transform: 'rotate(360deg)', transition: {...} }} />
|
|
131
|
+
|
|
132
|
+
// CircularEasing — after
|
|
133
|
+
<motion.svg animate={{ rotate: 360, transition: {...} }} />
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
// Pulse — before
|
|
138
|
+
<motion.div animate={{
|
|
139
|
+
opacity: [1, 1, 0.7, 1, 1],
|
|
140
|
+
transform: ['scale(1)', 'scale(1)', 'scale(0.1)', 'scale(1)', 'scale(1)'],
|
|
141
|
+
transition: {...},
|
|
142
|
+
}} />
|
|
143
|
+
|
|
144
|
+
// Pulse — after
|
|
145
|
+
<motion.div animate={{
|
|
146
|
+
opacity: [1, 1, 0.7, 1, 1],
|
|
147
|
+
scale: [1, 1, 0.1, 1, 1],
|
|
148
|
+
transition: {...},
|
|
149
|
+
}} />
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Toast container — calc → numeric pixels
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
// before — calc with mixed units; complex parser stumbles on remeasure
|
|
156
|
+
const h = toastRef.current?.children?.item(y === 'top' ? notifications.length - 1 : 0)
|
|
157
|
+
?.getBoundingClientRect().height;
|
|
158
|
+
const translateY = isMobile
|
|
159
|
+
? { translateY: mobileY === 'top'
|
|
160
|
+
? `calc(-100% + ${h ?? 0}px + 1rem)`
|
|
161
|
+
: `calc(100% - ${h ?? 0}px - 1rem)` }
|
|
162
|
+
: { translateY: y === 'top'
|
|
163
|
+
? `calc(-100% + ${h ?? 0}px + 8rem)`
|
|
164
|
+
: `calc(100% - ${h ?? 0}px - 3.5rem)` };
|
|
165
|
+
toastControls.set(translateY);
|
|
166
|
+
toastControls.start(translateY);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
// after — resolve to a single number, hand framer-motion a primitive
|
|
171
|
+
const h = toastRef.current?.children?.item(y === 'top' ? notifications.length - 1 : 0)
|
|
172
|
+
?.getBoundingClientRect().height ?? 0;
|
|
173
|
+
const containerHeight =
|
|
174
|
+
toastRef.current?.getBoundingClientRect().height ?? 0;
|
|
175
|
+
const remPx =
|
|
176
|
+
typeof window !== 'undefined'
|
|
177
|
+
? parseFloat(getComputedStyle(document.documentElement).fontSize) || 16
|
|
178
|
+
: 16;
|
|
179
|
+
const translateYValue = isMobile
|
|
180
|
+
? mobileY === 'top'
|
|
181
|
+
? -containerHeight + h + 1 * remPx
|
|
182
|
+
: containerHeight - h - 1 * remPx
|
|
183
|
+
: y === 'top'
|
|
184
|
+
? -containerHeight + h + 8 * remPx
|
|
185
|
+
: containerHeight - h - 3.5 * remPx;
|
|
186
|
+
const translateY = { y: translateYValue };
|
|
187
|
+
toastControls.set(translateY);
|
|
188
|
+
toastControls.start(translateY);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Notification — shape consistency across initial/animate/exit
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
// before — translateY/translateX aliases, exit drops the transform keys entirely
|
|
195
|
+
const initial = isMobile
|
|
196
|
+
? { opacity: 0, translateY: '200px' }
|
|
197
|
+
: { opacity: 0, translateX: '200px' };
|
|
198
|
+
const animate = isMobile
|
|
199
|
+
? { opacity: 1, translateY: 0 }
|
|
200
|
+
: { opacity: 1, translateX: 0 };
|
|
201
|
+
// ...
|
|
202
|
+
<motion.div initial={initial} animate={animate} exit={{ opacity: 0 }}>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
// after — primitives, same keys in all three phases
|
|
207
|
+
const initial = isMobile
|
|
208
|
+
? { opacity: 0, x: 0, y: 200 }
|
|
209
|
+
: { opacity: 0, x: 200, y: 0 };
|
|
210
|
+
const animate = { opacity: 1, x: 0, y: 0 };
|
|
211
|
+
const exit = { opacity: 0, x: 0, y: 0 };
|
|
212
|
+
// ...
|
|
213
|
+
<motion.div initial={initial} animate={animate} exit={exit}>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### `initial` / `animate` / `exit` shape consistency in app code
|
|
217
|
+
|
|
218
|
+
Same fix applies to consumer-side `motion.div` usage. From `CybersourcePaymentMethod`:
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
// before — shouldAnimate=false branch omits x, but animate sets x:0; exit drops x too
|
|
222
|
+
<motion.div
|
|
223
|
+
key="newCardForm"
|
|
224
|
+
initial={shouldAnimate ? { opacity: 0, x: 32 } : { opacity: 1 }}
|
|
225
|
+
animate={{ opacity: 1, x: 0 }}
|
|
226
|
+
exit={{ opacity: 0 }}
|
|
227
|
+
/>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
// after — every key in animate is also in both initial branches and exit
|
|
232
|
+
<motion.div
|
|
233
|
+
key="newCardForm"
|
|
234
|
+
initial={shouldAnimate ? { opacity: 0, x: 32 } : { opacity: 1, x: 0 }}
|
|
235
|
+
animate={{ opacity: 1, x: 0 }}
|
|
236
|
+
exit={{ opacity: 0, x: 0 }}
|
|
237
|
+
/>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Consumer Impact
|
|
241
|
+
|
|
242
|
+
Five categories of code in consuming projects to audit:
|
|
243
|
+
|
|
244
|
+
| Pattern | Required change |
|
|
245
|
+
|---|---|
|
|
246
|
+
| Custom `animationVariants` on `Dialog` / `ModalOverlay` containing **any** non-opacity property (scale, x/y primitives, transform strings, pointer-events, etc.) | Strip to `{ opacity: 0/1 }` and move all other styling to static CSS classes. |
|
|
247
|
+
| Custom `animationVariants` on `Badge`, loaders, or other non-modal components using multi-function `transform` strings | Split into framer-motion primitives (`x`, `y`, `scale`, `rotate`). |
|
|
248
|
+
| Local `motion.div` / `motion.section` / etc. where one of `initial` / `animate` / `exit` includes a key the others don't | Add the missing key with a sensible base value (usually `0`) to the other branches. |
|
|
249
|
+
| `motion.*` variants animating `calc(...)` strings with mixed units, or other complex CSS-value strings | Resolve to numeric pixels in JS first; hand framer-motion a number. |
|
|
250
|
+
| `motion.*` variants animating `backdrop-filter`, `clip-path`, `filter`, gradients, `pointer-events`, or any WAAPI-incompatible property | Move the value to a static CSS class; only animate `opacity` (or a primitive) to fade them on/off. |
|
|
251
|
+
|
|
252
|
+
Search commands:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
# Multi-function transform strings in motion code
|
|
256
|
+
grep -rn "transform:.*translate\|transform:.*scale\|transform:.*rotate" apps/ libs/ \
|
|
257
|
+
| grep -v vendor | grep -v node_modules
|
|
258
|
+
|
|
259
|
+
# Possibly-mismatched initial/animate shapes — look for `initial=` lines that
|
|
260
|
+
# differ in length from neighboring animate/exit lines
|
|
261
|
+
grep -rnA2 "initial=" apps/ libs/ | grep -v vendor | grep -v node_modules
|
|
262
|
+
|
|
263
|
+
# Custom variants for @clicktap/ui Dialog/Modal — any non-opacity prop here is suspect
|
|
264
|
+
grep -rn "animationVariants" apps/ libs/ | grep -v vendor
|
|
265
|
+
|
|
266
|
+
# calc() strings inside motion code
|
|
267
|
+
grep -rn "calc(" apps/ libs/ | grep -v node_modules | grep -v vendor | grep -i 'motion\|controls\|animate\|translate'
|
|
268
|
+
|
|
269
|
+
# WAAPI-incompatible CSS properties in motion variants
|
|
270
|
+
grep -rn "backdropFilter\|backdrop-filter\|clipPath\|clip-path\|pointerEvents:\s*'" apps/ libs/ | grep -v vendor
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Pattern for Consumers
|
|
274
|
+
|
|
275
|
+
The five rules below cover every framer-motion 11.x crash pattern we found. Apply them to any `motion.*` element or component receiving `variants` / `animate`.
|
|
276
|
+
|
|
277
|
+
**Rule 1 — Modal entry/exit: opacity only.** For `Dialog`, `ModalOverlay`, `Drawer`, and any other modal-like component, the entry/exit animation should be a fade. Everything else (centering, slide direction, scale) lives in static CSS. The default `@clicktap/ui` variants now follow this.
|
|
278
|
+
|
|
279
|
+
**Rule 2 — Primitives over transform strings.** For non-modal `motion.*` elements, prefer framer-motion primitives over `transform: '...'` strings.
|
|
280
|
+
|
|
281
|
+
| Avoid | Use |
|
|
282
|
+
|---|---|
|
|
283
|
+
| `transform: 'translateX(-50%)'` | `x: '-50%'` |
|
|
284
|
+
| `transform: 'translateY(10px)'` | `y: 10` |
|
|
285
|
+
| `transform: 'scale(0.9)'` | `scale: 0.9` |
|
|
286
|
+
| `transform: 'rotate(45deg)'` | `rotate: 45` |
|
|
287
|
+
| `transform: 'translate(-50%, -50%) scale(0.8)'` | `x: '-50%', y: '-50%', scale: 0.8` |
|
|
288
|
+
| `translateY: '200px'` (alias) | `y: 200` |
|
|
289
|
+
|
|
290
|
+
**Rule 3 — Keyframe shapes must match.** If `animate` sets `{ opacity, x, scale }`, then `initial` and `exit` must each include all three keys. The framer-motion docs allow omission in theory; in practice the 11.x keyframe resolver stores nulls and crashes on remeasure.
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
// REFERENCE — same shape across initial/animate/exit
|
|
294
|
+
<motion.div
|
|
295
|
+
initial={{ opacity: 0, x: 32 }}
|
|
296
|
+
animate={{ opacity: 1, x: 0 }}
|
|
297
|
+
exit={{ opacity: 0, x: 0 }}
|
|
298
|
+
/>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
If you need a "no animation" branch, the skipped shape still has to include the same keys:
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
// REFERENCE — branched initial that still matches the target shape
|
|
305
|
+
<motion.div
|
|
306
|
+
initial={shouldAnimate ? { opacity: 0, x: 32 } : { opacity: 1, x: 0 }}
|
|
307
|
+
animate={{ opacity: 1, x: 0 }}
|
|
308
|
+
exit={{ opacity: 0, x: 0 }}
|
|
309
|
+
/>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Rule 4 — Resolve unit math in JS, not in motion values.** Don't hand framer-motion calc strings with mixed units. Compute pixels in the consumer and pass a number.
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
// AVOID
|
|
316
|
+
toastControls.start({ translateY: `calc(-100% + ${h}px + 8rem)` });
|
|
317
|
+
|
|
318
|
+
// USE
|
|
319
|
+
const remPx = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
320
|
+
const containerHeight = ref.current?.getBoundingClientRect().height ?? 0;
|
|
321
|
+
toastControls.start({ y: -containerHeight + h + 8 * remPx });
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Rule 5 — Un-interpolatable CSS goes on a static class.** `backdrop-filter`, complex `clip-path`, gradient `background`, `pointer-events`, etc. Don't put these in variants. Put them in a Tailwind class (or a `style` literal) and animate only `opacity` to fade them in/out. The `@clicktap/ui` `ModalOverlay` does this for `backdrop-blur-sm` — copy that pattern.
|
|
325
|
+
|
|
326
|
+
## Migration Steps
|
|
327
|
+
|
|
328
|
+
1. **Update `@clicktap/ui`** to a version that includes all of: Dialog opacity-only, ModalOverlay opacity-only, Badge primitive, Loader primitives (≥ this change).
|
|
329
|
+
|
|
330
|
+
2. **If your project overrides `animationVariants` on Dialog or ModalOverlay** with anything beyond opacity, simplify:
|
|
331
|
+
|
|
332
|
+
```tsx
|
|
333
|
+
// before — risky: x/y primitives still route through the complex parser
|
|
334
|
+
// under enough ancestor-AnimatePresence load
|
|
335
|
+
<Dialog
|
|
336
|
+
animationVariants={{
|
|
337
|
+
hidden: { x: '-50%', y: '-50%', scale: 0.9, opacity: 0 },
|
|
338
|
+
visible: { x: '-50%', y: '-50%', scale: 1, opacity: 1 },
|
|
339
|
+
}}
|
|
340
|
+
/>
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
// after — strip to opacity; centering moves to a wrapper className
|
|
345
|
+
<Dialog
|
|
346
|
+
animationVariants={{
|
|
347
|
+
hidden: { opacity: 0 },
|
|
348
|
+
visible: { opacity: 1 },
|
|
349
|
+
}}
|
|
350
|
+
className="-translate-x-1/2 -translate-y-1/2 ..."
|
|
351
|
+
/>
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
3. **Audit local `motion.*` usage in your app** for the five rules. For each `motion.div` (or `motion.section` etc.):
|
|
355
|
+
- Strip any non-opacity values out of the modal variants if it's a modal-shaped component.
|
|
356
|
+
- Replace `transform: '...'` strings with primitives.
|
|
357
|
+
- Match `initial` / `animate` / `exit` shapes — every key in `animate` must be in `initial` and `exit`.
|
|
358
|
+
- Resolve calc-with-units to pixel numbers in JS.
|
|
359
|
+
- Move backdrop-filter / pointer-events / clip-path / gradients to static CSS classes.
|
|
360
|
+
|
|
361
|
+
4. **Verify.**
|
|
362
|
+
- Run the dev server and open every modal flow your project supports (cart drawer, address book Add/Edit, checkout payment-method toggle, custom dialogs). No console errors referencing `mixObject` or `complex.mjs`.
|
|
363
|
+
- Open and close drawers/modals rapidly while a toast is mid-animation. No console errors.
|
|
364
|
+
- Visit a page that renders the cart-count Badge while opening a modal. The Badge animation should play smoothly with no console error.
|
|
365
|
+
- Run typecheck:
|
|
366
|
+
```bash
|
|
367
|
+
docker compose exec -u app node-app sh -c 'cd /app && npx nx run frontend:typecheck'
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Rollback
|
|
371
|
+
|
|
372
|
+
Reverting any of the framer-motion 11 changes is safe — they're behavioral fixes with no schema or data implications. Down-migration: re-introduce the transform strings or trimmed `initial` shapes in the affected files. Note that the crash returns once you do — the framer-motion 11.x parser hasn't changed.
|
|
373
|
+
|
|
374
|
+
## Files Changed
|
|
375
|
+
|
|
376
|
+
```
|
|
377
|
+
libs/ui/src/components/Dialog/Dialog.tsx
|
|
378
|
+
libs/ui/src/components/ModalOverlay/ModalOverlay.tsx
|
|
379
|
+
libs/ui/src/components/Badge/Badge.tsx
|
|
380
|
+
libs/ui/src/components/Loader/CircularEasing.tsx
|
|
381
|
+
libs/ui/src/components/Loader/Pulse.tsx
|
|
382
|
+
apps/frontend/components/Toast/Toast.tsx
|
|
383
|
+
apps/frontend/components/Toast/Notification.tsx
|
|
384
|
+
apps/frontend/components/Checkout/Step/Payment/Cybersource/CybersourcePaymentMethod.tsx
|
|
385
|
+
apps/frontend/components/Checkout/Step/Payment/Authorizenet/AuthorizenetPaymentMethod.tsx
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## References
|
|
389
|
+
|
|
390
|
+
- framer-motion `mixObject` source: `node_modules/framer-motion/dist/es/utils/mix/complex.mjs:48` — `if (a[key] !== undefined && b[key] !== undefined)` dereferences a key on `null` because `typeof null === 'object'` made `getMixer(null)` return `mixObject`.
|
|
391
|
+
- The framer-motion 11.x keyframe resolver is the upstream root cause; tracked in framer-motion's own issue tracker. 12.x rewrites this path. Adopt the rules above as long-term convention regardless of upgrade timing.
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# `ButtonLoadingContent` helper — standardized right-side spinner for all submit buttons
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-05-21
|
|
4
|
+
**Affects:** `@clicktap/ui/components/Button` (new `ButtonLoadingContent` export) and every submit-style button in the frontend app.
|
|
5
|
+
**Classification:** IMPROVEMENT
|
|
6
|
+
**Breaking:** No
|
|
7
|
+
|
|
8
|
+
## Dependencies
|
|
9
|
+
|
|
10
|
+
*None.*
|
|
11
|
+
|
|
12
|
+
## Summary
|
|
13
|
+
|
|
14
|
+
A new `ButtonLoadingContent` helper exported from `@clicktap/ui/components/Button` provides the canonical "label + right-side spinner" layout for any submit button. The spinner fades in via opacity (no layout shift), an invisible spacer on the left keeps the label visually centered in both states, and the label stays visible the whole time. This pattern is now used everywhere a submit button needs to show progress — Place Order, Continue (checkout step actions), Sign in / Sign up / Reset password, Save Address, Update / Remove / Add payment method, PDP Add to cart, etc.
|
|
15
|
+
|
|
16
|
+
Before this change, submit buttons across the app inconsistently mixed:
|
|
17
|
+
- text swaps (`isSubmitting ? 'Updating…' : 'Update'`),
|
|
18
|
+
- spinner-only (`isSubmitting ? <CircularEasing /> : 'Continue'`),
|
|
19
|
+
- inline copy of the Place-Order span/spacer/spinner pattern (`<span className="inline-flex ..."><span className="w-5" /> Place order <span className={isLoading ? 'opacity-100' : 'opacity-0'}>...`),
|
|
20
|
+
- nothing at all.
|
|
21
|
+
|
|
22
|
+
They now all use `ButtonLoadingContent`.
|
|
23
|
+
|
|
24
|
+
## Why
|
|
25
|
+
|
|
26
|
+
Two reasons converged:
|
|
27
|
+
|
|
28
|
+
1. **Inconsistent UX.** Some buttons swapped to "Updating…" text; others showed a spinner instead of the label; some did both; some did neither. There was no single canonical pattern, so visitors got different feedback styles depending on which surface they were on.
|
|
29
|
+
|
|
30
|
+
2. **A real bug behind the inconsistency.** The Continue-to-review button in checkout was set up to show a loader during step submission, but the Cybersource and Auth.Net `NewCardForm` event handlers were flipping `isSubmitting` back to `false` immediately after dispatching the form-submit event. The label stayed visible (or no loader appeared) during the entire save-card mutation; the loader only reappeared when the state machine reached `submittingStepData`. Standardizing on the helper also forced fixing that — the save-card path now keeps the spinner visible from click through save-card → step-submit.
|
|
31
|
+
|
|
32
|
+
## Changes
|
|
33
|
+
|
|
34
|
+
| File | Change |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `libs/ui/src/components/Button/ButtonLoadingContent.tsx` | **New.** The helper component (see API below). |
|
|
37
|
+
| `libs/ui/src/components/Button/index.ts` | Adds `ButtonLoadingContent` to the exports. |
|
|
38
|
+
| `apps/frontend/components/Checkout/Step/Payment/Cybersource/NewCardForm.tsx` | Removed the premature `setIsSubmitting(false)` inside the `paymentMethodSubmit` event handler — the spinner now stays visible through the entire save-card + step-submit flow. |
|
|
39
|
+
| `apps/frontend/components/Checkout/Step/Payment/Authorizenet/NewCardForm.tsx` | Same fix as Cybersource. |
|
|
40
|
+
| `apps/frontend/components/Checkout/Step/StepActions.tsx` | `Continue to review` button: replaced spinner-only-when-loading with `<ButtonLoadingContent>{nextLabel}</ButtonLoadingContent>`. |
|
|
41
|
+
| `apps/frontend/components/Checkout/Step/Review/CheckoutButton.tsx` | The three `Place order` buttons (Cybersource / Auth.Net / NO_PAYMENT_REQUIRED) replaced their hand-rolled label+spacer+spinner spans with `<ButtonLoadingContent>`. |
|
|
42
|
+
| `apps/frontend/components/Checkout/Step/Review/CheckoutButton/OfflineCheckoutButton.tsx` | Same. |
|
|
43
|
+
| `apps/frontend/components/Product/components/AddToCart/AddToCart.tsx` | PDP Add-to-cart now wraps its label in `ButtonLoadingContent` with `isLoading={isUpdatingItems}`. |
|
|
44
|
+
| `apps/frontend/components/Checkout/Step/Payment/Cybersource/SavedCardsForm/SavedCard/EditButton/UpdatePayment.tsx`, `.../Authorizenet/...UpdatePayment.tsx`, `.../Account/PaymentMethods/Cybersource/PaymentMethod/EditButton/UpdatePayment.tsx`, `.../Authorizenet/...UpdatePayment.tsx` | `isSubmitting ? 'Updating…' : 'Update'` → `<ButtonLoadingContent isLoading={isSubmitting}>Update</ButtonLoadingContent>`. |
|
|
45
|
+
| `apps/frontend/components/Checkout/Step/Payment/Cybersource/SavedCardsForm/SavedCard/DeleteButton.tsx`, `.../Authorizenet/.../DeleteButton.tsx` | `isRemoving ? 'Removing…' : 'Remove'` → `<ButtonLoadingContent isLoading={isRemoving}>Remove</ButtonLoadingContent>`. |
|
|
46
|
+
| `apps/frontend/components/Account/PaymentMethods/Cybersource/CreateNewPayment.tsx`, `.../Authorizenet/CreateNewPayment.tsx` | `isSubmitting ? 'Adding payment method' : 'Add payment method'` → `<ButtonLoadingContent>`. |
|
|
47
|
+
| `apps/frontend/components/Account/Address/Create.tsx` | `isSubmitting ? <>Saving Address <CircularEasing/></> : 'Save Address'` → `<ButtonLoadingContent isLoading={isSubmitting}>Save Address</ButtonLoadingContent>`. |
|
|
48
|
+
| `apps/frontend/components/Account/Address/Edit.tsx` | `isSubmitting ? 'Updating' : 'Update'` → `<ButtonLoadingContent>`. |
|
|
49
|
+
| `apps/frontend/components/Account/Auth/Authenticate/Signup.tsx` | `isSubmitting ? 'Signing up' : 'Sign up'` → `<ButtonLoadingContent>`. |
|
|
50
|
+
| `apps/frontend/components/Account/Auth/Authenticate/Signin.tsx` | `isSubmitting || isSubmitted ? 'Signing in' : 'Sign in'` → `<ButtonLoadingContent isLoading={isSubmitting || isSubmitted}>Sign in</ButtonLoadingContent>`. |
|
|
51
|
+
| `apps/frontend/components/Account/Auth/Authenticate/ResetPassword.tsx` | `isSubmitting ? 'Resetting password' : 'Reset password'` → `<ButtonLoadingContent>`. |
|
|
52
|
+
|
|
53
|
+
## API
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// @clicktap/ui/components/Button
|
|
57
|
+
export function ButtonLoadingContent({
|
|
58
|
+
isLoading?: boolean, // default false
|
|
59
|
+
spinnerStroke?: string, // default '#FFFFFF' (suits solid dark buttons)
|
|
60
|
+
children: ReactNode, // the button's label
|
|
61
|
+
}): JSX.Element;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Renders (semantically):
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
<span className="inline-flex items-center gap-2">
|
|
68
|
+
<span className="w-5" aria-hidden="true" /> {/* invisible spacer */}
|
|
69
|
+
{children} {/* the label */}
|
|
70
|
+
<span className={isLoading ? 'opacity-100' : 'opacity-0'}>
|
|
71
|
+
<CircularEasing stroke={spinnerStroke} width={20} />
|
|
72
|
+
</span>
|
|
73
|
+
</span>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The spacer matches the spinner's 20px width — the label stays visually centered whether the spinner is visible or not. The spinner fades via opacity, so the layout doesn't shift when `isLoading` flips.
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import {
|
|
82
|
+
Button,
|
|
83
|
+
ButtonLoadingContent,
|
|
84
|
+
} from '@clicktap/ui/components/Button';
|
|
85
|
+
|
|
86
|
+
<Button
|
|
87
|
+
type="submit"
|
|
88
|
+
isLoading={isSubmitting}
|
|
89
|
+
isDisabled={isSubmitting}
|
|
90
|
+
>
|
|
91
|
+
<ButtonLoadingContent isLoading={isSubmitting}>
|
|
92
|
+
Save Address
|
|
93
|
+
</ButtonLoadingContent>
|
|
94
|
+
</Button>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
For outline / ghost buttons (dark text, light background), pass `spinnerStroke`:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
<Button variant="outline">
|
|
101
|
+
<ButtonLoadingContent isLoading={isSubmitting} spinnerStroke="#0f172a">
|
|
102
|
+
Cancel
|
|
103
|
+
</ButtonLoadingContent>
|
|
104
|
+
</Button>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## The NewCardForm `setIsSubmitting(false)` bug
|
|
108
|
+
|
|
109
|
+
A separate but related fix in the same PR: the Cybersource and Auth.Net `NewCardForm` `paymentMethodSubmit` event handlers were doing this:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// before
|
|
113
|
+
const handleCustomEvent = (event: Event) => {
|
|
114
|
+
if (detail?.code === paymentMethodCardCode) {
|
|
115
|
+
formRef.current?.dispatchEvent(
|
|
116
|
+
new Event('submit', { cancelable: true, bubbles: true }),
|
|
117
|
+
);
|
|
118
|
+
setIsSubmitting(false); // ← bug: kills the loader before the async work even starts
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
After dispatching the form submit, the handler flipped `isSubmitting` to false synchronously. The form's `onSubmit` then began the async save-card mutation + step submission, but the Continue-to-review button no longer showed a loader. The loader only reappeared when the checkout state machine reached `submittingStepData` (after the save-card mutation completed).
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
// after
|
|
127
|
+
const handleCustomEvent = (event: Event) => {
|
|
128
|
+
if (detail?.code === paymentMethodCardCode) {
|
|
129
|
+
formRef.current?.dispatchEvent(
|
|
130
|
+
new Event('submit', { cancelable: true, bubbles: true }),
|
|
131
|
+
);
|
|
132
|
+
// Do NOT flip isSubmitting back to false here — the save-card
|
|
133
|
+
// mutation + step submission both run after this dispatch and
|
|
134
|
+
// we want the Continue button's spinner to stay visible the
|
|
135
|
+
// whole time. The error paths in onSubmit and the state
|
|
136
|
+
// machine's submittingStepData transition own the reset-to-false.
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Apply this same change to your local `NewCardForm` for both gateways if your project pulled the earlier version.
|
|
142
|
+
|
|
143
|
+
## Migration Steps
|
|
144
|
+
|
|
145
|
+
1. **Update `@clicktap/ui`** to the version that exports `ButtonLoadingContent`.
|
|
146
|
+
|
|
147
|
+
2. **Find your existing inconsistent submit-button patterns.** Quick search commands:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# text-swap pattern
|
|
151
|
+
grep -rnE "isSubmitting \\? ['\"]" apps/ libs/ | grep -v vendor | grep -v node_modules
|
|
152
|
+
grep -rnE "isRemoving \\? ['\"]" apps/ libs/ | grep -v vendor | grep -v node_modules
|
|
153
|
+
grep -rnE "isLoading \\? ['\"]" apps/ libs/ | grep -v vendor | grep -v node_modules
|
|
154
|
+
|
|
155
|
+
# spinner-only pattern in Buttons
|
|
156
|
+
grep -rnE "isSubmitting \\? <CircularEasing" apps/ libs/ | grep -v vendor
|
|
157
|
+
|
|
158
|
+
# hand-rolled Place-Order-style pattern
|
|
159
|
+
grep -rn "inline-flex items-center gap-2" apps/ | grep -B1 -A4 "CircularEasing"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
3. **Rewrite each to use `ButtonLoadingContent`.** Three common transforms:
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
// text-swap → helper
|
|
166
|
+
// before
|
|
167
|
+
<Button isLoading={isSubmitting}>
|
|
168
|
+
{isSubmitting ? 'Updating…' : 'Update'}
|
|
169
|
+
</Button>
|
|
170
|
+
// after
|
|
171
|
+
<Button isLoading={isSubmitting}>
|
|
172
|
+
<ButtonLoadingContent isLoading={isSubmitting}>
|
|
173
|
+
Update
|
|
174
|
+
</ButtonLoadingContent>
|
|
175
|
+
</Button>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
// spinner-only → helper (the label stays visible now)
|
|
180
|
+
// before
|
|
181
|
+
<Button>
|
|
182
|
+
{isSubmitting ? <CircularEasing stroke="#FFFFFF" width={24} /> : nextLabel}
|
|
183
|
+
</Button>
|
|
184
|
+
// after
|
|
185
|
+
<Button>
|
|
186
|
+
<ButtonLoadingContent isLoading={isSubmitting}>
|
|
187
|
+
{nextLabel}
|
|
188
|
+
</ButtonLoadingContent>
|
|
189
|
+
</Button>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
// hand-rolled span pattern → helper (DRY)
|
|
194
|
+
// before
|
|
195
|
+
<Button>
|
|
196
|
+
<span className="inline-flex items-center gap-2">
|
|
197
|
+
<span className="w-5" aria-hidden="true" />
|
|
198
|
+
Place order
|
|
199
|
+
<span className={isSubmitting ? 'opacity-100' : 'opacity-0'}>
|
|
200
|
+
<CircularEasing stroke="#FFFFFF" width={20} />
|
|
201
|
+
</span>
|
|
202
|
+
</span>
|
|
203
|
+
</Button>
|
|
204
|
+
// after
|
|
205
|
+
<Button>
|
|
206
|
+
<ButtonLoadingContent isLoading={isSubmitting}>
|
|
207
|
+
Place order
|
|
208
|
+
</ButtonLoadingContent>
|
|
209
|
+
</Button>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
4. **If your checkout uses the Cybersource or Auth.Net `NewCardForm` pattern**, remove the premature `setIsSubmitting(false)` in the `paymentMethodSubmit` event handler (see "The NewCardForm `setIsSubmitting(false)` bug" above). The save-card mutation + step-submit path own the lifecycle now.
|
|
213
|
+
|
|
214
|
+
5. **Verify.**
|
|
215
|
+
- Click Place order on the review step: spinner appears on the right of "Place order", label stays put, no layout shift.
|
|
216
|
+
- Click Continue to review on the checkout payment step with an unsaved card: spinner appears immediately and stays visible through both the save-card mutation and the step submission, with no flash of label-only state in between.
|
|
217
|
+
- Click Update / Remove on any saved-card modal: spinner on the right, label stays.
|
|
218
|
+
- Sign in / Sign up / Reset password: same pattern, no "Signing in…" text variants.
|
|
219
|
+
|
|
220
|
+
## Pattern for Consumers
|
|
221
|
+
|
|
222
|
+
The right-side spinner with persistent label is now the canonical submit-feedback pattern across the whole frontend. When adding a new submit button:
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
<Button
|
|
226
|
+
type="submit"
|
|
227
|
+
isLoading={isSubmitting}
|
|
228
|
+
isDisabled={isSubmitting}
|
|
229
|
+
>
|
|
230
|
+
<ButtonLoadingContent isLoading={isSubmitting}>
|
|
231
|
+
{/* the static label */}
|
|
232
|
+
</ButtonLoadingContent>
|
|
233
|
+
</Button>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Don't reach for text-swap variants (`'Saving...'`, `'Updating...'`, `'Signing in'`) — they look inconsistent with the rest of the site and force the visitor to re-read the button. Don't reach for spinner-only-when-loading — same problem in reverse.
|
|
237
|
+
|
|
238
|
+
For non-submit buttons that just need a spinner overlay (e.g., a loading state on a non-action element), `CircularEasing` is still exported directly and is the right primitive.
|
|
239
|
+
|
|
240
|
+
## Files Changed
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
libs/ui/src/components/Button/ButtonLoadingContent.tsx (new)
|
|
244
|
+
libs/ui/src/components/Button/index.ts
|
|
245
|
+
|
|
246
|
+
apps/frontend/components/Checkout/Step/Payment/Cybersource/NewCardForm.tsx
|
|
247
|
+
apps/frontend/components/Checkout/Step/Payment/Authorizenet/NewCardForm.tsx
|
|
248
|
+
apps/frontend/components/Checkout/Step/StepActions.tsx
|
|
249
|
+
apps/frontend/components/Checkout/Step/Review/CheckoutButton.tsx
|
|
250
|
+
apps/frontend/components/Checkout/Step/Review/CheckoutButton/OfflineCheckoutButton.tsx
|
|
251
|
+
apps/frontend/components/Product/components/AddToCart/AddToCart.tsx
|
|
252
|
+
apps/frontend/components/Checkout/Step/Payment/Cybersource/SavedCardsForm/SavedCard/DeleteButton.tsx
|
|
253
|
+
apps/frontend/components/Checkout/Step/Payment/Cybersource/SavedCardsForm/SavedCard/EditButton/UpdatePayment.tsx
|
|
254
|
+
apps/frontend/components/Checkout/Step/Payment/Authorizenet/SavedCardsForm/SavedCard/DeleteButton.tsx
|
|
255
|
+
apps/frontend/components/Checkout/Step/Payment/Authorizenet/SavedCardsForm/SavedCard/EditButton/UpdatePayment.tsx
|
|
256
|
+
apps/frontend/components/Account/PaymentMethods/Cybersource/PaymentMethod/EditButton/UpdatePayment.tsx
|
|
257
|
+
apps/frontend/components/Account/PaymentMethods/Authorizenet/PaymentMethod/EditButton/UpdatePayment.tsx
|
|
258
|
+
apps/frontend/components/Account/PaymentMethods/Cybersource/CreateNewPayment.tsx
|
|
259
|
+
apps/frontend/components/Account/PaymentMethods/Authorizenet/CreateNewPayment.tsx
|
|
260
|
+
apps/frontend/components/Account/Address/Create.tsx
|
|
261
|
+
apps/frontend/components/Account/Address/Edit.tsx
|
|
262
|
+
apps/frontend/components/Account/Auth/Authenticate/Signup.tsx
|
|
263
|
+
apps/frontend/components/Account/Auth/Authenticate/Signin.tsx
|
|
264
|
+
apps/frontend/components/Account/Auth/Authenticate/ResetPassword.tsx
|
|
265
|
+
```
|