@clicktap/ui 0.40.7 → 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 (23) hide show
  1. package/components/Menu/Menu.js +1 -1
  2. package/docs/migrations/2026/01/2026-01-14-external-link-detection.md +290 -0
  3. package/docs/migrations/2026/01/2026-01-14-use-route-hook.md +142 -0
  4. package/docs/migrations/2026/01/2026-01-15-input-disable-skeleton.md +82 -0
  5. package/docs/migrations/2026/02/2026-02-16-env-utils-from-clicktap-ui.md +94 -0
  6. package/docs/migrations/2026/02/2026-02-17-select-popover-portal-container-restored.md +84 -0
  7. package/docs/migrations/2026/03/2026-03-24-image-dimmed-hidden-onready.md +108 -0
  8. package/docs/migrations/2026/03/2026-03-24-modal-overlay-backdrop-dismiss.md +81 -0
  9. package/docs/migrations/2026/04/2026-04-17-select-address-input-onchange-key-or-item.md +182 -0
  10. package/docs/migrations/2026/04/2026-04-24-image-placeholder-frontend-fallback.md +85 -0
  11. package/docs/migrations/2026/04/2026-04-30-prune-invalid-pdp-selections.md +166 -0
  12. package/docs/migrations/2026/04/2026-04-30-top-down-pdp-option-dependency.md +286 -0
  13. package/docs/migrations/2026/05/2026-05-01-build-pdp-url-util.md +128 -0
  14. package/docs/migrations/2026/05/2026-05-03-auth-aware-graphql-client.md +278 -0
  15. package/docs/migrations/2026/05/2026-05-20-framer-motion-11-idioms.md +391 -0
  16. package/docs/migrations/2026/05/2026-05-21-button-loading-content.md +265 -0
  17. package/docs/migrations/2026/05/2026-05-21-password-check-hide-on-empty.md +50 -0
  18. package/docs/migrations/2026/05/2026-05-29-password-check-requirements-variant.md +63 -0
  19. package/docs/migrations/2026/06/2026-06-17-select-standardization.md +286 -0
  20. package/docs/migrations/2026/06/2026-06-18-select-hiddenselect-setvalue-crash.md +75 -0
  21. package/docs/migrations/2026/07/2026-07-31-ui-exports-bundler-resolution.md +50 -0
  22. package/docs/migrations/README.md +31 -0
  23. package/package.json +2 -2
