@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.
Files changed (22) hide show
  1. package/docs/migrations/2026/01/2026-01-14-external-link-detection.md +290 -0
  2. package/docs/migrations/2026/01/2026-01-14-use-route-hook.md +142 -0
  3. package/docs/migrations/2026/01/2026-01-15-input-disable-skeleton.md +82 -0
  4. package/docs/migrations/2026/02/2026-02-16-env-utils-from-clicktap-ui.md +94 -0
  5. package/docs/migrations/2026/02/2026-02-17-select-popover-portal-container-restored.md +84 -0
  6. package/docs/migrations/2026/03/2026-03-24-image-dimmed-hidden-onready.md +108 -0
  7. package/docs/migrations/2026/03/2026-03-24-modal-overlay-backdrop-dismiss.md +81 -0
  8. package/docs/migrations/2026/04/2026-04-17-select-address-input-onchange-key-or-item.md +182 -0
  9. package/docs/migrations/2026/04/2026-04-24-image-placeholder-frontend-fallback.md +85 -0
  10. package/docs/migrations/2026/04/2026-04-30-prune-invalid-pdp-selections.md +166 -0
  11. package/docs/migrations/2026/04/2026-04-30-top-down-pdp-option-dependency.md +286 -0
  12. package/docs/migrations/2026/05/2026-05-01-build-pdp-url-util.md +128 -0
  13. package/docs/migrations/2026/05/2026-05-03-auth-aware-graphql-client.md +278 -0
  14. package/docs/migrations/2026/05/2026-05-20-framer-motion-11-idioms.md +391 -0
  15. package/docs/migrations/2026/05/2026-05-21-button-loading-content.md +265 -0
  16. package/docs/migrations/2026/05/2026-05-21-password-check-hide-on-empty.md +50 -0
  17. package/docs/migrations/2026/05/2026-05-29-password-check-requirements-variant.md +63 -0
  18. package/docs/migrations/2026/06/2026-06-17-select-standardization.md +286 -0
  19. package/docs/migrations/2026/06/2026-06-18-select-hiddenselect-setvalue-crash.md +75 -0
  20. package/docs/migrations/2026/07/2026-07-31-ui-exports-bundler-resolution.md +50 -0
  21. package/docs/migrations/README.md +31 -0
  22. package/package.json +1 -1
