@uselets/ui 1.20.4 → 2.0.0

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 (252) hide show
  1. package/README.md +309 -426
  2. package/dist/as-child-Dxg3Xo1G.d.ts +41 -0
  3. package/dist/cli/uselets.mjs +5 -4
  4. package/dist/cli/uselets.mjs.map +1 -1
  5. package/dist/components/accordion.d.ts +12 -8
  6. package/dist/components/accordion.js +1 -1
  7. package/dist/components/accordion.js.map +1 -1
  8. package/dist/components/alert-dialog.d.ts +20 -15
  9. package/dist/components/alert-dialog.js +1 -1
  10. package/dist/components/alert-dialog.js.map +1 -1
  11. package/dist/components/alert.d.ts +1 -1
  12. package/dist/components/anchor.d.ts +3 -5
  13. package/dist/components/anchor.js +1 -1
  14. package/dist/components/anchor.js.map +1 -1
  15. package/dist/components/area-chart.d.ts +54 -0
  16. package/dist/components/area-chart.js +1 -1
  17. package/dist/components/area-chart.js.map +1 -1
  18. package/dist/components/avatar.d.ts +12 -7
  19. package/dist/components/avatar.js +1 -1
  20. package/dist/components/avatar.js.map +1 -1
  21. package/dist/components/badge.d.ts +3 -5
  22. package/dist/components/badge.js +1 -1
  23. package/dist/components/badge.js.map +1 -1
  24. package/dist/components/bar-chart.d.ts +48 -0
  25. package/dist/components/bar-chart.js +1 -1
  26. package/dist/components/bar-chart.js.map +1 -1
  27. package/dist/components/bottom-nav.d.ts +7 -3
  28. package/dist/components/bottom-nav.js +1 -1
  29. package/dist/components/bottom-nav.js.map +1 -1
  30. package/dist/components/breadcrumb.d.ts +3 -4
  31. package/dist/components/breadcrumb.js +1 -1
  32. package/dist/components/breadcrumb.js.map +1 -1
  33. package/dist/components/button-group.d.ts +3 -4
  34. package/dist/components/button-group.js +1 -1
  35. package/dist/components/button-group.js.map +1 -1
  36. package/dist/components/button.d.ts +8 -4
  37. package/dist/components/button.js +1 -1
  38. package/dist/components/button.js.map +1 -1
  39. package/dist/components/calendar.d.ts +4 -0
  40. package/dist/components/calendar.js +1 -1
  41. package/dist/components/calendar.js.map +1 -1
  42. package/dist/components/card.d.ts +4 -0
  43. package/dist/components/card.js.map +1 -1
  44. package/dist/components/carousel.d.ts +11 -3
  45. package/dist/components/carousel.js +1 -1
  46. package/dist/components/carousel.js.map +1 -1
  47. package/dist/components/center.d.ts +1 -1
  48. package/dist/components/chart.js +4 -4
  49. package/dist/components/checkbox.d.ts +14 -4
  50. package/dist/components/checkbox.js +1 -1
  51. package/dist/components/checkbox.js.map +1 -1
  52. package/dist/components/collapsible.d.ts +8 -7
  53. package/dist/components/collapsible.js +1 -1
  54. package/dist/components/collapsible.js.map +1 -1
  55. package/dist/components/combobox.d.ts +48 -26
  56. package/dist/components/combobox.js +1 -1
  57. package/dist/components/combobox.js.map +1 -1
  58. package/dist/components/command.d.ts +16 -2
  59. package/dist/components/command.js +1 -1
  60. package/dist/components/command.js.map +1 -1
  61. package/dist/components/container.d.ts +1 -1
  62. package/dist/components/data-table.d.ts +39 -0
  63. package/dist/components/data-table.js +1 -1
  64. package/dist/components/data-table.js.map +1 -1
  65. package/dist/components/date-range-picker.d.ts +24 -2
  66. package/dist/components/date-range-picker.js +1 -1
  67. package/dist/components/date-range-picker.js.map +1 -1
  68. package/dist/components/dialog.d.ts +33 -15
  69. package/dist/components/dialog.js +1 -1
  70. package/dist/components/dialog.js.map +1 -1
  71. package/dist/components/direction.d.ts +7 -0
  72. package/dist/components/direction.js +1 -0
  73. package/dist/components/donut-chart.d.ts +12 -0
  74. package/dist/components/donut-chart.js +1 -1
  75. package/dist/components/donut-chart.js.map +1 -1
  76. package/dist/components/drawer.d.ts +33 -15
  77. package/dist/components/drawer.js +1 -1
  78. package/dist/components/drawer.js.map +1 -1
  79. package/dist/components/dropdown-menu.d.ts +36 -26
  80. package/dist/components/dropdown-menu.js +1 -1
  81. package/dist/components/dropdown-menu.js.map +1 -1
  82. package/dist/components/dropzone.d.ts +19 -0
  83. package/dist/components/dropzone.js +1 -1
  84. package/dist/components/dropzone.js.map +1 -1
  85. package/dist/components/field.d.ts +16 -4
  86. package/dist/components/field.js +1 -1
  87. package/dist/components/field.js.map +1 -1
  88. package/dist/components/flex.d.ts +1 -1
  89. package/dist/components/grid.d.ts +13 -0
  90. package/dist/components/grid.js.map +1 -1
  91. package/dist/components/group.d.ts +1 -1
  92. package/dist/components/heading.d.ts +3 -5
  93. package/dist/components/heading.js +1 -1
  94. package/dist/components/heading.js.map +1 -1
  95. package/dist/components/hover-card.d.ts +7 -6
  96. package/dist/components/hover-card.js +1 -1
  97. package/dist/components/hover-card.js.map +1 -1
  98. package/dist/components/input-group.d.ts +2 -2
  99. package/dist/components/input-group.js +1 -1
  100. package/dist/components/input-group.js.map +1 -1
  101. package/dist/components/input-otp.d.ts +4 -2
  102. package/dist/components/input-otp.js +1 -1
  103. package/dist/components/input-otp.js.map +1 -1
  104. package/dist/components/input.d.ts +1 -1
  105. package/dist/components/input.js +1 -1
  106. package/dist/components/input.js.map +1 -1
  107. package/dist/components/label.d.ts +3 -4
  108. package/dist/components/label.js +1 -1
  109. package/dist/components/label.js.map +1 -1
  110. package/dist/components/list.d.ts +8 -5
  111. package/dist/components/list.js +1 -1
  112. package/dist/components/list.js.map +1 -1
  113. package/dist/components/logo.d.ts +113 -0
  114. package/dist/components/marker.d.ts +4 -5
  115. package/dist/components/marker.js +1 -1
  116. package/dist/components/marker.js.map +1 -1
  117. package/dist/components/mask-input.d.ts +6 -0
  118. package/dist/components/mask-input.js.map +1 -1
  119. package/dist/components/native-select.d.ts +5 -1
  120. package/dist/components/native-select.js +1 -1
  121. package/dist/components/native-select.js.map +1 -1
  122. package/dist/components/navigation-menu.d.ts +19 -20
  123. package/dist/components/navigation-menu.js +1 -1
  124. package/dist/components/navigation-menu.js.map +1 -1
  125. package/dist/components/number-field.d.ts +18 -13
  126. package/dist/components/number-field.js +1 -1
  127. package/dist/components/number-field.js.map +1 -1
  128. package/dist/components/pagination.d.ts +9 -0
  129. package/dist/components/pagination.js +1 -1
  130. package/dist/components/pagination.js.map +1 -1
  131. package/dist/components/phone-input.d.ts +3 -0
  132. package/dist/components/phone-input.js +1 -1
  133. package/dist/components/phone-input.js.map +1 -1
  134. package/dist/components/pie-chart.d.ts +53 -0
  135. package/dist/components/pie-chart.js +1 -1
  136. package/dist/components/pie-chart.js.map +1 -1
  137. package/dist/components/popover.d.ts +13 -12
  138. package/dist/components/popover.js +1 -1
  139. package/dist/components/popover.js.map +1 -1
  140. package/dist/components/progress.d.ts +9 -3
  141. package/dist/components/progress.js +1 -1
  142. package/dist/components/progress.js.map +1 -1
  143. package/dist/components/radio-group.d.ts +20 -6
  144. package/dist/components/radio-group.js +1 -1
  145. package/dist/components/radio-group.js.map +1 -1
  146. package/dist/components/resizable.d.ts +1 -0
  147. package/dist/components/resizable.js.map +1 -1
  148. package/dist/components/ring-progress.d.ts +9 -1
  149. package/dist/components/ring-progress.js.map +1 -1
  150. package/dist/components/scroll-area.d.ts +3 -3
  151. package/dist/components/scroll-area.js +1 -1
  152. package/dist/components/scroll-area.js.map +1 -1
  153. package/dist/components/select.d.ts +25 -20
  154. package/dist/components/select.js +1 -1
  155. package/dist/components/select.js.map +1 -1
  156. package/dist/components/separator.d.ts +9 -4
  157. package/dist/components/separator.js +1 -1
  158. package/dist/components/separator.js.map +1 -1
  159. package/dist/components/sidebar.d.ts +63 -18
  160. package/dist/components/sidebar.js +1 -1
  161. package/dist/components/sidebar.js.map +1 -1
  162. package/dist/components/simple-grid.d.ts +7 -0
  163. package/dist/components/simple-grid.js +1 -1
  164. package/dist/components/simple-grid.js.map +1 -1
  165. package/dist/components/skeleton.d.ts +4 -0
  166. package/dist/components/skeleton.js.map +1 -1
  167. package/dist/components/slider.d.ts +2 -2
  168. package/dist/components/slider.js +1 -1
  169. package/dist/components/slider.js.map +1 -1
  170. package/dist/components/spinner.d.ts +1 -1
  171. package/dist/components/stack.d.ts +1 -1
  172. package/dist/components/stepper.d.ts +46 -25
  173. package/dist/components/stepper.js +1 -1
  174. package/dist/components/stepper.js.map +1 -1
  175. package/dist/components/switch.d.ts +17 -3
  176. package/dist/components/switch.js +1 -1
  177. package/dist/components/switch.js.map +1 -1
  178. package/dist/components/table.d.ts +10 -1
  179. package/dist/components/table.js +1 -1
  180. package/dist/components/table.js.map +1 -1
  181. package/dist/components/tabs.d.ts +14 -10
  182. package/dist/components/tabs.js +1 -1
  183. package/dist/components/tabs.js.map +1 -1
  184. package/dist/components/text.d.ts +3 -5
  185. package/dist/components/text.js +1 -1
  186. package/dist/components/text.js.map +1 -1
  187. package/dist/components/textarea.d.ts +1 -1
  188. package/dist/components/textarea.js +1 -1
  189. package/dist/components/textarea.js.map +1 -1
  190. package/dist/components/theme.d.ts +12 -0
  191. package/dist/components/theme.js +1 -1
  192. package/dist/components/theme.js.map +1 -1
  193. package/dist/components/timeline.d.ts +25 -1
  194. package/dist/components/timeline.js +1 -1
  195. package/dist/components/timeline.js.map +1 -1
  196. package/dist/components/toggle-group.d.ts +16 -6
  197. package/dist/components/toggle-group.js +1 -1
  198. package/dist/components/toggle-group.js.map +1 -1
  199. package/dist/components/toggle.d.ts +4 -4
  200. package/dist/components/toggle.js +1 -1
  201. package/dist/components/toggle.js.map +1 -1
  202. package/dist/components/tooltip.d.ts +15 -8
  203. package/dist/components/tooltip.js +1 -1
  204. package/dist/components/tooltip.js.map +1 -1
  205. package/dist/components/unstyled-button.d.ts +3 -5
  206. package/dist/components/unstyled-button.js +1 -1
  207. package/dist/components/unstyled-button.js.map +1 -1
  208. package/dist/components/visibility.d.ts +2 -0
  209. package/dist/components/visibility.js +1 -1
  210. package/dist/components/visibility.js.map +1 -1
  211. package/dist/hooks/use-breakpoint-value.d.ts +1 -0
  212. package/dist/hooks/use-breakpoint-value.js.map +1 -1
  213. package/dist/hooks/use-breakpoint.d.ts +1 -0
  214. package/dist/hooks/use-breakpoint.js.map +1 -1
  215. package/dist/hooks/use-disclosure.d.ts +3 -0
  216. package/dist/hooks/use-disclosure.js +1 -1
  217. package/dist/hooks/use-disclosure.js.map +1 -1
  218. package/dist/hooks/use-document-visibility.d.ts +1 -0
  219. package/dist/hooks/use-document-visibility.js.map +1 -1
  220. package/dist/hooks/use-hotkeys.d.ts +1 -1
  221. package/dist/hooks/use-idle.d.ts +9 -0
  222. package/dist/hooks/use-idle.js.map +1 -1
  223. package/dist/hooks/use-interval.d.ts +1 -0
  224. package/dist/hooks/use-interval.js.map +1 -1
  225. package/dist/hooks/use-local-storage.d.ts +6 -0
  226. package/dist/hooks/use-local-storage.js +1 -1
  227. package/dist/hooks/use-local-storage.js.map +1 -1
  228. package/dist/hooks/use-mask.d.ts +1 -0
  229. package/dist/hooks/use-mask.js.map +1 -1
  230. package/dist/hooks/use-mobile.d.ts +1 -0
  231. package/dist/hooks/use-mobile.js.map +1 -1
  232. package/dist/index.d.ts +7 -5
  233. package/dist/index.js +1 -1
  234. package/dist/lib/as-child.d.ts +2 -0
  235. package/dist/lib/as-child.js +2 -0
  236. package/dist/lib/as-child.js.map +1 -0
  237. package/dist/lib/utils.d.ts +1 -1
  238. package/dist/logo--R3_XDgA.js.map +1 -1
  239. package/dist/resolve-class-name-BurP8ngI.js +2 -0
  240. package/dist/resolve-class-name-BurP8ngI.js.map +1 -0
  241. package/dist/skills/uselets-ui/SKILL.md +8 -3
  242. package/dist/skills/uselets-ui/references/component-catalog.md +31 -16
  243. package/dist/skills/uselets-ui/references/migration-v1-to-v2.md +599 -0
  244. package/dist/skills/uselets-ui/references/package-guide.md +7 -7
  245. package/dist/styles/base.css +21 -0
  246. package/dist/{use-hotkeys-DOjcX9Tt.d.ts → use-hotkeys-D1_TEIwN.d.ts} +2 -1
  247. package/dist/use-hotkeys-YDJmGWX6.js.map +1 -1
  248. package/package.json +16 -17
  249. package/skills/uselets-ui/SKILL.md +8 -3
  250. package/skills/uselets-ui/references/component-catalog.md +31 -16
  251. package/skills/uselets-ui/references/migration-v1-to-v2.md +599 -0
  252. package/skills/uselets-ui/references/package-guide.md +7 -7