@@ -0,0 +1,286 @@
1
+ # Select standardization — one `<Select searchable>`, `DropdownSelect` removed
2
+
3
+ **Date:** 2026-06-17
4
+ **Affects:** Any app consuming `@clicktap/ui`'s `Select` / `DropdownSelect` (Crispi, FFL, and any fork that renders pickers, sort controls, country/region selects, or configurable-product options).
5
+ **Classification:** FEATURE + BREAKING
6
+ **Breaking:** Yes — (1) `DropdownSelect` removed, (2) the bare `<Select>` is now a **button picker**, not a combobox, (3) `DropdownSelect`'s `classNames.trigger` is now `classNames.input`, (4) the root is `w-full` by default, (5) `<Option>` is react-stately's `Item` — keyed by `key` (no `id`) and **cannot be wrapped**.
7
+
8
+ ## Dependencies
9
+
10
+ | Package | Minimum version |
11
+ |---|---|
12
+ | `@clicktap/ui` | The release bundling this commit. Confirm with `jq '.dependencies["@clicktap/ui"]' apps/frontend/package.json`. |
13
+
14
+ (Actual version is set by semantic-release when `bugfixes` merges to `master`.)
15
+
16
+ ## Summary
17
+
18
+ There used to be **two** components: `DropdownSelect` (a native-feel button picker) and `Select` (a react-aria `ComboBox` with a text input). They had different APIs, different `classNames` keys, and overlapping purposes.
19
+
20
+ They're now **one** component — `<Select>` — with a `searchable` prop:
21
+
22
+ - `searchable={false}` *(default)* — **button picker**: click to open, pick one. Right for short, fixed lists (sort order, "show per row"). Backed by a real hidden `<select>`, so it autofills.
23
+ - `searchable` *(`true`)* — **combobox**: type to filter; the first match highlights, Tab/Enter commit it and Tab advances to the next field. Right for long lists (country, region). The dropdown is **portaled** so it works inside modals/drawers, and it ships with a hidden `<select>` mirror so password managers / browser autofill still work.
24
+ - `searchable="auto"` — searchable once there are more than ~8 options.
25
+
26
+ `<Option>` is react-stately's `Item` (`OptionProps<T> = ItemProps<T>`). **This is a breaking change if you previously passed `id` or wrapped `<Option>`** — see [Options: `id` → `key`, do not wrap](#options-id--key-do-not-wrap).
27
+
28
+ ## What changed
29
+
30
+ | Area | Before | After |
31
+ |---|---|---|
32
+ | Exports | `DropdownSelect`, `DropdownSelectProps` + `Select` | `Select` only (`DropdownSelect` removed) |
33
+ | Bare `<Select>` | Combobox (type-to-filter) | **Button picker** — add `searchable` to keep filtering |
34
+ | `DropdownSelect` button class | `classNames.trigger` | `classNames.input` |
35
+ | Root width | `DropdownSelect`: auto (`flex items-center`) | `w-full` (form-field default) — override for inline/`justify-end` rows |
36
+ | **`<Option>`** | own element / accepted `id`, could be wrapped | **react-stately `Item`** — keyed by React `key` (no `id`); **cannot be wrapped** (it's a collection descriptor, not a rendered element). Style rows via `classNames.option`. |
37
+ | `SelectSlots.listBoxComponent` | Custom listbox renderer | **No-op** — the dropdown is rendered internally now; remove it |
38
+ | Option row theming | — | new `classNames.option` slot; focus/selected/disabled are `data-[focused]` / `data-[selected]` / `data-[disabled]` variants |
39
+ | Option groups | — | new `<Section title="…">` to group options (accessible heading); `classNames.sectionHeading` slot |
40
+ | Multi-select | — | `selectionMode="multiple"` → `selectedKeys: Key[]` + `onSelectionChange(keys: Key[])`. Searchable = combobox with chips; non-searchable = button with a summary. Single-select props/types unchanged. |
41
+ | Selection types | `SelectProps` is one interface | `SelectProps` is a **discriminated union** on `selectionMode`. New `SelectBaseProps` export = everything except the selection contract — base single-select **wrappers** on it (`Omit` over the union collapses to common keys). |
42
+ | New props | — | `searchable`, `selectTextOnFocus`, `autoFocusFirstOption`, `name` + `autoComplete` (autofill) |
43
+ | Filtering | name only | match by name **or** option `textValue` aliases (codes) |
44
+
45
+ Find affected usages:
46
+
47
+ ```bash
48
+ grep -rn 'DropdownSelect\|classNames=' apps/frontend libs/ui \
49
+ | grep -E 'DropdownSelect|trigger:|listBoxComponent' | head -40
50
+ # And every <Select> that relied on type-to-filter:
51
+ grep -rn '<Select' apps/frontend | grep -v 'searchable' | head -40
52
+ ```
53
+
54
+ ## Migration Steps
55
+
56
+ ### 1. `DropdownSelect` → `<Select>` (default button mode)
57
+
58
+ ```tsx
59
+ // Before
60
+ import { DropdownSelect, Option } from '@clicktap/ui/components/Select';
61
+
62
+ <DropdownSelect
63
+ selectedKey={key}
64
+ onSelectionChange={onChange}
65
+ classNames={{ trigger: 'text-xs w-44 h-8', value: 'text-xs', label: '...' }}
66
+ >
67
+ <Option key="a">A</Option>
68
+ </DropdownSelect>
69
+
70
+ // After — same button look; `trigger` → `input`
71
+ import { Select, Option } from '@clicktap/ui/components/Select';
72
+
73
+ <Select
74
+ selectedKey={key}
75
+ onSelectionChange={onChange}
76
+ classNames={{ input: 'text-xs w-44 h-8', value: 'text-xs', label: '...' }}
77
+ >
78
+ <Option key="a">A</Option>
79
+ </Select>
80
+ ```
81
+
82
+ ### 2. Old `<Select>` that filtered → add `searchable`
83
+
84
+ The bare `<Select>` no longer renders a text input. If you relied on type-to-filter, opt in:
85
+
86
+ ```tsx
87
+ // Before — combobox behavior was implicit
88
+ <Select selectedKey={key} onSelectionChange={onChange}>{options}</Select>
89
+
90
+ // After — keep type-to-filter
91
+ <Select searchable selectedKey={key} onSelectionChange={onChange}>{options}</Select>
92
+ ```
93
+
94
+ For long lists where you're unsure, `searchable="auto"` turns it on past ~8 options.
95
+
96
+ ### 3. Fix inline / right-floated layout (the `w-full` root)
97
+
98
+ `DropdownSelect`'s root was auto-width; the new `Select` root is `w-full` (correct for form fields, wrong for a toolbar row). In a `justify-end` / `flex items-center` row it now fills the width and sits left. `cn` is `twMerge`, so a `className` width override on the **root** wins:
99
+
100
+ ```tsx
101
+ // Before (broke: fills the row, left-aligned)
102
+ <div className="flex items-center justify-end">
103
+ <Select classNames={{ input: 'w-44' }} ... />
104
+ </div>
105
+
106
+ // After (floats right again)
107
+ <div className="flex items-center justify-end">
108
+ <Select className="w-44" classNames={{ input: 'w-44' }} ... />
109
+ </div>
110
+ ```
111
+
112
+ > `classNames.input` sizes the button/input; **`className` sizes the root.** Constrain the root anywhere the Select isn't meant to be full-width.
113
+
114
+ ### 4. Options: `id` → `key`, do not wrap
115
+
116
+ `<Option>` is now react-stately's `Item` (`OptionProps<T> = ItemProps<T>`). Two consequences:
117
+
118
+ - **`id` is gone — options are keyed by the React `key`.** Every `<Option id={…}>` fails to typecheck.
119
+ ```tsx
120
+ // Before
121
+ <Option id={country.code}>{country.name}</Option>
122
+ // After
123
+ <Option key={country.code}>{country.name}</Option>
124
+ ```
125
+ - **You cannot wrap `<Option>`.** `Item` is a *collection descriptor*, not a rendered element — the Select reads its props to build the list and renders the rows itself. A wrapper (`<MyOption>` → styled `<div>` → `<Item>`) breaks twice: the React `key` is swallowed by the wrapper (identity lost) and the wrapper's markup/`className` is never rendered. Use `<Option>` **directly** and style rows with `classNames.option` (next step).
126
+
127
+ ### 5. Theme the option rows (`classNames.option`)
128
+
129
+ Option rows previously hard-coded a light palette with no override hook. There's now a `classNames.option` slot, and the focus/selected/disabled states are emitted as **`data-*` variants** so a non-light theme can retarget them:
130
+
131
+ ```tsx
132
+ <Select
133
+ searchable
134
+ classNames={{
135
+ listContainer: 'bg-zinc-900 border-zinc-700', // the dropdown surface
136
+ option: 'text-zinc-100 data-[focused]:bg-zinc-800 data-[disabled]:text-zinc-500',
137
+ }}
138
+ >
139
+ {items.map((i) => <Option key={i.id}>{i.label}</Option>)}
140
+ </Select>
141
+ ```
142
+
143
+ `classNames.option` is merged last, so `twMerge` lets it win over the defaults (`text-slate-900`, `data-[focused]:bg-slate-100`, …). This replaces the old descendant-selector hack (`classNames.list: '[&_li]:…'`) and any per-option wrapper.
144
+
145
+ ### 6. Remove `slots.listBoxComponent`
146
+
147
+ The dropdown list is rendered internally now; a custom `listBoxComponent` is ignored. Delete it (style via `classNames.listContainer` / `classNames.list` / `classNames.option` instead).
148
+
149
+ ### 7. (Optional) Enable autofill on a searchable select
150
+
151
+ A searchable combobox has no native form control for its *selection*, so password managers / browser autofill can't fill it — **unless** you pass `name` (and ideally an `autoComplete` token). The Select then renders a hidden native `<select>` (for ≤300 options) that autofill targets; its change drives the selection.
152
+
153
+ ```tsx
154
+ <Select
155
+ searchable
156
+ name="region"
157
+ autoComplete="address-level1" // "country" for a country field, etc.
158
+ selectedKey={value ?? null}
159
+ onSelectionChange={onChange}
160
+ >
161
+ {regions.map((r) => <Option key={r.code}>{r.name}</Option>)}
162
+ </Select>
163
+ ```
164
+
165
+ Autofill matches the **option text** (the state/country name), so render the name as the option's children.
166
+
167
+ ### 8. (Optional) Match by code/alias when typing
168
+
169
+ By default filtering matches the option's `textValue` (its display name). To let users type a **code** ("tx", "us-tx") and still find the row, put the aliases in `textValue` — the field still displays the rendered children, not `textValue`:
170
+
171
+ ```tsx
172
+ <Option key={code} textValue={`${name} ${shortCode} ${code}`}>
173
+ {name}
174
+ </Option>
175
+ ```
176
+
177
+ **Match ranking.** While filtering a flat searchable list, matches are reordered
178
+ by relevance: an exact whitespace-token match (a code, e.g. `us`) first, then
179
+ options whose name **starts with** the query, then mid-word substring hits. So
180
+ typing `us` ranks *United States* (code `us`) above *Belarus* (substring), and
181
+ typing `uni` ranks *United States* (prefix) above *Reunion* (mid-word). Ranking
182
+ applies only when the Select owns filtering — it's skipped when you control
183
+ `inputValue` (you own ordering), pass a render-fn `children`, or use `<Section>`
184
+ groups (reordering would break grouping). See the `MatchRanking` story.
185
+
186
+ ### 9. (Optional) Group options with `<Section>`
187
+
188
+ Wrap `<Option>`s in `<Section title="…">` for grouped lists (an accessible group
189
+ heading per section). Works in both modes; in searchable mode the filter matches
190
+ options within groups and drops empty ones, and Tab-complete commits the first
191
+ matching **option** (section headers are skipped). Style headings via
192
+ `classNames.sectionHeading`.
193
+
194
+ ```tsx
195
+ import { Select, Option, Section } from '@clicktap/ui/components/Select';
196
+
197
+ <Select searchable classNames={{ sectionHeading: 'text-zinc-400' }}>
198
+ <Section title="North America">
199
+ <Option key="us">United States</Option>
200
+ <Option key="ca">Canada</Option>
201
+ </Section>
202
+ <Section title="Europe">
203
+ <Option key="fr">France</Option>
204
+ </Section>
205
+ </Select>
206
+ ```
207
+
208
+ `<Section>` is react-stately's `Section` (a collection descriptor, like `<Option>`); it also accepts the dynamic `items` render form. `searchable="auto"` counts the options inside groups, not the groups.
209
+
210
+ ### 10. (Optional) Multi-select
211
+
212
+ `selectionMode="multiple"` switches the selection contract to an array: pick
213
+ several options, the menu stays open until you click away. The callback receives
214
+ a `Key[]`. It works in **both** modes — searchable and not:
215
+
216
+ ```tsx
217
+ const [keys, setKeys] = useState<Key[]>([]);
218
+
219
+ // Searchable → combobox multi: type to filter, picks shown as removable chips.
220
+ <Select searchable selectionMode="multiple" selectedKeys={keys} onSelectionChange={setKeys}>
221
+ {/* combines with <Section> groups too */}
222
+ </Select>
223
+
224
+ // Not searchable → button multi: a button whose summary lists the picks; the
225
+ // open listbox toggles each option (stays open).
226
+ <Select selectionMode="multiple" selectedKeys={keys} onSelectionChange={setKeys}>
227
+ {/* short fixed lists */}
228
+ </Select>
229
+ ```
230
+
231
+ `SelectProps` is a **discriminated union** on `selectionMode`, so single-select
232
+ keeps its exact `selectedKey`/`onSelectionChange(Key | null)` types — no change to
233
+ existing call sites.
234
+
235
+ **Display & order conventions:**
236
+ - *Combobox (chips)* — `classNames.tagGroup` / `tag` / `tagRemoveButton`; chips in
237
+ **selection order** (newest last, the usual token-input convention); full
238
+ keyboard a11y via react-aria-components `TagGroup` (arrow-nav, Backspace/Delete).
239
+ - *Button (summary)* — comma-joined labels in **collection order** (stable, reads
240
+ like the list), styled via `classNames.value`.
241
+
242
+ > **Wrappers:** if you build a single-select wrapper that does
243
+ > `Omit<SelectProps, …>`, switch the base to the new **`SelectBaseProps`** export.
244
+ > `Omit` over a union keeps only keys common to *all* members, which silently
245
+ > drops the per-mode selection props. (This is why `AddressInput` was rebased.)
246
+ >
247
+ > _Caveats:_ button-mode multi has **no hidden native `<select>`** (so no
248
+ > native-form submission / autofill — consume via `selectedKeys`), and selected
249
+ > rows show via `data-[selected]` (no checkmark icon). A `tagOrder` toggle
250
+ > (collection-order chips) is deferred until a use case needs it.
251
+
252
+ ### Verification
253
+
254
+ ```bash
255
+ nx run frontend:typecheck # no errors referencing DropdownSelect / trigger / listBoxComponent
256
+ nx run ui:typecheck
257
+ nx run ui:build-storybook
258
+ ```
259
+
260
+ Visually confirm, per migrated select:
261
+ - [ ] Button selects open/select/close; value shows.
262
+ - [ ] Searchable selects: type to filter, Tab/Enter commit, Tab advances; clear + Tab clears.
263
+ - [ ] Searchable inside a **modal/drawer**: Tab moves to the next field (dropdown is portaled out of the focus trap).
264
+ - [ ] Toolbar/inline selects float/size correctly (root width set).
265
+ - [ ] Autofill (if enabled): password manager fills the field; value sticks.
266
+ - [ ] Themed rows (if you set `classNames.option`): text + focus/selected/disabled colors apply (see the `ThemedOptions` story).
267
+ - [ ] Grouped options (if you use `<Section>`): headings render, filtering matches within groups, Tab commits the first matching option (see the `Grouped` story).
268
+ - [ ] Multi-select (if you use `selectionMode="multiple"`): combobox shows chips (filter clears per pick, × removes); button shows a collection-order summary; menu stays open until click-away; callback gets `Key[]` (see `Multiple` / `GroupedMultiple` / `ButtonMultiple`).
269
+
270
+ ## Rollback
271
+
272
+ Revert `@clicktap/ui` in the consuming app:
273
+
274
+ ```bash
275
+ docker compose exec -u app node-app pnpm --filter <app> add '@clicktap/ui@<previous-minor>'
276
+ ```
277
+
278
+ Safe — presentation-layer only, no persistent state.
279
+
280
+ ## References
281
+
282
+ - Source: `libs/ui/src/components/Select/` (`Select.tsx` dispatches to `ButtonSelect.tsx` / `ComboBoxSelect.tsx`; shared parts in `parts.tsx`)
283
+ - Types: `libs/ui/src/components/Select/Select.types.ts`
284
+ - Stories: `libs/ui/src/components/Select/Select.stories.tsx` (`Picker`, `Searchable`, `SearchableAuto`, `AsyncDataSource`, `ServerSideSearch`, `ThemedOptions`, `Grouped`, `Multiple`, `GroupedMultiple`, `ButtonMultiple`)
285
+ - In-repo consumer examples: `apps/frontend/components/AddressForm/AddressForm.tsx` (country/region, autofill, aliases), `apps/frontend/components/Collection/CollectionToolbar.tsx` (toolbar width), `apps/frontend/components/Product/components/ConfigurableOptions/ConfigurableOptions.tsx`
286
+ - Related: [`Select` `onSelectionChange` widened to `Key | T`](../04/2026-04-17-select-address-input-onchange-key-or-item.md), [`Select` `popoverPortalContainer` restored](../02/2026-02-17-select-popover-portal-container-restored.md)
@@ -0,0 +1,75 @@
1
+ # `Select` (searchable): fix "setValue is not a function" crash on browser autofill
2
+
3
+ **Date:** 2026-06-18
4
+ **Affects:** `@clicktap/ui` `Select` — the **searchable** (ComboBox) variant (`ComboBoxSelect`)
5
+ **Classification:** BUGFIX
6
+ **Breaking:** No
7
+
8
+ ## Why
9
+
10
+ A **searchable** `<Select>` with a `name` (for autofill) crashed on **browser autofill** with:
11
+
12
+ ```
13
+ TypeError: setValue is not a function
14
+ at @react-aria/select .../HiddenSelect.mjs (its <select> onChange calls state.setValue)
15
+ ```
16
+
17
+ `ComboBoxSelect` renders react-aria's `HiddenSelect` (a select-only helper) so password
18
+ managers / browser autofill have a native control to fill, and passed it the **combobox** state:
19
+
20
+ ```ts
21
+ <HiddenSelect state={state as unknown as SelectState<T>} … />
22
+ ```
23
+
24
+ That cast is a lie at runtime: `HiddenSelect`'s `<select> onChange` calls `state.setValue`, which
25
+ exists on **SelectState** (`useSelectState`, the non-searchable `ButtonSelect`) but **not** on
26
+ **ComboBoxState** (`useComboBoxState`). So when autofill wrote into the hidden `<select>`, its change
27
+ handler threw. Manual entry didn't trigger it; autofilling an address/country/region/profile form
28
+ did. (`ButtonSelect` never crashed — it has a real `SelectState`.)
29
+
30
+ This was **not** dep-fixable — it reproduced on the latest `@clicktap/ui` + `react-aria`.
31
+
32
+ ## The fix
33
+
34
+ Wrap the combobox state in a `Proxy` that supplies `setValue` (mapped to `setSelectedKey`) while
35
+ preserving the state's live getters (`selectedKey`, `collection`, …), and pass *that* to
36
+ `HiddenSelect`:
37
+
38
+ ```ts
39
+ const hiddenSelectState = useMemo(
40
+ () =>
41
+ new Proxy(state, {
42
+ get(target, prop) {
43
+ if (prop === 'setValue') {
44
+ return (key: Key | null) => target.setSelectedKey(key);
45
+ }
46
+ return Reflect.get(target, prop);
47
+ },
48
+ }) as unknown as SelectState<T>,
49
+ [state],
50
+ );
51
+ // …
52
+ <HiddenSelect state={hiddenSelectState} … />
53
+ ```
54
+
55
+ A `Proxy` (not a spread) is used so the combobox state's getters keep reflecting the current
56
+ selection. The hidden native `<select>` mirror — and thus autofill support — is retained; only the
57
+ crash is removed.
58
+
59
+ ## Consumer action
60
+
61
+ If you applied an **interim workaround** while this was unfixed, you can remove it once on a build
62
+ with this fix (the next `@clicktap/ui` release, **> 0.40.1**):
63
+
64
+ - `searchable={false}` forced onto autofill-prone form selects (it routed to `ButtonSelect`/`SelectState` to dodge the crash, at the cost of type-to-filter) — restore `searchable`.
65
+ - a `pnpm patch @clicktap/ui` carrying the `setValue → setSelectedKey` shim — drop the patch.
66
+
67
+ No API change; searchable `Select`s with a `name` now survive autofill.
68
+
69
+ ## Notes
70
+
71
+ - Dropping `name` does **not** avoid it — react-aria renders `HiddenSelect` regardless, so autofill
72
+ still reaches it.
73
+ - A larger future option is to drop `HiddenSelect` for the combobox entirely and use react-aria's
74
+ combobox form-value path (a hidden text input). The Proxy shim is the contained fix and keeps the
75
+ existing `<select>`-based autofill behavior.
@@ -0,0 +1,50 @@
1
+ # `@clicktap/ui` resolves its deep subpaths under `moduleResolution: bundler` (CTAP-1060)
2
+
3
+ **Date:** 2026-07-31
4
+ **Package:** `@clicktap/ui`
5
+ **Breaking:** No — additive to the `exports` map. Existing `moduleResolution: node` / `node10`
6
+ consumers are unaffected (that resolver ignores `exports`); this only *adds* working resolution under
7
+ `bundler` / `node16` / `nodenext`.
8
+ **Classification:** BUGFIX (packaging) — unblocks `moduleResolution: bundler`, and with it TypeScript 7
9
+
10
+ ## What was wrong
11
+
12
+ `@clicktap/ui`'s `exports` map pointed its wildcard subpaths at **extensionless** targets
13
+ (`"./utils/*": "./utils/*"`). The legacy `moduleResolution: node` resolver ignores `exports` and
14
+ probes files, and runtime bundlers append extensions, so this *appeared* to work — but
15
+ `moduleResolution: bundler` / `node16` / `nodenext` **use `exports` strictly** and require an
16
+ extensioned target. A deep import like `@clicktap/ui/utils/env` resolved to `./utils/env` (no such
17
+ file) → **~928 `Cannot find module` errors** the moment a consumer switched to `bundler`. TypeScript 7
18
+ removes `node10` outright (`TS5108`), forcing `bundler` / `node16` — so the old map made TS 7
19
+ unreachable for every consumer.
20
+
21
+ ## What changed
22
+
23
+ The `exports` targets are now extensioned + `types`-conditioned, with an **array fallback** so both
24
+ flat files (`utils/cn.js`) and directory-index modules (`utils/variantOptions/index.js`) resolve:
25
+
26
+ ```jsonc
27
+ "./utils/*": {
28
+ "types": ["./utils/*.d.ts", "./utils/*/index.d.ts"],
29
+ "import": ["./utils/*.js", "./utils/*/index.js"]
30
+ }
31
+ ```
32
+
33
+ The same shape applies to `./components/*` and `./hooks/*`; `./types/*` is **type-only** (the build
34
+ emits only `.d.ts` for it).
35
+
36
+ ## What to do
37
+
38
+ - **On `moduleResolution: node` today:** nothing — you are unaffected. Bump `@clicktap/ui` as usual.
39
+ - **Switching to `moduleResolution: bundler` / `node16`:** bump `@clicktap/ui` to ≥ this release and the
40
+ deep `@clicktap/ui/utils/*`, `/components/*`, `/hooks/*` imports resolve (types included). This was the
41
+ prerequisite that previously produced the wall of `Cannot find module` errors.
42
+ - **TypeScript 7:** this removes the `@clicktap/ui` blocker. Note TS 7 has a *separate*, non-clicktap
43
+ blocker — **nx 20.0.3 cannot build a project graph with TS 7 installed** (`Cannot read properties of
44
+ undefined (reading 'readFile')`) — so TS 7 also needs an nx major.
45
+
46
+ ## Verified
47
+
48
+ Resolution was tested against the built package in both modes: under `bundler` all deep subpaths (flat
49
+ file, directory-index, and nested file) resolve **with types**; under `node` (node10) resolution is
50
+ unchanged (no regression).
@@ -0,0 +1,31 @@
1
+ # UI Library Migration Guides
2
+
3
+ Migration guides for the `@clicktap/ui` library. Each doc covers a specific change with before/after examples and checklists.
4
+
5
+ For migrations affecting the frontend app, see [frontend migrations](../../../../apps/frontend/docs/migrations/README.md).
6
+
7
+ ## Index
8
+
9
+ | Date | Migration | Breaking | Dependencies |
10
+ |------|-----------|----------|--------------|
11
+ | 2026-01-14 | [External link detection + `LinkProvider`](./2026/01/2026-01-14-external-link-detection.md) | No | *None* |
12
+ | 2026-01-14 | [`useRoute` hook + `RouteProvider`](./2026/01/2026-01-14-use-route-hook.md) | No | *None* |
13
+ | 2026-01-15 | [`Input` `disableSkeleton` prop](./2026/01/2026-01-15-input-disable-skeleton.md) | No | *None* |
14
+ | 2026-02-16 | [Environment utilities — `@clicktap/ui/utils/env`](./2026/02/2026-02-16-env-utils-from-clicktap-ui.md) | No | *None* |
15
+ | 2026-02-17 | [`Select` `popoverPortalContainer` restored](./2026/02/2026-02-17-select-popover-portal-container-restored.md) | No | *None* |
16
+ | 2026-03-24 | [Image: dimmed, hidden, and onReady Props](./2026/03/2026-03-24-image-dimmed-hidden-onready.md) ↔ | No | *None* |
17
+ | 2026-03-24 | [ModalOverlay: fix dismiss on backdrop click](./2026/03/2026-03-24-modal-overlay-backdrop-dismiss.md) | No | *None* |
18
+ | 2026-04-17 | [`Select`/`AddressInput` `onChange` widened to `Key \| T`](./2026/04/2026-04-17-select-address-input-onchange-key-or-item.md) | Yes | *None* |
19
+ | 2026-04-24 | [`PLACEHOLDER_IMAGE` Export from `@clicktap/ui/utils/placeholder`](./2026/04/2026-04-24-image-placeholder-frontend-fallback.md) ↔ | No | *None* |
20
+ | 2026-04-30 | [Top-Down PDP Option Dependency — `@clicktap/ui/utils/variantOptions`](./2026/04/2026-04-30-top-down-pdp-option-dependency.md) ↔ | No | *None* |
21
+ | 2026-04-30 | [PDP Option Selection — Conservative Prune Helper (`pruneInvalidSelections`)](./2026/04/2026-04-30-prune-invalid-pdp-selections.md) ↔ | No | Top-Down PDP Option Dependency |
22
+ | 2026-05-01 | [`buildPdpUrl` Utility — `@clicktap/ui/utils/buildPdpUrl`](./2026/05/2026-05-01-build-pdp-url-util.md) | No | *None* |
23
+ | 2026-05-03 | [Auth-aware GraphQL client — `@clicktap/ui/utils/createAuthAwareGraphqlClient`](./2026/05/2026-05-03-auth-aware-graphql-client.md) | No | *None* |
24
+ | 2026-05-20 | [framer-motion 11.x idioms — opacity-only for modals, primitives elsewhere, matched keyframe shapes](./2026/05/2026-05-20-framer-motion-11-idioms.md) ↔ | Yes (custom `animationVariants` only) | *None* |
25
+ | 2026-05-21 | [`ButtonLoadingContent` helper — right-side spinner for all submit buttons](./2026/05/2026-05-21-button-loading-content.md) ↔ | No | *None* |
26
+ | 2026-05-21 | [`PasswordCheck` `requirements` variant hides until the visitor types](./2026/05/2026-05-21-password-check-hide-on-empty.md) | No | *None* |
27
+ | 2026-05-29 | [`PasswordCheck` — the `requirements` variant (adoption guide for forks)](./2026/05/2026-05-29-password-check-requirements-variant.md) | No | PasswordCheck hide-on-empty |
28
+ | 2026-06-17 | [Select standardization — one `<Select searchable>`, `DropdownSelect` removed](./2026/06/2026-06-17-select-standardization.md) ↔ | Yes | *None* |
29
+ | 2026-06-18 | [Select (searchable): fix "setValue is not a function" crash on browser autofill](./2026/06/2026-06-18-select-hiddenselect-setvalue-crash.md) | No | Select standardization |
30
+
31
+ ↔ = cross-app migration (has counterpart in frontend app)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clicktap/ui",
3
- "version": "0.40.7",
3
+ "version": "0.40.9",
4
4
  "private": false,
5
5
  "author": "Clicktap",
6
6
  "description": "A library of React UI components and low-level hooks.",
@@ -75,7 +75,7 @@
75
75
  "tailwindcss": "^4.0.15"
76
76
  },
77
77
  "peerDependencies": {
78
- "@floating-ui/react": "^0.25.4",
78
+ "@floating-ui/react": "^0.27.0",
79
79
  "@nextui-org/accordion": "^2.2.0",
80
80
  "@nextui-org/badge": "^2.2.0",
81
81
  "@nextui-org/pagination": "^2.2.0",