@@ -0,0 +1,290 @@
1
+ # External Link Detection Migration
2
+
3
+ **Date:** 2026-01-14
4
+ **Affects:** `@clicktap/ui`, `apps/frontend`
5
+ **Breaking:** No (additive, but requires provider setup for SSR)
6
+ **Classification:** IMPROVEMENT
7
+
8
+
9
+ ## Dependencies
10
+
11
+ *None*
12
+
13
+ ## Summary
14
+
15
+ The `Link` component from `@clicktap/ui` now automatically detects external URLs and applies `target="_blank"` and `rel="noopener noreferrer"` attributes. This requires setting up `LinkProvider` for proper SSR support.
16
+
17
+ ## Changes
18
+
19
+ 1. `Link` component auto-detects external URLs
20
+ 2. New `LinkProvider` for SSR base URL configuration
21
+ 3. New `isExternalUrl` utility function
22
+ 4. Layout files must pass `baseUrl` from host header
23
+
24
+ ## Setup
25
+
26
+ ### 1. Update Layouts to Pass baseUrl
27
+
28
+ In your Next.js layout files (Server Components), extract the host header and pass it to Providers:
29
+
30
+ ```tsx
31
+ // app/(base)/layout.tsx
32
+ import { headers } from 'next/headers';
33
+
34
+ export default function Layout({ children }) {
35
+ const h = headers();
36
+ const baseUrl = `https://${h.get('host')}`;
37
+
38
+ return (
39
+ <Providers baseUrl={baseUrl}>
40
+ {children}
41
+ </Providers>
42
+ );
43
+ }
44
+ ```
45
+
46
+ ```tsx
47
+ // app/(checkout)/layout.tsx
48
+ import { headers } from 'next/headers';
49
+
50
+ export default function CheckoutLayout({ children }) {
51
+ const h = headers();
52
+ const baseUrl = `https://${h.get('host')}`;
53
+
54
+ return (
55
+ <Providers baseUrl={baseUrl}>
56
+ {children}
57
+ </Providers>
58
+ );
59
+ }
60
+ ```
61
+
62
+ ### 2. Update Providers Component
63
+
64
+ Add the `baseUrl` prop and wrap with `LinkProvider`:
65
+
66
+ ```tsx
67
+ // providers/Providers.tsx
68
+ import { LinkProvider } from '@clicktap/ui/components/Link';
69
+
70
+ type Props = {
71
+ baseUrl?: string;
72
+ children: ReactNode;
73
+ // ... other props
74
+ };
75
+
76
+ export function Providers({ baseUrl, children, ...props }: Props) {
77
+ return (
78
+ <LinkProvider config={{ baseUrl }}>
79
+ {/* other providers */}
80
+ {children}
81
+ </LinkProvider>
82
+ );
83
+ }
84
+ ```
85
+
86
+ ## How It Works
87
+
88
+ ### Detection Logic
89
+
90
+ - **Relative URLs** (`/path`, `#anchor`) → Internal (no attributes added)
91
+ - **Absolute URLs** → Compared against current host
92
+ - Same host → Internal
93
+ - Different host → External (adds `target="_blank"` and `rel="noopener noreferrer"`)
94
+
95
+ ### Host Detection
96
+
97
+ - **Client-side:** Uses `window.location.host` automatically
98
+ - **Server-side (SSR):** Uses `baseUrl` from `LinkProvider`
99
+
100
+ This ensures external links are correctly identified during both SSR and client-side rendering.
101
+
102
+ ## Usage
103
+
104
+ No changes needed in component code. The `Link` component handles detection automatically:
105
+
106
+ ```tsx
107
+ import { Link } from '@clicktap/ui/components/Link';
108
+
109
+ // Internal - renders normally
110
+ <Link href="/products">Products</Link>
111
+
112
+ // External - automatically gets target="_blank" rel="noopener noreferrer"
113
+ <Link href="https://google.com">Google</Link>
114
+
115
+ // Anchor - renders normally
116
+ <Link href="#section">Jump to section</Link>
117
+ ```
118
+
119
+ ### Override Behavior
120
+
121
+ You can override the automatic behavior if needed:
122
+
123
+ ```tsx
124
+ // Force internal link behavior on external URL
125
+ <Link href="https://subdomain.mysite.com" target="_self">
126
+ Subdomain
127
+ </Link>
128
+
129
+ // Force external link behavior on internal URL
130
+ <Link href="/download" target="_blank" rel="noopener noreferrer">
131
+ Download
132
+ </Link>
133
+ ```
134
+
135
+ ## Utility Function
136
+
137
+ The `isExternalUrl` utility is exported for use outside the Link component:
138
+
139
+ ```tsx
140
+ import { isExternalUrl } from '@clicktap/ui/components/Link';
141
+
142
+ const external = isExternalUrl('https://google.com', 'mysite.com');
143
+ // true
144
+
145
+ const internal = isExternalUrl('/products', 'mysite.com');
146
+ // false
147
+ ```
148
+
149
+ ## Files Changed
150
+
151
+ - `libs/ui/src/components/Link/Link.tsx` - Auto-detection logic
152
+ - `libs/ui/src/components/Link/LinkContext.tsx` - New provider
153
+ - `libs/ui/src/components/Link/utils.ts` - `isExternalUrl` utility
154
+ - `libs/ui/src/components/Link/index.ts` - Updated exports
155
+ - `apps/frontend/app/(base)/layout.tsx` - Pass baseUrl
156
+ - `apps/frontend/app/(checkout)/layout.tsx` - Pass baseUrl
157
+ - `apps/frontend/providers/Providers.tsx` - Add LinkProvider
158
+
159
+ ## Migration Checklist
160
+
161
+ 1. [ ] Update `libs/ui` to latest version
162
+ 2. [ ] Add `baseUrl` prop to Providers component
163
+ 3. [ ] Wrap app with `LinkProvider`
164
+ 4. [ ] Update all layout files to pass `baseUrl` from host header
165
+ 5. [ ] Test external links open in new tab
166
+ 6. [ ] Test internal links work normally
167
+ 7. [ ] Verify SSR renders correct attributes
168
+
169
+ ---
170
+
171
+ # LinkAsButton → @clicktap/ui
172
+
173
+ **Date:** 2026-02-06
174
+
175
+ ## Summary
176
+
177
+ `LinkAsButton` has been promoted from `apps/frontend/components/LinkAsButton/` into the shared UI library. `Button` also gains `primary`/`secondary`/`tertiary` variant aliases.
178
+
179
+ ## Changes
180
+
181
+ ### `LinkAsButton` promoted to shared UI library
182
+
183
+ - **Old import:** `import { LinkAsButton } from '../LinkAsButton/LinkAsButton'` (relative, app-level)
184
+ - **New import:** `import { LinkAsButton } from '@clicktap/ui/components/LinkAsButton'`
185
+ - The `[k: string]: any` catch-all type is removed — proper typing via `LinkProps` + `Pick<ButtonProps, 'variant' | 'size'>`
186
+ - Now supports all Button variants (`solid`, `outline`, `ghost`) plus aliases (`primary`, `secondary`, `tertiary`), `size` prop (`sm`, `md`, `lg`), and `isLoading`
187
+ - `apps/frontend/components/LinkAsButton/` has been deleted
188
+
189
+ ### Breaking: `LinkAsButton` default behavior changed
190
+
191
+ The old local component had no default variant — it rendered as a transparent link with base layout classes. The new shared component defaults to `variant="solid"` and `size="md"`:
192
+
193
+ | Property | Old (local) | New (`@clicktap/ui`) |
194
+ |----------|-------------|----------------------|
195
+ | `variant` | none (transparent background, transparent border) | `solid` (dark background, white text) |
196
+ | `size` | none (no height constraint) | `md` (`h-10` = 40px height) |
197
+
198
+ **Impact:** Any `<LinkAsButton>` without an explicit `variant` prop now renders as a dark solid button with a 40px height constraint. This is especially problematic when wrapping images — the image gets constrained inside a styled button container.
199
+
200
+ **Fix:** Use `variant="ghost"` for transparent links, or use `<Link>` directly when button styling isn't appropriate (e.g., clickable product images):
201
+
202
+ ```tsx
203
+ // Before: image wrapped in default LinkAsButton (was transparent, now solid)
204
+ <LinkAsButton href={url}>
205
+ <Image ... />
206
+ </LinkAsButton>
207
+
208
+ // After: use plain Link for image wrappers
209
+ <Link href={url}>
210
+ <Image ... />
211
+ </Link>
212
+
213
+ // After: use ghost variant if button semantics are needed
214
+ <LinkAsButton variant="ghost" href={url}>
215
+ Text link
216
+ </LinkAsButton>
217
+ ```
218
+
219
+ ### Button variant aliases
220
+
221
+ - `Button` now accepts `primary`, `secondary`, and `tertiary` as variant values
222
+ - `primary` → `solid`, `secondary` → `outline`, `tertiary` → `ghost`
223
+ - Existing `solid`, `outline`, `ghost` values continue to work unchanged
224
+
225
+ ---
226
+
227
+ # Image Component: Remove `width: 'auto'` Auto-Styling
228
+
229
+ **Date:** 2026-02-08
230
+ **Affects:** `@clicktap/ui` Image component, all consumers
231
+ **Breaking:** Yes - images using `width`/`height` props may render differently
232
+
233
+ ## Summary
234
+
235
+ The `@clicktap/ui` Image component previously auto-prepended `{ width: 'auto', height: 'auto' }` to the style prop when `width` and `height` props were provided (not `fill`). The `width: 'auto'` has been removed because it overrides the HTML `width` attribute via CSS, causing images to render at their natural intrinsic size instead of the dimensions specified by the props.
236
+
237
+ ## What Changed
238
+
239
+ ```diff
240
+ const imageStyle = hasSizedProps
241
+ - ? { width: 'auto' as const, height: 'auto' as const, ...style }
242
+ + ? { height: 'auto' as const, ...style }
243
+ : style;
244
+ ```
245
+
246
+ **Before:** Images with `width={100} height={100}` and no explicit `style` rendered at their natural resolution (CSS `width: auto` overrode the HTML attribute).
247
+
248
+ **After:** Images render at the width specified by the `width` prop. `height: 'auto'` preserves the natural aspect ratio.
249
+
250
+ ## Migration
251
+
252
+ ### Review Image usages without explicit `style` prop
253
+
254
+ Any `<Image>` that passes `width` and `height` props without a `style` override will now render at the `width` prop size instead of the natural image size. In most cases this is the correct behavior.
255
+
256
+ If the natural-size behavior was intentional, restore it with an explicit style:
257
+
258
+ ```tsx
259
+ <Image width={100} height={100} style={{ width: 'auto', height: 'auto' }} ... />
260
+ ```
261
+
262
+ ### Add aspect ratio control where needed
263
+
264
+ If the displayed image needs a specific aspect ratio that differs from the source image's natural ratio, add CSS classes. Use the actual source image dimensions to determine the ratio (e.g., 960x1200 images → `aspect-[4/5]`):
265
+
266
+ ```tsx
267
+ // Explicit aspect ratio matching source images (e.g., 960x1200 = 4:5)
268
+ <Image
269
+ width={100} height={125}
270
+ className="aspect-[4/5] object-cover"
271
+ style={{ width: '100%', height: 'auto' }}
272
+ />
273
+
274
+ // Fill container width, natural aspect ratio (common pattern, no extra classes needed)
275
+ <Image
276
+ width={400} height={300}
277
+ style={{ width: '100%', height: 'auto' }}
278
+ />
279
+ ```
280
+
281
+ ### Affected areas to check
282
+
283
+ Visually inspect any page that displays product images:
284
+ - Cart drawer (mini-cart)
285
+ - Cart page
286
+ - Checkout order summary
287
+ - Checkout success / order confirmation
288
+ - Category listing pages
289
+ - Product collection components
290
+ - Account order history
@@ -0,0 +1,142 @@
1
+ # useRoute Hook Migration
2
+
3
+ **Date:** 2026-01-14
4
+ **Affects:** `@clicktap/ui`, `apps/frontend`
5
+ **Breaking:** No (additive, but recommended migration)
6
+ **Classification:** IMPROVEMENT
7
+
8
+
9
+ ## Dependencies
10
+
11
+ *None*
12
+
13
+ ## Summary
14
+
15
+ Introduces a centralized route configuration system with the `useRoute` hook for generating URLs. Handles dynamic params, protected route redirects, and login-with-return patterns automatically.
16
+
17
+ ## New Files
18
+
19
+ - `libs/ui/src/hooks/useRoute/` - Hook and RouteProvider
20
+ - `apps/frontend/config/routes.ts` - Centralized route definitions
21
+
22
+ ## Provider Setup
23
+
24
+ Add `RouteProviderWithAuth` inside your `AuthProvider`:
25
+
26
+ ```tsx
27
+ import { RouteProvider } from '@clicktap/ui/hooks/useRoute';
28
+ import { useAuth } from '../state/auth/AuthProvider';
29
+
30
+ function RouteProviderWithAuth({ children }: { children: ReactNode }) {
31
+ const { isLoggedIn } = useAuth();
32
+ return <RouteProvider config={{ isLoggedIn }}>{children}</RouteProvider>;
33
+ }
34
+
35
+ // In provider tree:
36
+ <AuthProvider actor={authActor}>
37
+ <RouteProviderWithAuth>
38
+ {children}
39
+ </RouteProviderWithAuth>
40
+ </AuthProvider>
41
+ ```
42
+
43
+ ## Migration Patterns
44
+
45
+ ### Static Routes
46
+
47
+ ```tsx
48
+ // Before
49
+ <Link href="/cart">Cart</Link>
50
+
51
+ // After
52
+ import { useRoute } from '@clicktap/ui/hooks/useRoute';
53
+ import { Routes } from '../config/routes';
54
+
55
+ const cartUrl = useRoute(Routes.cart.index);
56
+ <Link href={cartUrl}>Cart</Link>
57
+ ```
58
+
59
+ ### Dynamic Routes
60
+
61
+ ```tsx
62
+ // Before
63
+ <Link href={`/account/orders/${orderId}`}>View Order</Link>
64
+
65
+ // After
66
+ const orderUrl = useRoute(Routes.account.orders.view, { orderId });
67
+ <Link href={orderUrl}>View Order</Link>
68
+ ```
69
+
70
+ ### Protected Routes
71
+
72
+ Protected routes automatically redirect through auth when logged out:
73
+
74
+ ```tsx
75
+ // Before
76
+ const ordersUrl = isLoggedIn
77
+ ? '/account/orders'
78
+ : `/account/authenticate?redirect=${encodeURIComponent('/account/orders')}`;
79
+
80
+ // After
81
+ const ordersUrl = useRoute(Routes.account.orders.index);
82
+ // Logged in: '/account/orders'
83
+ // Logged out: '/account/authenticate?redirect=%2Faccount%2Forders'
84
+ ```
85
+
86
+ ### Login with Return
87
+
88
+ ```tsx
89
+ // Before
90
+ const loginUrl = `/account/authenticate?redirect=${encodeURIComponent(pathname)}`;
91
+ const loginUrl = Routes.account.loginAndReturn(Routes.cart.index);
92
+
93
+ // After
94
+ const loginUrl = useRoute(Routes.account.authenticate, null, { returnHere: true });
95
+ ```
96
+
97
+ ### router.push
98
+
99
+ ```tsx
100
+ // Before
101
+ router.push('/checkout');
102
+ router.push(`/checkout/success/${orderId}`);
103
+
104
+ // After
105
+ const checkoutUrl = useRoute(Routes.checkout.index);
106
+ router.push(checkoutUrl);
107
+
108
+ const successUrl = useRoute(Routes.checkout.success, { orderId });
109
+ router.push(successUrl);
110
+ ```
111
+
112
+ ## Adding New Routes
113
+
114
+ Add to `apps/frontend/config/routes.ts`:
115
+
116
+ ```tsx
117
+ const RawRoutes = {
118
+ // Static route
119
+ newPage: {
120
+ path: '/new-page',
121
+ isProtected: false,
122
+ },
123
+
124
+ // Nested with dynamic param
125
+ feature: {
126
+ index: {
127
+ path: '/feature',
128
+ isProtected: false,
129
+ },
130
+ details: {
131
+ path: '/feature/:featureId',
132
+ isProtected: true,
133
+ },
134
+ },
135
+ } as const;
136
+ ```
137
+
138
+ ## Deprecated
139
+
140
+ - `Routes.account.loginAndReturn()` - Use `useRoute(route, null, { returnHere: true })`
141
+ - `isProtectedRoute(pathname)` - Use `route.isProtected` directly
142
+ - Direct `.path` access - Use `useRoute()` for all URL generation
@@ -0,0 +1,82 @@
1
+ # Input disableSkeleton Prop
2
+
3
+ **Date:** 2026-01-15
4
+ **Affects:** `@clicktap/ui`, `apps/frontend`
5
+ **Breaking:** No
6
+ **Classification:** IMPROVEMENT
7
+
8
+
9
+ ## Dependencies
10
+
11
+ *None*
12
+
13
+ ## Summary
14
+
15
+ Added a `disableSkeleton` prop to the Input component that allows consumers to skip the skeleton loading state during hydration. This is useful when you need immediate interactivity (e.g., auto-focus) on page load.
16
+
17
+ ## Background
18
+
19
+ The Input component uses `useIsClient()` to show a skeleton placeholder during server-side rendering and hydration. This prevents hydration mismatches but causes a brief "flash" where the input appears disabled/skeleton before becoming interactive.
20
+
21
+ For forms where immediate user interaction is expected (like login/authentication pages), this delay can feel sluggish.
22
+
23
+ ## Changes
24
+
25
+ ### New Prop
26
+
27
+ ```typescript
28
+ interface InputProps extends TextFieldProps {
29
+ // ... existing props
30
+
31
+ /**
32
+ * When true, renders the input immediately without showing a skeleton during hydration.
33
+ * Use this when the input doesn't need hydration protection (e.g., simple text inputs
34
+ * that don't have complex state or need immediate interactivity).
35
+ */
36
+ disableSkeleton?: boolean;
37
+ }
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### Before
43
+
44
+ ```tsx
45
+ <Input
46
+ name="email"
47
+ type="email"
48
+ label="Email"
49
+ autoFocus // Won't work - skeleton renders first
50
+ />
51
+ ```
52
+
53
+ ### After
54
+
55
+ ```tsx
56
+ <Input
57
+ name="email"
58
+ type="email"
59
+ label="Email"
60
+ disableSkeleton // Renders immediately
61
+ autoFocus // Now works as expected
62
+ />
63
+ ```
64
+
65
+ ## When to Use
66
+
67
+ **Use `disableSkeleton` when:**
68
+ - The input needs to auto-focus on page load
69
+ - Immediate interactivity is important for UX
70
+ - The input is a simple text/email/password field without complex state
71
+
72
+ **Don't use `disableSkeleton` when:**
73
+ - The input has complex state that could cause hydration mismatches
74
+ - The input relies on client-only data that isn't available during SSR
75
+ - You're unsure - the default skeleton behavior is safer
76
+
77
+ ## Migration Checklist
78
+
79
+ - [ ] Identify inputs that need immediate interactivity (e.g., login forms)
80
+ - [ ] Add `disableSkeleton` prop to those inputs
81
+ - [ ] Add `autoFocus` if the input should focus on page load
82
+ - [ ] Test that hydration works correctly (no console errors)
@@ -0,0 +1,94 @@
1
+ # Environment Utilities from @clicktap/ui
2
+
3
+ **Date:** 2026-02-16
4
+ **Affects:** All files importing `isDevelopment` / `isProduction` from local `utils/env`
5
+ **Classification:** IMPROVEMENT
6
+ **Breaking:** No
7
+
8
+ ## Dependencies
9
+
10
+ - [Payment Method Error Handling](../../../../../../apps/frontend/docs/migrations/2026/01/2026-01-14-payment-error-handling.md) — introduced the local `utils/env.ts`
11
+
12
+ ## Summary
13
+
14
+ The `isDevelopment` and `isProduction` boolean constants have been moved from the frontend's local `utils/env.ts` into `@clicktap/ui/utils/env`, making them available to all apps consuming `@clicktap/ui`. The local file has been deleted.
15
+
16
+ ## Problem
17
+
18
+ The `isDevelopment` / `isProduction` utility was defined locally in `apps/frontend/utils/env.ts`. Other apps (admin, future storefronts) that need the same pattern would have to duplicate it. Moving it into the shared UI library centralises the definition.
19
+
20
+ Additionally, the `@clicktap/ui` Vite build was replacing `process.env.NODE_ENV` with `"production"` at library build time, which meant any `NODE_ENV`-based guards inside the UI library (e.g., `console.warn` in the Image component) were dead-code-eliminated before consumers ever saw them. A `define` override in `libs/ui/vite.config.ts` now preserves `process.env.NODE_ENV` so the **consuming app's** bundler handles the replacement.
21
+
22
+ ## Changes
23
+
24
+ ### Vite library build fix
25
+
26
+ The `@clicktap/ui` Vite config now includes:
27
+
28
+ ```ts
29
+ define: {
30
+ 'process.env.NODE_ENV': 'process.env.NODE_ENV',
31
+ },
32
+ ```
33
+
34
+ This prevents Vite from baking `"production"` into the published bundle, so environment-guarded code (like dev-only `console.warn`) works correctly at the consumer's build time.
35
+
36
+ **Source:** `libs/ui/vite.config.ts`
37
+
38
+ ### New shared utility
39
+
40
+ ```ts
41
+ // @clicktap/ui/utils/env
42
+ export const isDevelopment = process.env.NODE_ENV === 'development';
43
+ export const isProduction = process.env.NODE_ENV === 'production';
44
+ ```
45
+
46
+ **Source:** `libs/ui/src/utils/env.ts`
47
+
48
+ ### Import change
49
+
50
+ **Before:**
51
+ ```ts
52
+ import { isDevelopment } from '../utils/env';
53
+ import { isDevelopment } from '../../../utils/env';
54
+ import { isDevelopment } from '../../../../../utils/env';
55
+ ```
56
+
57
+ **After:**
58
+ ```ts
59
+ import { isDevelopment } from '@clicktap/ui/utils/env';
60
+ ```
61
+
62
+ ### Files changed
63
+
64
+ | File | Change |
65
+ |------|--------|
66
+ | `apps/frontend/utils/env.ts` | Deleted |
67
+ | `providers/CybersourceMicroformV2Provider.tsx` | Import updated |
68
+ | `components/Checkout/ExpressCheckout/PaypalButton.tsx` | Import updated |
69
+ | `components/Checkout/ExpressCheckout/GooglePayButton.tsx` | Import updated |
70
+ | `components/Checkout/ExpressCheckout/ApplePayButton.tsx` | Import updated |
71
+ | `components/Checkout/ExpressCheckout/AmazonPayButton.tsx` | Import updated |
72
+ | `components/Checkout/Step/Review/CheckoutButton/ApplePayCheckoutButton.tsx` | Import updated |
73
+ | `components/Checkout/Step/Review/CheckoutButton/GooglePayCheckoutButton.tsx` | Import updated |
74
+ | `components/Checkout/Step/Review/CheckoutButton/PaypalCheckoutButton.tsx` | Import updated |
75
+ | `components/Checkout/Step/Review/CheckoutButton/AmazonCheckoutButton.tsx` | Import updated |
76
+ | `components/Checkout/Step/Payment/Authorizenet/AuthorizenetPaymentMethod.tsx` | Import updated |
77
+
78
+ ## Migration Steps
79
+
80
+ 1. Update `@clicktap/ui` to the version containing this change.
81
+ 2. Find all imports from the local `utils/env`:
82
+ ```bash
83
+ grep -r "from.*utils/env" apps/frontend/
84
+ ```
85
+ 3. Replace each import path with `@clicktap/ui/utils/env`.
86
+ 4. Delete `apps/frontend/utils/env.ts`.
87
+ 5. Run typecheck to confirm no broken imports.
88
+
89
+ ## Migration Checklist
90
+
91
+ - [ ] Update `@clicktap/ui` to latest
92
+ - [ ] Replace all `utils/env` imports with `@clicktap/ui/utils/env`
93
+ - [ ] Delete `apps/frontend/utils/env.ts`
94
+ - [ ] Run typecheck: `nx run frontend:typecheck`
@@ -0,0 +1,84 @@
1
+ # Select popoverPortalContainer Restored
2
+
3
+ **Date:** 2026-02-17
4
+ **Affects:** `@clicktap/ui` Select component, any consumer using Select inside Dialog or Modal
5
+ **Classification:** BUGFIX
6
+ **Breaking:** No
7
+
8
+ ## Dependencies
9
+
10
+ *None*
11
+
12
+ ## Summary
13
+
14
+ The `popoverPortalContainer` prop was incorrectly removed from the Select component on 2026-01-30 during the react-aria upgrade refactor. This prop controls where the Popover is portalled, which is essential when Select is used inside a Dialog — without it, the Popover renders into `document.body`, causing incorrect width calculation because both Dialog and Popover use react-aria's Modal internals. The prop has been restored using `UNSAFE_PortalProvider` from `react-aria`.
15
+
16
+ ## Problem
17
+
18
+ The react-aria upgrade (commits `7c27599` and `93ae6b3` on 2026-01-30) removed `popoverPortalContainer` in two steps:
19
+
20
+ 1. Commit `7c27599` correctly migrated from `UNSTABLE_portalContainer` (on Popover) to `UNSAFE_PortalProvider` (wrapper), preserving the feature
21
+ 2. Commit `93ae6b3` then removed the feature entirely — deleting the prop from types, removing the `UNSAFE_PortalProvider` import and conditional rendering
22
+
23
+ The migration guide in [Checkout Fixes — Section 6](../../../../../../apps/frontend/docs/migrations/2026/01/2026-01-14-checkout-fixes.md) was updated to document this removal as intentional. It was not — `popoverPortalContainer` was a working feature, not a deprecated one.
24
+
25
+ **If your codebase was updated following that migration guide**, you may have removed `popoverPortalContainer` usage from Select instances. Review git history around 2026-01-30 for removals:
26
+
27
+ ```bash
28
+ git log --all --oneline --after="2026-01-28" --before="2026-02-01" -- '*.tsx' '*.ts' | head -20
29
+ git log --all -p --after="2026-01-28" --before="2026-02-01" -S "popoverPortalContainer" -- '*.tsx'
30
+ ```
31
+
32
+ ## Changes
33
+
34
+ | File | What changed |
35
+ |------|-------------|
36
+ | `libs/ui/src/components/Select/Select.tsx` | Restored `UNSAFE_PortalProvider` import from `react-aria`; restored conditional portal wrapping |
37
+ | `libs/ui/src/components/Select/Select.types.ts` | Restored `popoverPortalContainer?: () => HTMLElement | null` prop |
38
+
39
+ ### Restored behavior
40
+
41
+ When `popoverPortalContainer` is provided, the Popover is wrapped in `UNSAFE_PortalProvider` which controls where the portal renders. When omitted, default react-aria behavior applies (portal to `document.body`).
42
+
43
+ ```tsx
44
+ // Select.tsx — restored conditional
45
+ {popoverPortalContainer ? (
46
+ <UNSAFE_PortalProvider getContainer={popoverPortalContainer}>
47
+ {popoverContent}
48
+ </UNSAFE_PortalProvider>
49
+ ) : (
50
+ popoverContent
51
+ )}
52
+ ```
53
+
54
+ **Source:** `libs/ui/src/components/Select/Select.tsx:209`
55
+
56
+ ## Migration Steps
57
+
58
+ 1. **If you removed `popoverPortalContainer` from Select usage** following the previous migration guide, restore it:
59
+
60
+ ```tsx
61
+ // Restore this where Select is used inside Dialog/Modal
62
+ <Select popoverPortalContainer={() => dialogRef.current}>
63
+ ```
64
+
65
+ 2. **Note the type change from the original API** — this was correctly updated during the react-aria upgrade and remains changed:
66
+
67
+ ```tsx
68
+ // Old (pre-upgrade)
69
+ popoverPortalContainer?: Element;
70
+
71
+ // Current (restored)
72
+ popoverPortalContainer?: () => HTMLElement | null;
73
+ ```
74
+
75
+ The prop now takes a **getter function** instead of a direct element reference.
76
+
77
+ 3. **The `UNSAFE_PortalProvider` import is from `react-aria`**, not `react-aria-components`. If you have custom components that also need portal control, import from the correct package.
78
+
79
+ ## Migration Checklist
80
+
81
+ - [ ] Search git history for removed `popoverPortalContainer` usage around 2026-01-30
82
+ - [ ] Restore `popoverPortalContainer` on any Select used inside Dialog, Modal, or other overlay contexts
83
+ - [ ] Update prop value from `Element` to `() => HTMLElement | null` getter if restoring pre-upgrade code
84
+ - [ ] Verify Select popover width matches trigger width when rendered inside Dialog