@@ -0,0 +1,599 @@
1
+ # Migrating `@uselets/ui` v1 → v2
2
+
3
+ v2 replaces `radix-ui` with Base UI (`@base-ui/react`) under every interactive component. Visual design and
4
+ runtime behavior are intended to stay the same; what changes is the **component API** wherever Radix and Base UI
5
+ differ, plus three intentional visual changes: the `Switch`/`Checkbox`/`RadioGroupItem` size scale (section 14),
6
+ the `Dialog` default size (section 17), and invalid `Checkbox`/`Switch`/`RadioGroupItem` no longer turning red
7
+ (section 19).
8
+
9
+ Use this guide when an app upgrades to `@uselets/ui@2`, or when you see v1-only APIs (the "Detect" patterns
10
+ below) in code that imports from `@uselets/ui`.
11
+
12
+ ## Contents
13
+
14
+ 1. [How to run the migration](#how-to-run-the-migration)
15
+ 2. [Polymorphism: `asChild` only](#1-polymorphism-aschild-only)
16
+ 3. [Change callbacks receive `eventDetails`](#2-change-callbacks-receive-eventdetails)
17
+ 4. [Accordion](#3-accordion)
18
+ 5. [ToggleGroup](#4-togglegroup)
19
+ 6. [Checkbox indeterminate](#5-checkbox-indeterminate)
20
+ 7. [Tooltip](#6-tooltip)
21
+ 8. [Select](#7-select)
22
+ 9. [Popover](#8-popover)
23
+ 10. [NavigationMenu](#9-navigationmenu)
24
+ 11. [DropdownMenu](#10-dropdownmenu)
25
+ 12. [HoverCard](#11-hovercard)
26
+ 13. [Progress](#12-progress)
27
+ 14. [Slider](#13-slider)
28
+ 15. [Switch, Checkbox and RadioGroupItem sizes](#14-switch-checkbox-and-radiogroupitem-sizes)
29
+ 16. [Styling against component state (CSS selectors and variables)](#15-styling-against-component-state)
30
+ 17. [Tests](#16-tests)
31
+ 18. [Dialog default size](#17-dialog-default-size)
32
+ 19. [Removed and renamed props](#18-removed-and-renamed-props)
33
+ 20. [Other behavior changes](#19-other-behavior-changes)
34
+ 21. [Unchanged APIs](#unchanged-apis)
35
+
36
+ ## How to run the migration
37
+
38
+ 1. Bump `@uselets/ui` to `^2`. Remove any direct `radix-ui` / `@radix-ui/*` dependency the app only had for
39
+ `@uselets/ui` (the package no longer depends on it).
40
+ 2. Run the app's typecheck first. Most breaking changes are prop renames or type changes, so `tsc` points at
41
+ almost every call site. Fix those using the sections below.
42
+ 3. Then grep for the patterns listed under each **Detect** heading — some changes still type-check but behave
43
+ differently (for example `SelectValue` showing the raw value, `data-[state=...]` selectors silently no longer
44
+ matching, or tests that open menus with `pointerDown`).
45
+ 4. Run the app's tests and check the affected screens visually (popovers, menus, dialogs, selects, switches).
46
+
47
+ Do the mechanical rewrites exactly as shown; don't re-architect the surrounding code while migrating.
48
+
49
+ ## 1. Polymorphism: `asChild` only
50
+
51
+ `asChild` is supported on triggers, closes, items, links, labels, titles/descriptions, contents/popups, overlays,
52
+ `Label`, `Toggle`, `ToggleGroupItem`, `TabsTrigger`, `AccordionTrigger`, `SelectTrigger`, `NavigationMenuTrigger`,
53
+ `AlertDialogAction`/`AlertDialogCancel`, and so on. Semantics: pass exactly one child element; the part's props,
54
+ classes, event handlers and ref are merged into it, and the child replaces the part's content.
55
+ `AlertDialogAction`/`AlertDialogCancel` with `asChild` keep the `Button` variant/color/size styles on the child.
56
+
57
+ Parts whose internal structure is required for them to work no longer accept `asChild`: `ScrollArea`, `ScrollBar`, `SelectContent`, `SelectItem`, `AccordionContent`, `Checkbox`, `Switch`, `RadioGroupItem`, `Progress`, `Slider`, `NavigationMenu` and `NumberFieldScrubArea`. In v1
58
+ these accepted `asChild` through Radix but broke or ignored the child when it was used. Render the part normally
59
+ and put your element inside it (or around it) instead.
60
+
61
+ - **Detect:** `asChild` on any of the parts above.
62
+
63
+ Base UI's own `render` prop is **not** part of the public API — don't introduce it. This now includes `Combobox`
64
+ and `NumberField`, which were already Base UI-based in v1 and accepted `render`: their parts take `asChild`
65
+ instead (`ComboboxTrigger`, `ComboboxItem`, `ComboboxList`, `ComboboxGroup`, `ComboboxLabel`, `ComboboxEmpty`,
66
+ `ComboboxSeparator`, `ComboboxContent`, `ComboboxChips`, `ComboboxChip`, `NumberFieldGroup`,
67
+ `NumberFieldIncrement`, `NumberFieldDecrement`). `ComboboxInput`, `ComboboxClear`, `ComboboxChipsInput`,
68
+ `NumberField`, `NumberFieldInput` and `NumberFieldScrubArea` accept neither.
69
+
70
+ ```tsx
71
+ // v1
72
+ <ComboboxTrigger render={<Button variant="outline" />} />
73
+ // v2
74
+ <ComboboxTrigger asChild>
75
+ <Button variant="outline">…</Button>
76
+ </ComboboxTrigger>
77
+ ```
78
+
79
+ - **Detect:** `render=` on any `@uselets/ui` component.
80
+
81
+ New gotcha: parts that render a native `<button>` (`DialogTrigger`, `PopoverTrigger`, `DropdownMenuTrigger`,
82
+ `CollapsibleTrigger`, `AlertDialogTrigger`, `DrawerTrigger`, `DialogClose`, `TabsTrigger`, `AccordionTrigger`,
83
+ `Toggle`, `ToggleGroupItem`, `SelectTrigger`, `AlertDialogAction`/`AlertDialogCancel`, …) assume their `asChild`
84
+ child is a `<button>`. If the child is something else (`<a>`, `<div>`, a router `Link`), also pass
85
+ `nativeButton={false}` so the part adds the right keyboard/ARIA behavior. `TooltipTrigger` and `HoverCardTrigger`
86
+ don't need it.
87
+
88
+ ```tsx
89
+ // v2
90
+ <PopoverTrigger asChild nativeButton={false}>
91
+ <a href="#details">Details</a>
92
+ </PopoverTrigger>
93
+ ```
94
+
95
+ - **Detect:** `asChild` on a part whose child is not a `<button>`/`Button`/`UnstyledButton`.
96
+
97
+ The `asChild` child must be the element that should receive the part's props. A wrapper component that doesn't
98
+ forward its props to the element it renders (for example `Tooltip`, which passes extra props to its popup) swallows
99
+ the trigger's handlers and ref, so the part never opens. Nest the wrapper outside the part instead:
100
+
101
+ ```tsx
102
+ // v1
103
+ <DialogTrigger asChild>
104
+ <Tooltip label="Test data">
105
+ <Button size="icon">…</Button>
106
+ </Tooltip>
107
+ </DialogTrigger>
108
+
109
+ // v2
110
+ <Tooltip label="Test data">
111
+ <DialogTrigger asChild>
112
+ <Button size="icon">…</Button>
113
+ </DialogTrigger>
114
+ </Tooltip>
115
+ ```
116
+
117
+ - **Detect:** `asChild` whose child is `Tooltip` or another wrapper component instead of the clickable element.
118
+
119
+ ## 2. Change callbacks receive `eventDetails`
120
+
121
+ `onOpenChange`, `onValueChange`, `onCheckedChange`, `onPressedChange`, etc. of Base UI-backed components now
122
+ receive `(value, eventDetails)`. Package-level callbacks that aren't Base UI parts (`DateRangePicker`,
123
+ `SidebarProvider`, `Stepper`) keep their v1 signatures. Passing a state setter directly still works when the
124
+ state already has the v2 value type; the extra argument is ignored. It no longer type-checks where the value type
125
+ changed: `Select` (`string | null`, section 7), `Accordion` and `ToggleGroup` (`string[]`, sections 3 and 4) and
126
+ `Slider` (`number | readonly number[]`, section 13).
127
+
128
+ ```tsx
129
+ const [reason, setReason] = useState('')
130
+ // v1
131
+ <Select value={reason} onValueChange={setReason}>…</Select>
132
+ // v2
133
+ <Select value={reason} onValueChange={(value) => value !== null && setReason(value)}>…</Select>
134
+ ```
135
+
136
+ Otherwise, only code that relies on the argument count, or tests asserting exact arguments, need updates:
137
+
138
+ ```ts
139
+ // v1
140
+ expect(onValueChange).toHaveBeenCalledWith('banana')
141
+ // v2
142
+ expect(onValueChange).toHaveBeenCalledWith('banana', expect.anything())
143
+ ```
144
+
145
+ `eventDetails.reason` (`'escape-key'`, `'outside-press'`, `'trigger-press'`, …) and `eventDetails.cancel()` let
146
+ you veto a change. You rarely need this: `Dialog`/`Drawer` still have `dismissible={false}`. To replace a v1
147
+ `onInteractOutside={(event) => event.preventDefault()}`, cancel both outside reasons:
148
+
149
+ ```tsx
150
+ <Popover
151
+ onOpenChange={(open, eventDetails) => {
152
+ if (!open && ['outside-press', 'focus-out'].includes(eventDetails.reason)) eventDetails.cancel()
153
+ }}
154
+ >
155
+ ```
156
+
157
+ ## 3. Accordion
158
+
159
+ `type="single" | "multiple"` and `collapsible` are gone. The accordion is single-item by default (items can
160
+ always be collapsed). Set `multiple` to allow several open items. `value`/`defaultValue` are **always arrays**.
161
+
162
+ ```tsx
163
+ // v1
164
+ <Accordion type="single" collapsible defaultValue="item-1">…</Accordion>
165
+ <Accordion type="multiple" defaultValue={['a', 'b']}>…</Accordion>
166
+
167
+ // v2
168
+ <Accordion defaultValue={['item-1']}>…</Accordion>
169
+ <Accordion multiple defaultValue={['a', 'b']}>…</Accordion>
170
+ ```
171
+
172
+ - **Detect:** `<Accordion` with `type=` or `collapsible`; a string `value`/`defaultValue`.
173
+ - A disabled `AccordionTrigger` now uses `aria-disabled="true"` instead of the native `disabled` attribute.
174
+ - Arrow keys, `Home` and `End` no longer move focus between triggers; `Tab` moves through them like regular
175
+ buttons. `orientation` and `loopFocus` still type-check but are deprecated by Base UI and do nothing: remove them.
176
+ - **Detect:** `orientation=`, `loop` or `loopFocus` on `<Accordion`.
177
+
178
+ ## 4. ToggleGroup
179
+
180
+ Same shape as Accordion: no `type`. Single selection is the default, and `multiple` enables multi-selection.
181
+ `value`/`defaultValue`/`onValueChange` always use `string[]`.
182
+
183
+ ```tsx
184
+ // v1
185
+ const [view, setView] = useState('board')
186
+ <ToggleGroup type="single" value={view} onValueChange={setView}>…</ToggleGroup>
187
+
188
+ // v2
189
+ const [view, setView] = useState(['board'])
190
+ <ToggleGroup value={view} onValueChange={setView}>…</ToggleGroup>
191
+ ```
192
+
193
+ - **Detect:** `<ToggleGroup` with `type=`; string state passed to `value`.
194
+ - Items are now toggle buttons (`aria-pressed`, `data-pressed`). They no longer use `role="radio"`/`aria-checked`
195
+ or `data-state="on"`.
196
+
197
+ ## 5. Checkbox indeterminate
198
+
199
+ `checked="indeterminate"` / `defaultChecked="indeterminate"` is no longer accepted. Use the boolean
200
+ `indeterminate` prop next to a boolean `checked`. `onCheckedChange` always receives a boolean, so drop
201
+ `Boolean(value)` / `value === true` coercions.
202
+
203
+ ```tsx
204
+ // v1
205
+ <Checkbox
206
+ checked={allSelected || (someSelected && 'indeterminate')}
207
+ onCheckedChange={(value) => toggleAll(Boolean(value))}
208
+ />
209
+
210
+ // v2
211
+ <Checkbox
212
+ checked={allSelected}
213
+ indeterminate={someSelected && !allSelected}
214
+ onCheckedChange={(value) => toggleAll(value)}
215
+ />
216
+ ```
217
+
218
+ - **Detect:** `'indeterminate'` string literal near `Checkbox`; `data-state="indeterminate"`.
219
+
220
+ ## 6. Tooltip
221
+
222
+ | v1 | v2 |
223
+ | --- | --- |
224
+ | `<TooltipProvider delayDuration={n}>` | `<TooltipProvider delay={n}>` (also `closeDelay`, `timeout`) |
225
+ | `<Tooltip delayDuration={n}>` | `<Tooltip delay={n} closeDelay={m}>` (forwarded to the trigger) |
226
+ | `<Tooltip disableHoverableContent>` | `<Tooltip disableHoverablePopup>` |
227
+ | `<TooltipRoot delayDuration={n}>` | put `delay` on `TooltipTrigger` instead |
228
+
229
+ `Tooltip label`, `disabled`, `TooltipRoot`, `TooltipTrigger asChild`, and `TooltipContent`
230
+ (`side`/`sideOffset`/`align`/`alignOffset`) are unchanged.
231
+
232
+ - **Detect:** `delayDuration`, `disableHoverableContent`.
233
+
234
+ ## 7. Select
235
+
236
+ - `SelectValue` renders the **raw value** unless `Select` receives `items`. Whenever an item's label differs from
237
+ its value, pass `items` (array of `{ value, label }` or a `value → label` record). If value and label are
238
+ identical (for example page sizes `'10'`, `'20'`), nothing to do. Neither `tsc` nor tests flag a missing
239
+ `items`: the trigger just shows `READY_FOR_PICKUP` instead of `Ready for pickup`. Review **every** `<Select`.
240
+ - `SelectContent`'s `position="item-aligned" | "popper"` → `alignItemWithTrigger` (default `true`, the old
241
+ item-aligned behavior). `position="popper"` → `alignItemWithTrigger={false}`.
242
+ - `onValueChange` may receive `null`. Guard before converting: `value !== null && setPageSize(Number(value))`.
243
+
244
+ ```tsx
245
+ // v1
246
+ <Select defaultValue="design-system">
247
+ …<SelectItem value="design-system">Design System</SelectItem>…
248
+ </Select>
249
+
250
+ // v2: keep one { value, label } list and use it for both `items` and the rendered items
251
+ const categories = [{ value: 'design-system', label: 'Design System' }]
252
+ <Select items={categories} defaultValue="design-system">
253
+ <SelectTrigger>
254
+ <SelectValue />
255
+ </SelectTrigger>
256
+ <SelectContent>
257
+ {categories.map((category) => (
258
+ <SelectItem key={category.value} value={category.value}>
259
+ {category.label}
260
+ </SelectItem>
261
+ ))}
262
+ </SelectContent>
263
+ </Select>
264
+ ```
265
+
266
+ When the list is built inline or the label is derived (`{item.displayName}`, `{format(item)}`), map it to
267
+ `{ value, label }` for `items` with the same expression used in the `SelectItem`.
268
+
269
+ - **Detect:** `<Select` without `items` whose `SelectItem` content is anything other than the literal value
270
+ (`{item.label}`, `{option.name}`, a hard-coded label); `position=` on `SelectContent`; `Number(value)` / string
271
+ methods on `onValueChange` values.
272
+
273
+ ## 8. Popover
274
+
275
+ `PopoverAnchor` was removed. To position against something other than the trigger, pass `anchor` (element,
276
+ ref, or virtual element) to `PopoverContent`.
277
+
278
+ ```tsx
279
+ // v1
280
+ <Popover>
281
+ <PopoverAnchor><div ref={…}>Anchor</div></PopoverAnchor>
282
+ <PopoverTrigger>Open</PopoverTrigger>
283
+ <PopoverContent>…</PopoverContent>
284
+ </Popover>
285
+
286
+ // v2
287
+ const anchorRef = useRef<HTMLDivElement>(null)
288
+ <div ref={anchorRef}>Anchor</div>
289
+ <Popover>
290
+ <PopoverTrigger>Open</PopoverTrigger>
291
+ <PopoverContent anchor={anchorRef}>…</PopoverContent>
292
+ </Popover>
293
+ ```
294
+
295
+ `PopoverTitle`/`PopoverDescription` now wire `aria-labelledby`/`aria-describedby` on the popup automatically.
296
+
297
+ `modal` only traps focus when a `PopoverClose` is rendered inside `PopoverContent`. Without one, it still blocks
298
+ outside clicks and page scroll, but `Tab` moves focus out of the popup. Add a `PopoverClose` (visually hidden if the
299
+ design has no close button):
300
+
301
+ ```tsx
302
+ <Popover modal>
303
+ <PopoverTrigger>Open</PopoverTrigger>
304
+ <PopoverContent>
305
+ …
306
+ <PopoverClose className="sr-only">Close</PopoverClose>
307
+ </PopoverContent>
308
+ </Popover>
309
+ ```
310
+
311
+ - **Detect:** `PopoverAnchor`; `<Popover modal` whose content has no `PopoverClose`.
312
+
313
+ ## 9. NavigationMenu
314
+
315
+ - The `viewport` prop and the `NavigationMenuViewport` export were removed. Content always renders through the
316
+ new `NavigationMenuPositioner` (portaled, added automatically by `NavigationMenu`). `NavigationMenu` accepts
317
+ `align` and forwards it to the positioner.
318
+ - `NavigationMenuIndicator` must be placed **inside** a `NavigationMenuItem` (next to its trigger). It throws
319
+ when rendered directly in the list.
320
+ - `NavigationMenuLink` keeps `active` (`data-active` + `aria-current="page"`) and supports `asChild` for router
321
+ links.
322
+ - `NavigationMenuLink` no longer closes the menu when clicked (`closeOnClick` defaults to `false`). With full page
323
+ loads nothing changes, but in client-side routing the popup stays open over the new page: pass `closeOnClick`.
324
+ `onSelect` still type-checks (it is the DOM text-selection event) but never fires on click: move its logic to
325
+ `onClick`, and drop `onSelect={(event) => event.preventDefault()}` (the menu already stays open).
326
+
327
+ ```tsx
328
+ // v1
329
+ <NavigationMenuLink asChild onSelect={() => track('nav')}>
330
+ <Link href="/orders">Orders</Link>
331
+ </NavigationMenuLink>
332
+
333
+ // v2
334
+ <NavigationMenuLink asChild closeOnClick onClick={() => track('nav')}>
335
+ <Link href="/orders">Orders</Link>
336
+ </NavigationMenuLink>
337
+ ```
338
+
339
+ - Hover timing: `delayDuration` (default 200ms) → `delay` (default 50ms); `skipDelayDuration` was removed;
340
+ `closeDelay` (default 50ms) is new. Pass `delay={200}` to keep the v1 opening delay.
341
+ - The closed `value` is `null`, not `''`, and `onValueChange` receives `null` when the menu closes. Any non-`null`
342
+ value counts as open, so a controlled `value=""` keeps the menu open:
343
+
344
+ ```tsx
345
+ // v1
346
+ const [value, setValue] = useState('')
347
+ <NavigationMenu value={value} onValueChange={setValue}>…</NavigationMenu>
348
+ setValue('') // close
349
+
350
+ // v2
351
+ const [value, setValue] = useState<string | null>(null)
352
+ <NavigationMenu value={value} onValueChange={setValue}>…</NavigationMenu>
353
+ setValue(null) // close
354
+ ```
355
+
356
+ - **Detect:** `NavigationMenuViewport`, `viewport={`, `NavigationMenuIndicator` outside an item; every
357
+ `NavigationMenuLink` used with client-side routing; `onSelect=` on `NavigationMenuLink`; `delayDuration` /
358
+ `skipDelayDuration` on `NavigationMenu`; a controlled `NavigationMenu` `value` that uses `''` for closed.
359
+
360
+ ## 10. DropdownMenu
361
+
362
+ - `DropdownMenuItem` has no `onSelect`. Use `onClick`. The menu still closes after the click.
363
+ - `DropdownMenuCheckboxItem`/`DropdownMenuRadioItem` now keep the menu open when toggled (v1 closed it). Drop
364
+ any v1 `onSelect={(event) => event.preventDefault()}` used to keep it open; pass `closeOnClick` where the menu
365
+ should close after toggling.
366
+ - `DropdownMenuSub` / `DropdownMenuSubTrigger` / `DropdownMenuSubContent` keep their names.
367
+ - `DropdownMenuLabel` works inside or outside `DropdownMenuGroup`/`DropdownMenuRadioGroup`. Inside a group it
368
+ also labels the group for assistive tech.
369
+ - `DropdownMenuContent` keeps `side`, `sideOffset`, `align`, `alignOffset`.
370
+
371
+ ```tsx
372
+ // v1
373
+ <DropdownMenuItem onSelect={() => logout()}>Sair</DropdownMenuItem>
374
+ // v2
375
+ <DropdownMenuItem onClick={() => logout()}>Sair</DropdownMenuItem>
376
+ ```
377
+
378
+ - **Detect:** `onSelect=` on `DropdownMenuItem`/`CheckboxItem`/`RadioItem`.
379
+
380
+ ## 11. HoverCard
381
+
382
+ Open/close timing moved from the root to the trigger: `<HoverCard openDelay closeDelay>` →
383
+ `<HoverCardTrigger delay closeDelay>`. The defaults stay at the v1 timing: `delay` `700`, `closeDelay` `300`.
384
+
385
+ - **Detect:** `openDelay` / `closeDelay` on `<HoverCard`.
386
+
387
+ ## 12. Progress
388
+
389
+ Omitting `value` or passing `null` now renders the **indeterminate** state (`data-indeterminate`, no width on the
390
+ indicator), matching Radix semantics. v1 silently treated it as `0`. Pass `value={0}` if you want an empty
391
+ determinate bar.
392
+
393
+ ## 13. Slider
394
+
395
+ `onValueChange` receives `number | readonly number[]` (an array when `value`/`defaultValue` is an array). A
396
+ `useState<number[]>` setter no longer type-checks directly:
397
+
398
+ ```tsx
399
+ <Slider
400
+ value={value}
401
+ onValueChange={(next) => setValue(Array.isArray(next) ? [...next] : [next])}
402
+ />
403
+ ```
404
+
405
+ ## 14. Switch, Checkbox and RadioGroupItem sizes
406
+
407
+ `Switch` now uses the same height scale as `Checkbox` (14 / 16 / 20px). Every switch size moved **down one
408
+ step**:
409
+
410
+ | v1 size | rendered track | v2 size for the same look |
411
+ | --- | --- | --- |
412
+ | `sm` | 34×16px | `md` (new default) |
413
+ | `md` (default) | 42×20px | `lg` |
414
+ | `lg` | 48×22px | removed: use `lg` (42×20px) |
415
+ | — | — | new `sm` is 30×14px |
416
+
417
+ To keep a v1 screen pixel-identical: `size="sm"` → `size="md"` (or drop it), and no `size` / `size="md"` →
418
+ `size="lg"`. If the v1 code was just using defaults with no visual requirement, prefer the new default (`md`). It
419
+ now lines up with a default `Checkbox`.
420
+
421
+ Composed label text is now the same across `Switch`, `Checkbox` and `RadioGroupItem`: `sm` 12px, `md` 14px,
422
+ `lg` 18px. Before, checkbox/radio `lg` was 20px and switch labels were one step larger.
423
+
424
+ - **Detect:** `<Switch` (every usage: decide per the table above).
425
+
426
+ ## 15. Styling against component state
427
+
428
+ If the app styles `@uselets/ui` parts by state (custom `className`, global CSS, or `group-*`/`peer-*`
429
+ variants), update the selectors. `data-open`, `data-closed`, `data-checked`, `data-unchecked`, `data-active`,
430
+ `data-disabled`, `data-horizontal` and `data-vertical` Tailwind variants keep working, because the package's
431
+ `shadcn/tailwind.css` maps them, but only on parts that set those attributes:
432
+
433
+ - `data-open:` / `data-closed:` match contents, popups and panels, **not triggers**. A trigger only gets
434
+ `data-popup-open` or `data-panel-open` while open and has no closed marker, so `data-open:rotate-180` on a
435
+ trigger silently stops matching. Use `data-popup-open:` / `data-panel-open:`, or `aria-expanded:`.
436
+ - `data-checked:` matches checkboxes, radios, switches and menu checkbox/radio items, **not `SelectItem`**. The
437
+ selected `SelectItem` gets an empty `data-selected` attribute, which the `data-selected:` variant (it expects
438
+ `data-selected="true"`) does not match. Use `data-[selected]:` or `aria-selected:`.
439
+
440
+ Literal `data-[state=…]` selectors and Radix CSS variables do not keep working:
441
+
442
+ | v1 (Radix) | v2 (Base UI) |
443
+ | --- | --- |
444
+ | `data-[state=open]` on a trigger | `data-popup-open` (popover, menu, select, dialog, alert dialog, drawer, tooltip and hover card triggers), `data-panel-open` (collapsible/accordion triggers) |
445
+ | `data-[state=open]` / `[state=closed]` on content | `data-open` / `data-closed` |
446
+ | `data-[state=checked]` / `[state=unchecked]` | `data-checked` / `data-unchecked` |
447
+ | `data-[state=checked]` on `SelectItem` | `data-[selected]` |
448
+ | `data-[state=indeterminate]` | `data-indeterminate` |
449
+ | `data-[state=on]` (Toggle, ToggleGroupItem) | `data-pressed` |
450
+ | `data-[state=active]` (TabsTrigger) | `data-active` |
451
+ | `data-[state=delayed-open]` / `[state=instant-open]` (Tooltip) | `data-open`; an instant open also sets `data-instant` |
452
+ | `data-[state=active]` / `[state=inactive]` (TabsContent) | inactive panels get `data-hidden`; the active one has no marker |
453
+ | `data-[state=loading]` / `[state=complete]` (Progress) | `data-progressing` / `data-complete` |
454
+ | `data-value`, `data-max` (Progress) | removed; style from your own `value` |
455
+ | `data-disabled` (Collapsible parts) | removed; `CollapsibleTrigger` still gets the native `disabled` attribute (`disabled:`) |
456
+ | `data-orientation` (NavigationMenu, NavigationMenuList, NavigationMenuItem) | removed |
457
+ | `data-[state=visible]` / `[state=hidden]` (ScrollBar) | `data-hovering` / `data-scrolling` while shown |
458
+ | `data-motion` (NavigationMenuContent) | `data-activation-direction="left" \| "right"` with `data-starting-style` / `data-ending-style` |
459
+ | `focus:` on menu/select items | still works. `data-highlighted` is also set |
460
+ | `disabled:` on Checkbox/Radio/Switch roots | `data-disabled:` (roots are `<span>`, not native inputs) |
461
+ | `peer-disabled:` on a label next to those controls | `peer-data-disabled:` |
462
+ | `--radix-*-content-transform-origin` | `--transform-origin` |
463
+ | `--radix-*-content-available-height` | `--available-height` |
464
+ | `--radix-*-content-available-width` | `--available-width` |
465
+ | `--radix-*-trigger-width` | `--anchor-width` |
466
+ | `--radix-*-trigger-height` | `--anchor-height` |
467
+ | `--radix-accordion-content-height` / `-width` | `--accordion-panel-height` / `--accordion-panel-width` |
468
+ | `--radix-collapsible-content-height` / `-width` | `--collapsible-panel-height` / `--collapsible-panel-width` |
469
+ | `--radix-navigation-menu-viewport-width/height` | `--popup-width` / `--popup-height` |
470
+
471
+ - **Detect:** `data-open:`/`data-closed:` on a trigger className, `data-checked:` on `SelectItem`. Also
472
+ `data-[state=`, `--radix-`, `[data-state=` in CSS/TSX. Ignore matches that aren't Radix state:
473
+ `TableRow data-state="selected"` (and `data-[state=selected]` table styles) and the Sidebar's own
474
+ `data-state="expanded" | "collapsed"` are package attributes and keep working unchanged.
475
+
476
+ ## 16. Tests
477
+
478
+ Base UI reacts to different DOM events than Radix. In `@testing-library/react` with `fireEvent`:
479
+
480
+ - Menus (`DropdownMenuTrigger`, `ModeToggle`) open with `fireEvent.click(trigger)`, not
481
+ `pointerDown`/`pointerUp`.
482
+ - Select options need `fireEvent.pointerDown(option)` followed by `fireEvent.click(option)`. Open the select
483
+ with `click` or `keyDown` `ArrowDown` on the trigger (`Enter` relies on the browser's native button click,
484
+ which jsdom doesn't synthesize).
485
+ - Tabs activate on `click`, not `mouseDown`.
486
+ - `role="listbox"` lives on an inner list. Use `closest('[data-slot="select-content"]')` to reach the content
487
+ slot.
488
+ - Disabled `AccordionTrigger` and `TabsTrigger` expose `aria-disabled="true"`, not `disabled`: assert
489
+ `toHaveAttribute('aria-disabled', 'true')` instead of `toBeDisabled()`.
490
+ - Assertions on `data-state="..."` must switch to the attributes in section 15
491
+ (`hasAttribute('data-checked')`, `hasAttribute('data-pressed')`, …).
492
+ - Callback assertions need the extra `eventDetails` argument (`expect.anything()`).
493
+ - Floating parts still need a `ResizeObserver` stub in jsdom.
494
+
495
+ ## 17. Dialog default size
496
+
497
+ `Dialog` without a `size` now defaults to `md` (v1 rendered `sm`, `sm:max-w-sm`). `CommandDialog` still defaults
498
+ to `sm`. To keep a v1 dialog pixel-identical, pass `size="sm"`.
499
+
500
+ ```tsx
501
+ // v1 (implicit sm) → v2
502
+ <Dialog>…</Dialog> → <Dialog size="sm">…</Dialog>
503
+ ```
504
+
505
+ - **Detect:** `<Dialog` without `size=`.
506
+
507
+ ## 18. Removed and renamed props
508
+
509
+ | v1 (Radix) | v2 |
510
+ | --- | --- |
511
+ | `Slider onValueCommit` | `onValueCommitted` |
512
+ | `Tabs activationMode="automatic" \| "manual"` | `TabsList activateOnFocus` (default `false` = manual: arrow keys only move focus, `Enter`/`Space` or a click selects). v1 defaulted to automatic, so pass `activateOnFocus` to keep it; `activationMode="manual"` → just remove it |
513
+ | `AvatarFallback delayMs` | `delay` |
514
+ | `forceMount` on `AccordionContent` / `CollapsibleContent` / `TabsContent` / `NavigationMenuContent` | `keepMounted` |
515
+ | `forceMount` on overlay contents (`DialogContent`, `PopoverContent`, `DropdownMenuContent`, …) | `keepMounted` on the same content (forwarded to its portal). `SelectContent` has no equivalent |
516
+ | `onOpenAutoFocus` / `onCloseAutoFocus` on contents | `initialFocus` / `finalFocus` (on `DialogContent`, `DrawerContent`, `PopoverContent`, `AlertDialogContent`); `SelectContent` and `DropdownMenuContent` only have `finalFocus` |
517
+ | `onOpenAutoFocus={(event) => event.preventDefault()}` | `initialFocus={false}` |
518
+ | `forceMount` on `DialogPortal` / `AlertDialogPortal` / `DrawerPortal` / `DropdownMenuPortal` | `keepMounted` |
519
+ | `forceMount` on `DialogOverlay` / `AlertDialogOverlay` / `DrawerOverlay` | removed (the overlay follows its portal) |
520
+ | `ScrollBar forceMount` | `keepMounted` |
521
+ | `textValue` on `SelectItem`, `DropdownMenuItem` / `CheckboxItem` / `RadioItem`, `DropdownMenuSubTrigger` | `label` (typeahead text) |
522
+ | `DropdownMenuCheckboxItem checked="indeterminate"` | removed; `checked` is boolean only |
523
+ | `DropdownMenu loop` | `loopFocus` on `DropdownMenu`, default `true` (focus now wraps; v1 stopped at the ends, so pass `loopFocus={false}` to keep it) |
524
+ | `DropdownMenuContent onFocusOutside` | removed; use the root `onOpenChange` with `eventDetails.reason === 'focus-out'` |
525
+ | `loop` on `TabsList` / `ToggleGroup` | `loopFocus` (default `true`) |
526
+ | `ToggleGroup rovingFocus` | removed (roving focus is always on) |
527
+ | `Slider minStepsBetweenThumbs` | `minStepsBetweenValues` |
528
+ | `Slider inverted` | removed |
529
+ | `Progress getValueLabel(value, max)` | `getAriaValueText(formattedValue, value)` |
530
+ | `NavigationMenu delayDuration` | `delay` (default `50`; v1 default was `200`) |
531
+ | `NavigationMenu skipDelayDuration` | removed |
532
+ | `TooltipProvider skipDelayDuration` | `timeout` (default `400`; v1 default was `300`) |
533
+ | `TooltipProvider disableHoverableContent` | `disableHoverablePopup` on each `Tooltip` / `TooltipRoot` |
534
+ | `dir="rtl"` on `Tabs`, `ToggleGroup`, `RadioGroup`, `DropdownMenu`, `Slider`, `ScrollArea`, `Select` | still renders the attribute but no longer flips keyboard navigation or positioning; wrap the subtree in `<DirectionProvider direction="rtl">` from `@uselets/ui/direction` |
535
+ | `onEscapeKeyDown`, `onPointerDownOutside`, `onInteractOutside` on contents | removed; use the root `onOpenChange(open, eventDetails)` and check `eventDetails.reason`, calling `eventDetails.cancel()` to keep it open. `onEscapeKeyDown` → `'escape-key'`; `onPointerDownOutside` → `'outside-press'`; `onInteractOutside` → `'outside-press'` **and** `'focus-out'` (Radix fired it for focus leaving the popup too). For dialogs/drawers, `dismissible={false}` covers the common case |
536
+ | `onEscapeKeyDown`, `onPointerDownOutside`, `onFocusOutside`, `onInteractOutside` on `NavigationMenuContent` | removed; `NavigationMenu` has no `onOpenChange`: use its `onValueChange(value, eventDetails)` with `eventDetails.reason` (`'escape-key'`, `'outside-press'`, `'focus-out'`) and `eventDetails.cancel()` |
537
+ | `onChange` on `Switch` / `Checkbox` | `onCheckedChange` (v1 accepted `onChange` because the root was a `<button>`, but it never fired on toggle) |
538
+ | `sticky="partial" \| "always"` on popover/menu/select/tooltip/hover-card contents | `sticky?: boolean` (default `false`): `true` keeps the popup in the viewport after the trigger scrolls away (the Radix values are not accepted) |
539
+ | `collisionPadding`, `avoidCollisions`, `collisionBoundary`, `arrowPadding`, `hideWhenDetached` on those contents | not exposed |
540
+ | `ScrollArea type` / `scrollHideDelay` | removed; the scrollbar shows while hovering or scrolling |
541
+ | `PopoverAnchor`, `PopoverAnchorProps` | removed (section 8) |
542
+ | `NavigationMenuViewport`, `NavigationMenuViewportProps`, `NavigationMenu viewport` | removed (section 9) |
543
+
544
+ - **Detect:** each v1 name in the left column.
545
+
546
+ ## 19. Other behavior changes
547
+
548
+ - Anchored popups (`PopoverContent`, `SelectContent`, `DropdownMenuContent`, `TooltipContent`, `HoverCardContent`)
549
+ now keep 5px from the viewport edge when they flip or shift (v1: 0px, and 10px for `SelectContent`), and the
550
+ `Tooltip` arrow keeps 5px from the popup corners. Nothing to change in code; only screenshots of popups near the
551
+ edge differ.
552
+ - A range `Slider` (two thumbs) now pushes the other thumb when one is dragged into it. In v1 the thumbs crossed
553
+ and swapped places. Values passed to `onValueChange` stay sorted either way. Pass
554
+ `thumbCollisionBehavior="swap"` to keep the v1 behavior (or `"none"` to stop at the other thumb).
555
+ - **Detect:** `<Slider` with a two-value `value`/`defaultValue`.
556
+ - `Checkbox`, `RadioGroupItem` and `Switch` render a `<span>` root plus a hidden `<input>`. The `id` prop (and the
557
+ `Field` control id) lands on the hidden input, not on the visible element. Code that does
558
+ `document.getElementById(id)` and expects the visible control, or styles `#id`, must target
559
+ `[data-slot="checkbox"]` / `[data-slot="switch"]` / `[data-slot="radio-group-item"]` instead.
560
+ - `Progress` renders a root plus an inner track: `className` is applied to the track (the visible bar), and the
561
+ root wraps it.
562
+ - `Checkbox`, `Switch` and `RadioGroupItem` no longer draw a red border/ring for `aria-invalid` (the attribute is
563
+ still set for assistive tech). Show the error through the `Field`: `<Field invalid>` colors the label and
564
+ `FieldError` shows the message. Nothing to change in code; only screenshots of invalid checkboxes/switches differ.
565
+ - **Detect:** `aria-invalid` on `<Checkbox`, `<Switch` or `<RadioGroupItem`.
566
+ - `Field`'s `data-invalid` is now typed `boolean | 'true' | 'false'`. Passing anything else, such as a
567
+ react-hook-form `FieldError` object, no longer type-checks (in v1 it rendered `data-invalid="[object Object]"` and
568
+ the invalid styles never matched). Pass a boolean, preferably through `invalid`:
569
+ `<Field invalid={!!errors.identifier}>`.
570
+ - **Detect:** `data-invalid={` on `<Field` with a non-boolean value (`errors.x`, `fieldState.error`).
571
+
572
+ ## Unchanged APIs
573
+
574
+ No migration is needed for: `Button`, `Badge`, `Anchor`, `Heading`, `Text`, `Marker`, `List`, `UnstyledButton`,
575
+ `Breadcrumb`, `ButtonGroup`, `BottomNav`, `Sidebar` parts (`asChild` still supported everywhere it was),
576
+ `Dialog`/`Drawer` (`direction`, `dismissible`, `showCloseButton`, `overlayRef`/`overlayProps`,
577
+ `DialogBody`/`DrawerBody`; only the `Dialog` default size changed, section 17), `AlertDialog` (`size`,
578
+ `AlertDialogAction` still closes the dialog; see the note below), `Tabs` (`variant`, `grow`, string `value`; see
579
+ the note below and section 18 for `activationMode`), `Separator` (`decorative`, `orientation`), `Collapsible`,
580
+ `RadioGroup` (string `value`; see the note below),
581
+ `ScrollArea` (see section 18), `Avatar` (see section 18), `Combobox` and `NumberField` (except `render`, see section 1), and every non-interactive
582
+ component.
583
+
584
+ These keep their props but changed behavior:
585
+
586
+ - `AlertDialog`: opening focuses the first focusable element in the content, not `AlertDialogCancel`. If a link or
587
+ `AlertDialogAction` comes before the cancel button, pass a ref to the element to focus:
588
+ `<AlertDialogContent initialFocus={cancelRef}>` with `<AlertDialogCancel ref={cancelRef}>`.
589
+ - **Detect:** `<AlertDialogContent` whose first focusable child is not `AlertDialogCancel`.
590
+ - `Tabs`: without `value` or `defaultValue`, the first enabled tab is now selected on mount (v1 selected none) and
591
+ `onValueChange` fires once with `eventDetails.reason === 'initial'`. Pass `defaultValue`, or ignore that reason in
592
+ handlers with side effects (tracking, fetching).
593
+ - **Detect:** `<Tabs` with `onValueChange` but neither `value` nor `defaultValue`.
594
+ - `TabsTrigger disabled` now sets `aria-disabled="true"` instead of the native `disabled` attribute. The tab still
595
+ can't be selected (click and `Enter` do nothing), but arrow keys now stop on it instead of skipping it, so screen
596
+ reader users can discover it. Style it with `aria-disabled:` / `data-disabled:` (not `disabled:`).
597
+ - **Detect:** `disabled:` classes on `TabsTrigger`; tests asserting `toBeDisabled()` on a tab.
598
+ - `RadioGroup`: `orientation`, `loop` and `dir` are no longer supported; arrow-key focus always wraps.
599
+ - **Detect:** `orientation=`, `loop` or `dir=` on `<RadioGroup`.
@@ -21,8 +21,8 @@ If the package is not installed yet, use the bundled references as a snapshot an
21
21
 
22
22
  ## Requirements
23
23
 
24
- - React `>=19`
25
- - React DOM `>=19`
24
+ - React `^19.2`
25
+ - React DOM `^19.2`
26
26
  - Tailwind CSS 4
27
27
 
28
28
  ## Installation
@@ -234,7 +234,7 @@ Examples:
234
234
  import { Tooltip, TooltipProvider } from '@uselets/ui/tooltip'
235
235
 
236
236
  export function AppRoot({ children }: { children: React.ReactNode }) {
237
- return <TooltipProvider delayDuration={200}>{children}</TooltipProvider>
237
+ return <TooltipProvider delay={200}>{children}</TooltipProvider>
238
238
  }
239
239
 
240
240
  export function SaveButton() {
@@ -318,13 +318,13 @@ Additional tokens:
318
318
 
319
319
  The `Button` component can apply those palette families directly through `color: UIColor`, with `primary` as the default. Its public variants are `solid`, `secondary`, `outline`, `subtle`, `ghost`, and `link`, with `solid` as the default. Its public sizes are `xs`, `sm`, `md`, `lg`, `xl`, `icon-xs`, `icon-sm`, `icon`, `icon-lg`, and `icon-xl`; `loading` shows a centered spinner, keeps native button content invisible to preserve size, and disables the button without dimming it.
320
320
 
321
- The `Checkbox` component can be used as a bare checkbox or with `label?: React.ReactNode`. It supports `size: 'sm' | 'md' | 'lg'` and `color: UIColor`, with `primary` as the default color. When `label` is provided, it composes the package `Label`, accepts `labelProps` for that label, and generates an accessible `id`/`htmlFor` pair when no `id` prop is passed. Composed label text follows checkbox size: `sm` 12px, `md` 14px, and `lg` 20px.
321
+ The `Checkbox` component can be used as a bare checkbox or with `label?: React.ReactNode`. It supports `size: 'sm' | 'md' | 'lg'` and `color: UIColor`, with `primary` as the default color. When `label` is provided, it composes the package `Label`, accepts `labelProps` for that label, and generates an accessible `id`/`htmlFor` pair when no `id` prop is passed. Composed label text follows checkbox size: `sm` 12px, `md` 14px, and `lg` 18px. Pass `description?: React.ReactNode` (and `descriptionProps`) to render muted helper text (a `<p>`) below the label, linked to the control via `aria-describedby`; description text is `sm` 12px, `md` 12px, `lg` 14px.
322
322
 
323
- The `Switch` component can be used as a bare switch or with `label?: React.ReactNode`. It supports `size: 'sm' | 'md' | 'lg'` and `color: UIColor`, with `green` as the default color. The `color` prop controls only the checked track and indicator color; unchecked remains neutral. Track size is `sm` 34x16px, `md` 42x20px, and `lg` 48x22px. The thumb and its internal indicator scale with the selected size. Composed label text follows switch size: `sm` 12px, `md` 14px, and `lg` 20px.
323
+ The `Switch` component can be used as a bare switch or with `label?: React.ReactNode`. It supports `size: 'sm' | 'md' | 'lg'` and `color: UIColor`, with `green` as the default color. The `color` prop controls only the checked track and indicator color; unchecked remains neutral. Track size is `sm` 30x14px, `md` 34x16px, and `lg` 42x20px (control heights match `Checkbox`). The thumb and its internal indicator scale with the selected size. Composed label text follows switch size: `sm` 12px, `md` 14px, and `lg` 18px. `description?: React.ReactNode` (and `descriptionProps`) works the same as on `Checkbox`.
324
324
 
325
325
  The `TabsList` component supports `variant: 'line' | 'enclosed'`, with `line` as the default, and `grow?: boolean` for equal-width horizontal tabs. The `line` variant keeps a neutral border line across tabs and uses the `primary` color for the active indicator; use `enclosed` for the filled container treatment.
326
326
 
327
- The `RadioGroup` component supports `color: UIColor`, with `primary` as the default group color. `RadioGroupItem` can be used as a bare radio item inside `RadioGroup` or with `label?: React.ReactNode`; it supports `size: 'sm' | 'md' | 'lg'` and optional `color: UIColor` item overrides. Composed label text follows item size: `sm` 12px, `md` 14px, and `lg` 20px.
327
+ The `RadioGroup` component supports `color: UIColor`, with `primary` as the default group color. `RadioGroupItem` can be used as a bare radio item inside `RadioGroup` or with `label?: React.ReactNode`; it supports `size: 'sm' | 'md' | 'lg'` and optional `color: UIColor` item overrides. Composed label text follows item size: `sm` 12px, `md` 14px, and `lg` 18px. `description?: React.ReactNode` (and `descriptionProps`) works the same as on `Checkbox`.
328
328
 
329
329
  The `DateRangePicker` component is a compound popover date range picker built on `Calendar` range mode. Use `DateRange | null` for controlled values; applying an empty draft or resetting emits `null` through `onValueChange`. Fixed presets, relative-day inputs, date inputs, and the calendar all edit the same draft, and `DateRangePickerPreset` displays a selected state when its range matches the current draft by calendar day.
330
330
 
@@ -365,7 +365,7 @@ import { Table, TableContainer } from '@uselets/ui/table'
365
365
  </DataTableRoot>
366
366
  ```
367
367
 
368
- `DataTableRoot` accepts either `{ columns, data, ...options }` (uncontrolled — creates the `table` instance internally) or `{ table }` (controlled — table instance comes from `useDataTable` called by the consumer). Use the controlled form whenever you need access to the `table` instance in the same scope as the JSX (e.g. for virtualization or external action buttons).
368
+ `DataTableRoot` accepts either `{ columns, data, ...options }` (uncontrolled — creates the `table` instance internally) or `{ table }` (controlled — table instance comes from `useDataTable` called by the consumer). In the uncontrolled form, pagination is on while a `<DataTablePagination />` is rendered inside the root and off otherwise (all rows); an explicit `enablePagination` overrides that. In the controlled form, `useDataTable` defaults to `enablePagination: false` — pass `true` when rendering `<DataTablePagination />`. Use the controlled form whenever you need access to the `table` instance in the same scope as the JSX (e.g. for virtualization or external action buttons).
369
369
 
370
370
  ### Virtualized lists (bring your own virtualizer)
371
371