@clicktap/ui 0.40.8 → 0.40.10
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/components/Accordion/Accordion.d.ts +1 -1
- package/components/Accordion/Accordion.js +1 -1
- package/components/Accordion/index.d.ts +2 -2
- package/components/Accordion/index.js +1 -1
- package/components/Badge/Badge.js +1 -1
- package/components/Badge/Badge.types.d.ts +1 -1
- package/components/Badge/index.js +1 -1
- package/components/Collection/CollectionFilters/CollectionFilters.js +1 -1
- package/components/Collection/CollectionFilters/index.js +1 -1
- package/components/Collection/index.js +1 -1
- package/components/Pagination/Pagination.d.ts +1 -1
- package/components/Pagination/Pagination.js +1 -1
- package/components/Pagination/index.js +1 -1
- package/components/Tooltip/Tooltip.d.ts +1 -1
- package/components/Tooltip/Tooltip.js +1 -1
- package/components/Tooltip/Tooltip.types.d.ts +1 -1
- package/components/Tooltip/index.js +1 -1
- 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 +141 -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/2026/08/2026-08-02-nextui-to-heroui.md +65 -0
- package/docs/migrations/README.md +31 -0
- package/package.json +7 -5
|
@@ -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
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# `PasswordCheck` `requirements` variant hides until the visitor types
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-05-21
|
|
4
|
+
**Affects:** `libs/ui/src/components/PasswordCheck/PasswordCheck.tsx`
|
|
5
|
+
**Classification:** IMPROVEMENT
|
|
6
|
+
**Breaking:** No
|
|
7
|
+
|
|
8
|
+
## Dependencies
|
|
9
|
+
|
|
10
|
+
*None.*
|
|
11
|
+
|
|
12
|
+
## Summary
|
|
13
|
+
|
|
14
|
+
The `requirements` variant of `PasswordCheck` previously rendered all four unchecked bullet-points and a 0% strength bar as soon as the password field was mounted. Before the visitor had typed a single character, this read as a list of demands rather than as live feedback — pre-emptive shouting. Now `PasswordCheck` returns `null` from its `requirements` branch when `value` is empty, and reveals the rules + strength bar on the first keystroke.
|
|
15
|
+
|
|
16
|
+
This matches the spirit of the existing pattern: feedback that reflects what the visitor is doing right now, not feedback that runs ahead of them.
|
|
17
|
+
|
|
18
|
+
## Changes
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
// PasswordCheck.tsx — requirements branch
|
|
22
|
+
if (variant === 'requirements') {
|
|
23
|
+
if (!value) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="mt-4 flex flex-col gap-2">
|
|
29
|
+
{/* requirements list + strength bar */}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The `default` variant is unchanged.
|
|
36
|
+
|
|
37
|
+
## Migration Steps
|
|
38
|
+
|
|
39
|
+
1. **Pull the latest `@clicktap/ui`** (or merge the equivalent change in your local copy). No public API changed; the only difference is render output for the `requirements` variant.
|
|
40
|
+
|
|
41
|
+
2. **Verify.**
|
|
42
|
+
- On the signup form (`/account/authenticate` or in checkout), open the page with the password field unfocused. The previous list of four unchecked requirements should no longer appear; the area below the password input should be empty.
|
|
43
|
+
- Focus the password input. Type one character. The requirements list and strength bar should reveal, with the rule that's now satisfied (or the first-character rule, if you have one) ticking checked as appropriate.
|
|
44
|
+
- Clear the field — the requirements should disappear again.
|
|
45
|
+
|
|
46
|
+
## Files Changed
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
libs/ui/src/components/PasswordCheck/PasswordCheck.tsx
|
|
50
|
+
```
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# `PasswordCheck` — the `requirements` variant (adoption guide for forks)
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-05-29 (retroactive — the variant already ships in `@clicktap/ui`)
|
|
4
|
+
**Affects:** `libs/ui/src/components/PasswordCheck/PasswordCheck.tsx`, `libs/ui/src/components/PasswordCheck/PasswordCheck.types.ts`
|
|
5
|
+
**Classification:** IMPROVEMENT
|
|
6
|
+
**Breaking:** No (the variant is additive; `default` is unchanged)
|
|
7
|
+
|
|
8
|
+
## Dependencies
|
|
9
|
+
|
|
10
|
+
- [`PasswordCheck` `requirements` variant hides until the visitor types](./2026-05-21-password-check-hide-on-empty.md) — the hide-on-empty refinement to this same variant.
|
|
11
|
+
|
|
12
|
+
## Summary
|
|
13
|
+
|
|
14
|
+
This guide documents the `requirements` variant of `PasswordCheck` so forked UI libraries (brand-specific `*-ui` packages) have a canonical surface to audit against. A forked library that branched before this variant landed only inherits what's written up — without a guide it stays on the meter-only `default` output and never picks up the per-rule checklist.
|
|
15
|
+
|
|
16
|
+
`PasswordCheck` renders a live ✓/○ checklist of password-policy rules plus a 5-bar strength meter when `variant="requirements"`. Reach for it on any complexity-enforcing form (signup, password reset, change password) — it surfaces each rule as the visitor types instead of waiting for zod's first-failing-rule message after blur.
|
|
17
|
+
|
|
18
|
+
## Props
|
|
19
|
+
|
|
20
|
+
| Prop | Type | Default | Notes |
|
|
21
|
+
|------|------|---------|-------|
|
|
22
|
+
| `value` | `string` | — | The current password value. |
|
|
23
|
+
| `variant` | `'default' \| 'short' \| 'requirements'` | `'default'` | `requirements` renders the checklist + meter. `default` renders the meter + "Password Strength" text. |
|
|
24
|
+
| `requirements` | `PasswordRequirement[]` | 8 chars / uppercase / digit / special | Override the rule list. Each rule is `{ label: string; test: (value: string) => boolean }`. |
|
|
25
|
+
|
|
26
|
+
## Behavior
|
|
27
|
+
|
|
28
|
+
- **Hides when `value` is empty** — showing four unchecked bullets before any input reads as a list of demands rather than live feedback. (See the [hide-on-empty guide](./2026-05-21-password-check-hide-on-empty.md).)
|
|
29
|
+
- Each rule transitions from `○` (slate) to `✓` (green) as it's met.
|
|
30
|
+
- The 5-bar strength meter colors match the `default` variant's meter (red → yellow → green → blue by strength).
|
|
31
|
+
- The component is stateless and pure — strength is derived from `value` via `checkStrength`.
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
<PasswordCheck value={password} variant="requirements" />
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
With a custom rule list:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
<PasswordCheck
|
|
43
|
+
value={password}
|
|
44
|
+
variant="requirements"
|
|
45
|
+
requirements={[
|
|
46
|
+
{ label: 'At least 12 characters', test: (v) => v.length >= 12 },
|
|
47
|
+
{ label: 'No common words', test: (v) => !COMMON.has(v.toLowerCase()) },
|
|
48
|
+
]}
|
|
49
|
+
/>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Forks — what to copy
|
|
53
|
+
|
|
54
|
+
If you forked `PasswordCheck` into a brand-specific UI library before this variant existed:
|
|
55
|
+
|
|
56
|
+
1. Copy the `variant === 'requirements'` branch and the `defaultRequirements` list verbatim, then re-skin colors to your palette (e.g. `brand-secondary-*` / `brand-error-*` / `brand-grayscale-*` in place of the raw Tailwind palette).
|
|
57
|
+
2. Add `'requirements'` to your `PasswordCheckProps['variant']` union and the `PasswordRequirement` type.
|
|
58
|
+
3. Switch your complexity-enforcing forms (`Signup`, `ResetPassword`, `Account/.../UpdatePassword`) to `variant="requirements"`.
|
|
59
|
+
4. **While you're in there — check for a stray `y: -10` translate.** Some forks carry a `motion.div initial={{ y: -10 }} animate={{ y: -10 }}` on the legacy text that leaves the strength text permanently shifted off the baseline. `@clicktap/ui` does **not** have this — its `default` variant positions the strength text with `translate-y-2/4` (intentional layout), not a framer-motion translate. If your fork has the `y: -10`, drop it.
|
|
60
|
+
|
|
61
|
+
## Why retroactive
|
|
62
|
+
|
|
63
|
+
A new variant on a shared component can only propagate to forks that know it exists. **Broader pattern:** when adding a variant or non-trivial prop to a `libs/ui` component, write a short migration guide noting (a) what's new, (b) when to reach for it, (c) what to copy if a downstream library forked. The cost is minutes; it closes the silent-drift gap.
|