@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.
Files changed (40) hide show
  1. package/components/Accordion/Accordion.d.ts +1 -1
  2. package/components/Accordion/Accordion.js +1 -1
  3. package/components/Accordion/index.d.ts +2 -2
  4. package/components/Accordion/index.js +1 -1
  5. package/components/Badge/Badge.js +1 -1
  6. package/components/Badge/Badge.types.d.ts +1 -1
  7. package/components/Badge/index.js +1 -1
  8. package/components/Collection/CollectionFilters/CollectionFilters.js +1 -1
  9. package/components/Collection/CollectionFilters/index.js +1 -1
  10. package/components/Collection/index.js +1 -1
  11. package/components/Pagination/Pagination.d.ts +1 -1
  12. package/components/Pagination/Pagination.js +1 -1
  13. package/components/Pagination/index.js +1 -1
  14. package/components/Tooltip/Tooltip.d.ts +1 -1
  15. package/components/Tooltip/Tooltip.js +1 -1
  16. package/components/Tooltip/Tooltip.types.d.ts +1 -1
  17. package/components/Tooltip/index.js +1 -1
  18. package/docs/migrations/2026/01/2026-01-14-external-link-detection.md +290 -0
  19. package/docs/migrations/2026/01/2026-01-14-use-route-hook.md +142 -0
  20. package/docs/migrations/2026/01/2026-01-15-input-disable-skeleton.md +82 -0
  21. package/docs/migrations/2026/02/2026-02-16-env-utils-from-clicktap-ui.md +94 -0
  22. package/docs/migrations/2026/02/2026-02-17-select-popover-portal-container-restored.md +84 -0
  23. package/docs/migrations/2026/03/2026-03-24-image-dimmed-hidden-onready.md +108 -0
  24. package/docs/migrations/2026/03/2026-03-24-modal-overlay-backdrop-dismiss.md +81 -0
  25. package/docs/migrations/2026/04/2026-04-17-select-address-input-onchange-key-or-item.md +182 -0
  26. package/docs/migrations/2026/04/2026-04-24-image-placeholder-frontend-fallback.md +85 -0
  27. package/docs/migrations/2026/04/2026-04-30-prune-invalid-pdp-selections.md +166 -0
  28. package/docs/migrations/2026/04/2026-04-30-top-down-pdp-option-dependency.md +286 -0
  29. package/docs/migrations/2026/05/2026-05-01-build-pdp-url-util.md +141 -0
  30. package/docs/migrations/2026/05/2026-05-03-auth-aware-graphql-client.md +278 -0
  31. package/docs/migrations/2026/05/2026-05-20-framer-motion-11-idioms.md +391 -0
  32. package/docs/migrations/2026/05/2026-05-21-button-loading-content.md +265 -0
  33. package/docs/migrations/2026/05/2026-05-21-password-check-hide-on-empty.md +50 -0
  34. package/docs/migrations/2026/05/2026-05-29-password-check-requirements-variant.md +63 -0
  35. package/docs/migrations/2026/06/2026-06-17-select-standardization.md +286 -0
  36. package/docs/migrations/2026/06/2026-06-18-select-hiddenselect-setvalue-crash.md +75 -0
  37. package/docs/migrations/2026/07/2026-07-31-ui-exports-bundler-resolution.md +50 -0
  38. package/docs/migrations/2026/08/2026-08-02-nextui-to-heroui.md +65 -0
  39. package/docs/migrations/README.md +31 -0
  40. 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.