@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,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
|