@primitiv-ui/react 0.1.29 → 0.1.30

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 (213) hide show
  1. package/README.md +15 -2
  2. package/package.json +1 -1
  3. package/src/AccessibleIcon/AccessibleIcon.tsx +1 -1
  4. package/src/Accordion/Accordion.tsx +7 -7
  5. package/src/Accordion/README.md +2 -2
  6. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +3 -3
  7. package/src/Breadcrumb/Breadcrumb.tsx +7 -7
  8. package/src/Breadcrumb/README.md +2 -2
  9. package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +1 -1
  10. package/src/Breadcrumb/types.ts +1 -1
  11. package/src/Button/__tests__/Button.contract.test.tsx +1 -1
  12. package/src/Carousel/Carousel.tsx +6 -6
  13. package/src/Carousel/README.md +22 -22
  14. package/src/Carousel/__tests__/Carousel.infinite.test.tsx +5 -5
  15. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +2 -2
  16. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +1 -1
  17. package/src/Carousel/loopEngine.ts +1 -1
  18. package/src/Carousel/types.ts +1 -1
  19. package/src/Checkbox/Checkbox.tsx +2 -2
  20. package/src/CheckboxCard/CheckboxCard.tsx +1 -1
  21. package/src/CheckboxCard/README.md +1 -1
  22. package/src/Collapsible/README.md +2 -2
  23. package/src/Combobox/Combobox.tsx +508 -0
  24. package/src/Combobox/ComboboxContext.ts +23 -0
  25. package/src/Combobox/README.md +170 -0
  26. package/src/Combobox/__tests__/Combobox.asChild.test.tsx +62 -0
  27. package/src/Combobox/__tests__/Combobox.basic-rendering.test.tsx +16 -0
  28. package/src/Combobox/__tests__/Combobox.content.test.tsx +36 -0
  29. package/src/Combobox/__tests__/Combobox.controlled-state.test.tsx +88 -0
  30. package/src/Combobox/__tests__/Combobox.empty.test.tsx +42 -0
  31. package/src/Combobox/__tests__/Combobox.ids.test.tsx +66 -0
  32. package/src/Combobox/__tests__/Combobox.item.test.tsx +76 -0
  33. package/src/Combobox/__tests__/Combobox.keyboard-interaction.test.tsx +196 -0
  34. package/src/Combobox/__tests__/Combobox.light-dismiss.test.tsx +101 -0
  35. package/src/Combobox/__tests__/Combobox.query.test.tsx +105 -0
  36. package/src/Combobox/hooks/index.ts +1 -0
  37. package/src/Combobox/hooks/useComboboxRoot.ts +2 -0
  38. package/src/Combobox/index.ts +3 -0
  39. package/src/Combobox/types.ts +133 -0
  40. package/src/ContextMenu/ContextMenu.tsx +3 -3
  41. package/src/ContextMenu/README.md +1 -1
  42. package/src/DirectionProvider/DirectionProvider.tsx +5 -5
  43. package/src/DirectionProvider/README.md +4 -4
  44. package/src/Divider/__tests__/Divider.contract.test.tsx +1 -1
  45. package/src/Drawer/Drawer.tsx +5 -5
  46. package/src/Drawer/README.md +2 -2
  47. package/src/Dropdown/Dropdown.tsx +2 -2
  48. package/src/Dropdown/README.md +7 -5
  49. package/src/EmptyState/README.md +2 -2
  50. package/src/Field/README.md +1 -1
  51. package/src/Field/hooks/useFieldProps.ts +1 -1
  52. package/src/Fieldset/Fieldset.tsx +3 -3
  53. package/src/Fieldset/README.md +2 -2
  54. package/src/Input/Input.tsx +1 -1
  55. package/src/Input/README.md +2 -2
  56. package/src/Listbox/Listbox.tsx +475 -0
  57. package/src/Listbox/ListboxContext.ts +48 -0
  58. package/src/Listbox/README.md +248 -0
  59. package/src/Listbox/__tests__/Listbox.active-descendant.test.tsx +128 -0
  60. package/src/Listbox/__tests__/Listbox.asChild.test.tsx +91 -0
  61. package/src/Listbox/__tests__/Listbox.basic-rendering.test.tsx +16 -0
  62. package/src/Listbox/__tests__/Listbox.controlled-state.test.tsx +114 -0
  63. package/src/Listbox/__tests__/Listbox.disabled.test.tsx +90 -0
  64. package/src/Listbox/__tests__/Listbox.fixtures.ts +27 -0
  65. package/src/Listbox/__tests__/Listbox.group-label.test.tsx +97 -0
  66. package/src/Listbox/__tests__/Listbox.group.test.tsx +45 -0
  67. package/src/Listbox/__tests__/Listbox.keyboard-edge-cases.test.tsx +144 -0
  68. package/src/Listbox/__tests__/Listbox.keyboard-interaction.test.tsx +112 -0
  69. package/src/Listbox/__tests__/Listbox.modifier-keys.test.tsx +86 -0
  70. package/src/Listbox/__tests__/Listbox.multi-select-keyboard.test.tsx +368 -0
  71. package/src/Listbox/__tests__/Listbox.multiple-mode.test.tsx +91 -0
  72. package/src/Listbox/__tests__/Listbox.mutation-hardening.test.tsx +480 -0
  73. package/src/Listbox/__tests__/Listbox.orientation.test.tsx +59 -0
  74. package/src/Listbox/__tests__/Listbox.reading-direction.test.tsx +65 -0
  75. package/src/Listbox/__tests__/Listbox.reorder.test.tsx +78 -0
  76. package/src/Listbox/__tests__/Listbox.selection-follows-focus.test.tsx +64 -0
  77. package/src/Listbox/__tests__/Listbox.strict-context.test.tsx +21 -0
  78. package/src/Listbox/__tests__/Listbox.typeahead.test.tsx +123 -0
  79. package/src/Listbox/__tests__/Listbox.uncontrolled-state.test.tsx +48 -0
  80. package/src/Listbox/constants.ts +2 -0
  81. package/src/Listbox/hooks/index.ts +1 -0
  82. package/src/Listbox/hooks/useListboxRoot.ts +407 -0
  83. package/src/Listbox/index.ts +3 -0
  84. package/src/Listbox/types.ts +238 -0
  85. package/src/MillerColumns/MillerColumns.tsx +104 -30
  86. package/src/MillerColumns/README.md +90 -20
  87. package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +45 -0
  88. package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +4 -2
  89. package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +18 -3
  90. package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +48 -0
  91. package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +35 -2
  92. package/src/MillerColumns/__tests__/MillerColumns.keyboard-selection-state.test.tsx +96 -0
  93. package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +29 -4
  94. package/src/MillerColumns/__tests__/MillerColumns.preview-visibility.test.tsx +79 -0
  95. package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +189 -0
  96. package/src/MillerColumns/__tests__/MillerColumns.styled-parts.test.tsx +143 -0
  97. package/src/MillerColumns/__tests__/MillerColumns.typeahead.test.tsx +104 -0
  98. package/src/MillerColumns/constants.ts +2 -0
  99. package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +5 -2
  100. package/src/MillerColumns/hooks/useMillerColumnsItem.ts +29 -7
  101. package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +80 -10
  102. package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
  103. package/src/MillerColumns/index.ts +4 -0
  104. package/src/MillerColumns/types.ts +56 -6
  105. package/src/MillerColumns/utils.ts +69 -3
  106. package/src/Modal/Modal.tsx +4 -4
  107. package/src/Modal/README.md +5 -5
  108. package/src/Modal/types.ts +1 -1
  109. package/src/NavigationMenu/NavigationMenu.tsx +6 -6
  110. package/src/NavigationMenu/README.md +3 -3
  111. package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +1 -1
  112. package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +1 -1
  113. package/src/Popover/Popover.tsx +2 -2
  114. package/src/Popover/README.md +1 -1
  115. package/src/Portal/Portal.tsx +2 -2
  116. package/src/Portal/README.md +2 -2
  117. package/src/Progress/README.md +1 -1
  118. package/src/Radio/Radio.tsx +1 -1
  119. package/src/RadioGroup/README.md +2 -2
  120. package/src/RadioGroup/RadioGroup.tsx +1 -1
  121. package/src/SegmentedControl/README.md +2 -2
  122. package/src/SegmentedControl/SegmentedControl.tsx +1 -1
  123. package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +1 -0
  124. package/src/Select/README.md +2 -2
  125. package/src/Select/Select.tsx +8 -8
  126. package/src/Select/__tests__/Select.placeholder.test.tsx +3 -3
  127. package/src/Select/__tests__/Select.typeahead.test.tsx +3 -3
  128. package/src/Select/__tests__/Select.value-mirror.test.tsx +1 -1
  129. package/src/Select/types.ts +2 -2
  130. package/src/SkipNav/README.md +3 -3
  131. package/src/SkipNav/SkipNav.tsx +5 -5
  132. package/src/Slider/README.md +37 -7
  133. package/src/Slider/Slider.tsx +9 -3
  134. package/src/Slot/Slot.tsx +2 -2
  135. package/src/Slot/__tests__/Slot.test.tsx +2 -2
  136. package/src/Slot/types.ts +1 -1
  137. package/src/SplitButton/README.md +225 -0
  138. package/src/SplitButton/SplitButton.tsx +457 -0
  139. package/src/SplitButton/SplitButtonContext.ts +26 -0
  140. package/src/SplitButton/__tests__/SplitButton.accessibility.test.tsx +55 -0
  141. package/src/SplitButton/__tests__/SplitButton.asChild.test.tsx +51 -0
  142. package/src/SplitButton/__tests__/SplitButton.basic-rendering.test.tsx +57 -0
  143. package/src/SplitButton/__tests__/SplitButton.data-attributes.test.tsx +28 -0
  144. package/src/SplitButton/__tests__/SplitButton.disabled.test.tsx +79 -0
  145. package/src/SplitButton/__tests__/SplitButton.error-handling.test.tsx +29 -0
  146. package/src/SplitButton/__tests__/SplitButton.keyboard-interaction.test.tsx +56 -0
  147. package/src/SplitButton/__tests__/SplitButton.state-modes.test.tsx +68 -0
  148. package/src/SplitButton/index.ts +2 -0
  149. package/src/SplitButton/types.ts +186 -0
  150. package/src/Switch/README.md +1 -1
  151. package/src/Switch/Switch.tsx +2 -2
  152. package/src/Switch/types.ts +1 -1
  153. package/src/Table/README.md +110 -8
  154. package/src/Table/Table.tsx +206 -10
  155. package/src/Table/TableExpandableContext.ts +7 -0
  156. package/src/Table/__tests__/Table.DetailRow.test.tsx +83 -0
  157. package/src/Table/__tests__/Table.ExpandTrigger.test.tsx +102 -0
  158. package/src/Table/__tests__/Table.Expandable.test.tsx +121 -0
  159. package/src/Table/types.ts +125 -0
  160. package/src/Tabs/README.md +4 -4
  161. package/src/Tabs/Tabs.tsx +5 -4
  162. package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +58 -0
  163. package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +15 -0
  164. package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +24 -0
  165. package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +36 -0
  166. package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +17 -0
  167. package/src/Tabs/hooks/useTabsContent.ts +5 -0
  168. package/src/Tabs/hooks/useTabsRoot.ts +29 -5
  169. package/src/Tabs/hooks/useTabsTrigger.ts +7 -2
  170. package/src/ToggleGroup/README.md +4 -4
  171. package/src/Tooltip/README.md +7 -7
  172. package/src/Tooltip/Tooltip.tsx +2 -2
  173. package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +50 -0
  174. package/src/Tooltip/__tests__/Tooltip.error-handling.test.tsx +37 -0
  175. package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
  176. package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +134 -0
  177. package/src/Tooltip/__tests__/Tooltip.skip-delay.test.tsx +216 -0
  178. package/src/Tooltip/__tests__/Tooltip.timer-coordination.test.tsx +223 -0
  179. package/src/Tooltip/hooks/useTooltipContent.ts +3 -0
  180. package/src/Tooltip/hooks/useTooltipProvider.ts +40 -14
  181. package/src/Tooltip/hooks/useTooltipRoot.ts +30 -6
  182. package/src/Tree/README.md +69 -5
  183. package/src/Tree/Tree.tsx +1 -1
  184. package/src/Tree/__tests__/Tree.styled-wrapper.test.tsx +159 -0
  185. package/src/Tree/hooks/useTreePath.ts +33 -1
  186. package/src/Tree/index.ts +6 -1
  187. package/src/Tree/utils.ts +46 -4
  188. package/src/VisuallyHidden/VisuallyHidden.tsx +1 -1
  189. package/src/hooks/index.ts +1 -0
  190. package/src/hooks/useTypeahead.ts +95 -0
  191. package/src/index.ts +6 -0
  192. package/src/useLocalStorage/__tests__/useLocalStorage.reading.test.tsx +31 -0
  193. package/src/useLocalStorage/__tests__/useLocalStorage.resilience.test.tsx +97 -0
  194. package/src/useLocalStorage/__tests__/useLocalStorage.ssr.test.tsx +20 -0
  195. package/src/useLocalStorage/__tests__/useLocalStorage.writing.test.tsx +72 -0
  196. package/src/useLocalStorage/index.ts +1 -0
  197. package/src/useLocalStorage/useLocalStorage.ts +140 -0
  198. package/src/useMediaQuery/README.md +66 -0
  199. package/src/useMediaQuery/__tests__/useMediaQuery.matching.test.tsx +42 -0
  200. package/src/useMediaQuery/__tests__/useMediaQuery.ssr.test.tsx +19 -0
  201. package/src/useMediaQuery/__tests__/useMediaQuery.subscription.test.tsx +89 -0
  202. package/src/useMediaQuery/index.ts +1 -0
  203. package/src/useMediaQuery/useMediaQuery.ts +49 -0
  204. package/src/usePagination/README.md +170 -0
  205. package/src/usePagination/__tests__/usePagination.navigation.test.tsx +109 -0
  206. package/src/usePagination/__tests__/usePagination.page-count.test.tsx +56 -0
  207. package/src/usePagination/__tests__/usePagination.page-state.test.tsx +87 -0
  208. package/src/usePagination/__tests__/usePagination.range.test.tsx +82 -0
  209. package/src/usePagination/__tests__/usePagination.slice.test.tsx +59 -0
  210. package/src/usePagination/__tests__/usePagination.windowing.test.tsx +68 -0
  211. package/src/usePagination/index.ts +1 -0
  212. package/src/usePagination/usePagination.ts +292 -0
  213. package/src/utils/getKeyToActionMap.ts +1 -1
package/README.md CHANGED
@@ -28,6 +28,7 @@ import { Tabs } from "@primitiv-ui/react";
28
28
  | [Checkbox](src/Checkbox/README.md) | A real, visually-hidden native `<input type="checkbox">` in a `<label>` box, so it participates in forms; the platform tri-state (`indeterminate` → `aria-checked="mixed"`, `:indeterminate`), controlled/uncontrolled state, `Indicator` mark, and `asChild` on the indicator |
29
29
  | [CheckboxCard](src/CheckboxCard/README.md) | WAI-ARIA Checkbox pattern in a card/tile layout — the entire card surface is the interactive element, with tri-state support, controlled/uncontrolled state, `Indicator` slot, `disabled` with `data-disabled` styling hook, and `asChild` composition |
30
30
  | [Collapsible](src/Collapsible/README.md) | Single-panel show/hide — boolean controlled/uncontrolled state, `disabled`, `asChild` composition with explicit Enter/Space handling, `forceMount` for CSS-Grid-driven animation, and `TriggerIcon` slot |
31
+ | [Combobox](src/Combobox/README.md) | WAI-ARIA APG Combobox pattern — an editable text field with a filtered popup listbox, using the same **virtual-focus** cursor as `Listbox` (`aria-activedescendant` on the input, `data-highlighted` on the item) so DOM focus never leaves the field; filtering is deliberately **consumer-owned** (no `filter` prop — `onQueryChange` reports keystrokes and you render the matches), the input holds the live query while open and resets from the committed value on Enter, Escape **or a click outside** — the panel is a `popover="auto"`, so the browser supplies both the top layer and light dismiss rather than a hand-rolled outside-click listener, and all three gestures land in the same state; **emptying the field clears the value too** (`onValueChange("")`), so a reset can never resurrect a label the user just deleted; arrow keys seed the cursor on the first/last item, an `Empty` part keeps the panel's height while nothing matches, controlled/uncontrolled `open` and `value`, and `asChild` on every part |
31
32
  | [ContextMenu](src/ContextMenu/README.md) | WAI-ARIA Menu pattern for right-click / long-press / context-key menus — native HTML `popover` compound component positioned at the cursor, with nested submenus, checkbox and radio items, `ItemIndicator` icon slot, groups and labels, keyboard navigation, typeahead, disabled-item skipping, `onSelect` escape hatch, and `asChild` composition |
32
33
  | [DirectionProvider](src/DirectionProvider/README.md) | Reading-direction context — broadcasts `ltr`/`rtl` to descendants and exposes the `useDirection` hook so direction-aware components inherit `dir` instead of each being passed it explicitly; renders no DOM |
33
34
  | [Divider](src/Divider/README.md) | WAI-ARIA separator role — horizontal or vertical content separator, with semantic and decorative use |
@@ -38,6 +39,7 @@ import { Tabs } from "@primitiv-ui/react";
38
39
  | [Fieldset](src/Fieldset/README.md) | Headless form-control grouping — a stateless `<fieldset>` / `<legend>` compound with an implicit `group` role, a legend-supplied accessible name, and `disabled` (cascading to every nested control) with a `data-disabled` styling hook |
39
40
  | [Input](src/Input/README.md) | Headless single-line text input — native `<input>` with `type="text"` default, full attribute pass-through (works directly with `react-hook-form`'s `register` spread), ref forwarding, native constraint validation, `disabled` with `data-disabled` styling hook, and `asChild` composition |
40
41
  | [InputGroup](src/InputGroup/README.md) | Stateless frame for `Input` (or any framed control) with named leading and trailing adornment slots — decorative icons, currency symbols, clear buttons, password-reveal toggles; `data-input-group` / `data-input-group-adornment` styling hooks, CSS-driven disabled / invalid / focus-within state via `:has()`, and `asChild` composition on every part |
42
+ | [Listbox](src/Listbox/README.md) | WAI-ARIA APG Listbox pattern — a persistently-visible option list using a **virtual-focus** cursor (`aria-activedescendant` on the root, `data-highlighted` on the option) rather than a roving tabstop, so an external input can hold DOM focus and drive the list; single/multiple selection with APG's Shift+Arrow / Ctrl+Shift+Home-End / Ctrl+A range shortcuts, opt-in `selectionFollowsFocus`, prefix typeahead, groups named by prop or visible `GroupLabel`, disabled options, DOM-order navigation that survives in-place reordering, orientation/RTL, and `asChild` composition on every part |
41
43
  | [MillerColumns](src/MillerColumns/README.md) | Miller columns / cascading-lists pattern — a horizontal strip of `role="group"` columns of `role="treeitem"` items, authored by recursive composition with portal-projected child columns, controlled/uncontrolled active path, single-tabstop roving keyboard navigation, disabled items, and `asChild` composition |
42
44
  | [Modal](src/Modal/README.md) | WAI-ARIA Modal Dialog pattern — native `<dialog>`-based compound component with Portal, bounding-rect click-outside, controlled/uncontrolled state, escape hatches, imperative API, `asChild`, and `forceMount` animation hooks |
43
45
  | [NavigationMenu](src/NavigationMenu/README.md) | ARIA APG Disclosure Navigation Menu pattern — the desktop dropdown site nav: `<nav>` landmark + `<ul>` of entries, one panel open at a time (`""` means closed), hover-intent open with instant switching and a nav-wide close grace, axis/RTL-aware arrow travel that keeps every entry in the tab order (not a roving tabindex), cross-axis arrow to enter a panel, Escape-to-close with focus return, an optional `Viewport` panels portal into so the open one morphs into the next, an optional `Indicator` publishing the open trigger's measured geometry as custom properties, and a `Link` carrying `aria-current` that the composed mobile nav (`Drawer` + `Collapsible`) reuses |
@@ -51,9 +53,10 @@ import { Tabs } from "@primitiv-ui/react";
51
53
  | [Select](src/Select/README.md) | Headless wrapper over native `<select>` / `<option>` / `<optgroup>` — controlled/uncontrolled state via `onValueChange`, `Select.Placeholder` with auto-`defaultValue` inference, `Select.Group` for optgroup labelling, `data-disabled` styling hook, `asChild` composition, and Field-aware (opts into `FieldContext` for auto-wired `id` / `aria-describedby` / `aria-invalid` / `disabled` / `required`) |
52
54
  | [SkipNav](src/SkipNav/README.md) | WCAG 2.4.1 Bypass Blocks technique — a stateless "skip to main content" link and its `tabIndex=-1` focus target, connected by a shared content id, working with no JavaScript at runtime |
53
55
  | [Slider](src/Slider/README.md) | WAI-ARIA Slider & Multi-Thumb Slider pattern — an array-valued range input with any number of independently focusable thumbs, keyboard and pointer dragging, `min`/`max`/`step`, `minStepsBetweenThumbs`, horizontal/vertical orientation, RTL and `inverted` direction, `onValueCommit`, hidden inputs for form submission, `disabled` with `data-disabled` styling hook, and `asChild` composition |
56
+ | [SplitButton](src/SplitButton/README.md) | One primary action plus a menu of related alternatives, bound into a single `role="group"` widget — a composition over `Dropdown` that adds the coordination Dropdown alone can't provide: the group boundary, the menu trigger's accessible name derived from the primary action (so a chevron-only control is never unlabelled), `disabled` propagating from the group to both halves, and `ArrowDown` on the action opening the menu; both halves stay independently tabbable, and `asChild` composition is available on the group and the action |
54
57
  | [Status](src/Status/README.md) | WAI-ARIA `status` role — a polite live region for low-priority, non-urgent status messages, announced once the user is idle, with `asChild` composition |
55
58
  | [Switch](src/Switch/README.md) | A real, visually-hidden native `<input type="checkbox" role="switch">` in a `<label>` track, so it participates in forms; binary on/off state, always-mounted `Thumb` for CSS-driven sliding (off the input's native `:checked`), controlled/uncontrolled state, `data-disabled` styling hook, and `asChild` on the thumb |
56
- | [Table](src/Table/README.md) | WAI-ARIA Table pattern — compound component wrapping standard HTML table elements with accessible headers, captions, and a responsive scroll wrapper |
59
+ | [Table](src/Table/README.md) | WAI-ARIA Table pattern — compound component wrapping standard HTML table elements with accessible headers, captions, a responsive scroll wrapper, and an opt-in expandable-row disclosure |
57
60
  | [Tabs](src/Tabs/README.md) | WAI-ARIA Tabs pattern — compound component with keyboard navigation, controlled/uncontrolled state, and `asChild` composition |
58
61
  | [Textarea](src/Textarea/README.md) | Headless multi-line text input — native `<textarea>` with full attribute pass-through, ref forwarding, `disabled` with `data-disabled` styling hook, `asChild` composition for wrapping autosizing implementations, and Field-aware (opts into `FieldContext` for auto-wired `id` / `aria-describedby` / `aria-invalid` / `disabled` / `required`) |
59
62
  | [Toggle](src/Toggle/README.md) | WAI-ARIA Button pattern with `aria-pressed` — single stateful toggle button with controlled/uncontrolled state, `data-state` styling hook, and `asChild` composition |
@@ -62,10 +65,20 @@ import { Tabs } from "@primitiv-ui/react";
62
65
  | [Tree](src/Tree/README.md) | WAI-ARIA Tree pattern — hierarchical tree view authored by recursive composition with `Branch`/`BranchControl`/`BranchContent`, controlled/uncontrolled expansion, single or multiple selection with Ctrl/Cmd and Shift+click range, `forceMount` for branch animation, single-tabstop roving keyboard navigation, disabled items, and `asChild` composition |
63
66
  | [VisuallyHidden](src/VisuallyHidden/README.md) | Screen-reader-only primitive — visually hides its children with the canonical clip styles while keeping them in the accessibility tree, with consumer `style` merging and `asChild` composition |
64
67
 
68
+ ## Hooks
69
+
70
+ | Hook | Description |
71
+ | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
72
+ | [useMediaQuery](src/useMediaQuery/README.md) | Tracks whether a CSS media query currently matches, re-rendering on change — generic, not tied to Primitiv's own breakpoint scale, with a safe `false` default during server rendering |
73
+ | [usePagination](src/usePagination/README.md) | Page state and arithmetic for any paged data source — clamped current page, prev/next/first/last, half-open `startIndex`/`endIndex` slice bounds for client- or server-side data, and the collapsed page range to render |
74
+
65
75
  ## Internal utilities
66
76
 
67
77
  **`Slot`** is the `asChild` composition utility used internally by components
68
- such as `Tabs.Trigger`. It is not a public component API.
78
+ such as `Tabs.Trigger`. It is not a public component API. The `hooks/`
79
+ directory (`useCollection`, `useControllableState`, `useRovingTabindex`) is
80
+ similarly internal — shared implementation details of compound components,
81
+ not part of the public API.
69
82
 
70
83
  ## Testing
71
84
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitiv-ui/react",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "description": "Headless, accessible React components built on the WAI-ARIA authoring patterns. Zero styles ship with this package — bring your own (CSS, Tailwind, CSS-in-JS, design tokens).",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -28,7 +28,7 @@ type DecorativeIconProps = { "aria-hidden": string; focusable: string };
28
28
  * if your icon library returns a fragment.
29
29
  *
30
30
  * **Renders no host element.** `AccessibleIcon` returns a React fragment
31
- * (`<>…</>`), so it adds no DOM node of its own. The icon element and the
31
+ * (`<>...</>`), so it adds no DOM node of its own. The icon element and the
32
32
  * {@link VisuallyHidden} span are the only nodes emitted.
33
33
  *
34
34
  * @example Icon-only button
@@ -57,8 +57,8 @@ import { useAccordionTrigger } from "./hooks/useAccordionTrigger";
57
57
  * @example Uncontrolled — single open item
58
58
  * ```tsx
59
59
  * <Accordion.Root defaultValue="item-1">
60
- * <Accordion.Item value="item-1">…</Accordion.Item>
61
- * <Accordion.Item value="item-2">…</Accordion.Item>
60
+ * <Accordion.Item value="item-1">...</Accordion.Item>
61
+ * <Accordion.Item value="item-2">...</Accordion.Item>
62
62
  * </Accordion.Root>
63
63
  * ```
64
64
  *
@@ -67,8 +67,8 @@ import { useAccordionTrigger } from "./hooks/useAccordionTrigger";
67
67
  * const [expanded, setExpanded] = useState<string[]>([]);
68
68
  *
69
69
  * <Accordion.Root multiple value={expanded} onValueChange={setExpanded}>
70
- * <Accordion.Item value="shipping">…</Accordion.Item>
71
- * <Accordion.Item value="returns">…</Accordion.Item>
70
+ * <Accordion.Item value="shipping">...</Accordion.Item>
71
+ * <Accordion.Item value="returns">...</Accordion.Item>
72
72
  * </Accordion.Root>
73
73
  * ```
74
74
  */
@@ -277,7 +277,7 @@ AccordionTrigger.displayName = "AccordionTrigger";
277
277
 
278
278
  /**
279
279
  * The panel that is revealed when the associated `Accordion.Trigger` is
280
- * activated. Renders a `<div role="region" aria-labelledby="…">` whose
280
+ * activated. Renders a `<div role="region" aria-labelledby="...">` whose
281
281
  * visibility is controlled by the `hidden` attribute.
282
282
  *
283
283
  * **`forceMount` prop.** By default the panel is removed from visibility with
@@ -296,7 +296,7 @@ AccordionTrigger.displayName = "AccordionTrigger";
296
296
  * default, creating an ARIA landmark. Accordions with many items can produce
297
297
  * landmark overload in screen readers. Opt out by passing `role={undefined}`:
298
298
  * ```tsx
299
- * <Accordion.Content role={undefined}>…</Accordion.Content>
299
+ * <Accordion.Content role={undefined}>...</Accordion.Content>
300
300
  * ```
301
301
  *
302
302
  * **Styling hooks.**
@@ -378,7 +378,7 @@ AccordionContent.displayName = "AccordionContent";
378
378
  * <Accordion.Trigger>
379
379
  * Shipping policy
380
380
  * <Accordion.TriggerIcon>
381
- * <svg …><path d="…" /></svg>
381
+ * <svg ...><path d="..." /></svg>
382
382
  * </Accordion.TriggerIcon>
383
383
  * </Accordion.Trigger>
384
384
  * ```
@@ -61,7 +61,7 @@ simultaneously:
61
61
 
62
62
  ```tsx
63
63
  <Accordion.Root multiple defaultValue="shipping">
64
- …
64
+ ...
65
65
  </Accordion.Root>
66
66
  ```
67
67
 
@@ -168,7 +168,7 @@ to invert the arrow-key direction so `ArrowLeft` moves forward and
168
168
 
169
169
  ```tsx
170
170
  <Accordion.Root orientation="horizontal" dir="rtl">
171
- …
171
+ ...
172
172
  </Accordion.Root>
173
173
  ```
174
174
 
@@ -73,14 +73,14 @@ describe("Avatar fallback", () => {
73
73
  });
74
74
 
75
75
  it("resets the timer when delayMs changes", () => {
76
- // Arrange — start a 300ms delay…
76
+ // Arrange — start a 300ms delay...
77
77
  const { rerender } = render(
78
78
  <Avatar.Root>
79
79
  <Avatar.Fallback delayMs={300}>AL</Avatar.Fallback>
80
80
  </Avatar.Root>,
81
81
  );
82
82
 
83
- // Act — …then extend it to 1000ms before the first delay elapses. The
83
+ // Act — ...then extend it to 1000ms before the first delay elapses. The
84
84
  // effect must clear the old timer and start a new one; a stale timer (no
85
85
  // cleanup) or a frozen dependency ([] deps) would fire at 300ms.
86
86
  rerender(
@@ -95,7 +95,7 @@ describe("Avatar fallback", () => {
95
95
  // Assert — still withheld at 300ms because the delay was reset to 1000ms
96
96
  expect(screen.queryByText("AL")).not.toBeInTheDocument();
97
97
 
98
- // …and shown once the new delay elapses
98
+ // ...and shown once the new delay elapses
99
99
  act(() => {
100
100
  vi.advanceTimersByTime(700);
101
101
  });
@@ -32,14 +32,14 @@ import {
32
32
  * @example
33
33
  * ```tsx
34
34
  * <Breadcrumb.Root>
35
- * <Breadcrumb.List>…</Breadcrumb.List>
35
+ * <Breadcrumb.List>...</Breadcrumb.List>
36
36
  * </Breadcrumb.Root>
37
37
  * ```
38
38
  *
39
39
  * @example Custom accessible name
40
40
  * ```tsx
41
41
  * <Breadcrumb.Root aria-label="You are here">
42
- * <Breadcrumb.List>…</Breadcrumb.List>
42
+ * <Breadcrumb.List>...</Breadcrumb.List>
43
43
  * </Breadcrumb.Root>
44
44
  * ```
45
45
  */
@@ -77,9 +77,9 @@ BreadcrumbRoot.displayName = "BreadcrumbRoot";
77
77
  * @example
78
78
  * ```tsx
79
79
  * <Breadcrumb.List>
80
- * <Breadcrumb.Item>…</Breadcrumb.Item>
80
+ * <Breadcrumb.Item>...</Breadcrumb.Item>
81
81
  * <Breadcrumb.Separator />
82
- * <Breadcrumb.Item>…</Breadcrumb.Item>
82
+ * <Breadcrumb.Item>...</Breadcrumb.Item>
83
83
  * </Breadcrumb.List>
84
84
  * ```
85
85
  */
@@ -132,7 +132,7 @@ BreadcrumbItem.displayName = "BreadcrumbItem";
132
132
  * **`asChild` composition.** Pass `asChild` to render a consumer-supplied
133
133
  * element — typically a routing library's `<Link>` — instead of the native
134
134
  * `<a>`, with all of `Breadcrumb.Link`'s props (`href`, event handlers,
135
- * `className`, `ref`, …) merged onto it via the {@link Slot} pattern. The
135
+ * `className`, `ref`, ...) merged onto it via the {@link Slot} pattern. The
136
136
  * child **must** be a single React element that accepts a `ref`; the
137
137
  * native `<a>` is dropped entirely rather than wrapping the child.
138
138
  *
@@ -254,7 +254,7 @@ BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
254
254
  * label) — `aria-hidden="true"` excludes this glyph from accessible-name
255
255
  * computation entirely, exactly like {@link BreadcrumbSeparator}.
256
256
  *
257
- * Defaults to a `"…"` glyph; pass `children` to use a custom one (an icon,
257
+ * Defaults to a `"..."` glyph; pass `children` to use a custom one (an icon,
258
258
  * `"···"`, etc.) — whatever is passed replaces the default entirely rather
259
259
  * than being appended to it.
260
260
  *
@@ -285,7 +285,7 @@ BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
285
285
  * ```
286
286
  */
287
287
  export function BreadcrumbEllipsis({
288
- children = "…",
288
+ children = "...",
289
289
  asChild = false,
290
290
  ...rest
291
291
  }: BreadcrumbEllipsisProps): ReactElement {
@@ -86,7 +86,7 @@ It defaults to a `"/"` glyph. Pass `children` to use a custom separator:
86
86
  which crumbs collapse is a styled-surface concern (see the registry `card`
87
87
  composite for the same "no data model in the primitive" philosophy). What
88
88
  the headless layer provides is `Breadcrumb.Ellipsis`: a purely decorative
89
- `"…"` glyph for the trigger's visual content.
89
+ `"..."` glyph for the trigger's visual content.
90
90
 
91
91
  Compose it with [`Dropdown`](../Dropdown/README.md) to build the actual
92
92
  trigger — a real interactive element (a `<button>`, or anything
@@ -118,7 +118,7 @@ import { Breadcrumb, Dropdown, VisuallyHidden } from "@primitiv-ui/react";
118
118
  </Breadcrumb.Item>;
119
119
  ```
120
120
 
121
- `Breadcrumb.Ellipsis` defaults to a `"…"` glyph; pass `children` to use a
121
+ `Breadcrumb.Ellipsis` defaults to a `"..."` glyph; pass `children` to use a
122
122
  custom one:
123
123
 
124
124
  ```tsx
@@ -17,7 +17,7 @@ describe("Breadcrumb ellipsis", () => {
17
17
  expect(ellipsis.tagName).toBe("SPAN");
18
18
  expect(ellipsis).toHaveAttribute("role", "presentation");
19
19
  expect(ellipsis).toHaveAttribute("aria-hidden", "true");
20
- expect(ellipsis).toHaveTextContent("…");
20
+ expect(ellipsis).toHaveTextContent("...");
21
21
  });
22
22
 
23
23
  it("lets the consumer override the Ellipsis glyph", () => {
@@ -71,7 +71,7 @@ export type BreadcrumbSeparatorProps = ComponentProps<"li">;
71
71
  * Props for {@link BreadcrumbEllipsis | `Breadcrumb.Ellipsis`} — all native
72
72
  * `<span>` attributes plus the `asChild` escape hatch. `role="presentation"`
73
73
  * and `aria-hidden="true"` are fixed by the component; `children` defaults
74
- * to a `"…"` glyph when omitted (see {@link BreadcrumbEllipsis}'s
74
+ * to a `"..."` glyph when omitted (see {@link BreadcrumbEllipsis}'s
75
75
  * custom-glyph example).
76
76
  *
77
77
  * @extends HTMLSpanElement
@@ -60,7 +60,7 @@ describe("Button styling contract", () => {
60
60
  );
61
61
 
62
62
  render(<Button disabled>Save</Button>);
63
- // Present with the documented value when the documented condition holds…
63
+ // Present with the documented value when the documented condition holds...
64
64
  expect(screen.getByRole("button")).toHaveAttribute(entry.name, entry.value);
65
65
  });
66
66
 
@@ -68,7 +68,7 @@ import type {
68
68
  *
69
69
  * @example Labelled inline, uncontrolled
70
70
  * ```tsx
71
- * <Carousel.Root ariaLabel="Featured products" defaultPage={0}>…</Carousel.Root>
71
+ * <Carousel.Root ariaLabel="Featured products" defaultPage={0}>...</Carousel.Root>
72
72
  * ```
73
73
  *
74
74
  * @example Controlled
@@ -80,14 +80,14 @@ import type {
80
80
  * page={page}
81
81
  * onPageChange={setPage}
82
82
  * >
83
- * …
83
+ * ...
84
84
  * </Carousel.Root>
85
85
  * ```
86
86
  *
87
87
  * @example Labelled by an existing heading
88
88
  * ```tsx
89
89
  * <h2 id="promos">Promotions</h2>
90
- * <Carousel.Root ariaLabelledBy="promos">…</Carousel.Root>
90
+ * <Carousel.Root ariaLabelledBy="promos">...</Carousel.Root>
91
91
  * ```
92
92
  */
93
93
  export const CarouselRoot: ForwardRefExoticComponent<
@@ -260,7 +260,7 @@ CarouselRoot.displayName = "CarouselRoot";
260
260
  * ```tsx
261
261
  * <Carousel.Root ariaLabel="Featured products">
262
262
  * <Carousel.Viewport>
263
- * <Carousel.Slide>…</Carousel.Slide>
263
+ * <Carousel.Slide>...</Carousel.Slide>
264
264
  * </Carousel.Viewport>
265
265
  * </Carousel.Root>
266
266
  * ```
@@ -399,12 +399,12 @@ CarouselViewport.displayName = "CarouselViewport";
399
399
  *
400
400
  * @example Override the auto-label
401
401
  * ```tsx
402
- * <Carousel.Slide ariaLabel="Hand-picked for you">…</Carousel.Slide>
402
+ * <Carousel.Slide ariaLabel="Hand-picked for you">...</Carousel.Slide>
403
403
  * ```
404
404
  *
405
405
  * @example Per-slide snap alignment (overrides the root default)
406
406
  * ```tsx
407
- * <Carousel.Slide snapAlign="end">…</Carousel.Slide>
407
+ * <Carousel.Slide snapAlign="end">...</Carousel.Slide>
408
408
  * ```
409
409
  */
410
410
  export function CarouselSlide({
@@ -149,7 +149,7 @@ import { Carousel } from "@primitiv-ui/react";
149
149
 
150
150
  ```tsx
151
151
  <h2 id="promos">Promotions</h2>
152
- <Carousel.Root ariaLabelledBy="promos">…</Carousel.Root>
152
+ <Carousel.Root ariaLabelledBy="promos">...</Carousel.Root>
153
153
  ```
154
154
 
155
155
  The discriminated union on the props type rejects shapes that supply both
@@ -170,7 +170,7 @@ Slides go inside `Carousel.Viewport`:
170
170
  ```
171
171
 
172
172
  Each `Carousel.Slide` self-registers with the Root, so every slide
173
- exposes its own `data-index="0"`, `data-index="1"`, … and a live
173
+ exposes its own `data-index="0"`, `data-index="1"`, ... and a live
174
174
  `data-total` reflecting the current slide count. Add or remove slides at
175
175
  runtime and the indices and totals update automatically.
176
176
 
@@ -179,7 +179,7 @@ the WAI-ARIA Carousel APG example uses. To override the auto-label with
179
179
  a more meaningful description, pass `ariaLabel`:
180
180
 
181
181
  ```tsx
182
- <Carousel.Slide ariaLabel="Hand-picked for you">…</Carousel.Slide>
182
+ <Carousel.Slide ariaLabel="Hand-picked for you">...</Carousel.Slide>
183
183
  ```
184
184
 
185
185
  The override remains stable as siblings mount and unmount around it.
@@ -220,7 +220,7 @@ strip), persisting the active page to a URL, or reacting to deep links.
220
220
  const [page, setPage] = useState(0);
221
221
 
222
222
  <Carousel.Root ariaLabel="Featured products" page={page} onPageChange={setPage}>
223
- …
223
+ ...
224
224
  </Carousel.Root>;
225
225
  ```
226
226
 
@@ -262,7 +262,7 @@ const [open, setOpen] = useState(false);
262
262
  slidesPerPage={3}
263
263
  >
264
264
  <Carousel.Viewport>
265
- <Carousel.Slide>…</Carousel.Slide>…
265
+ <Carousel.Slide>...</Carousel.Slide>...
266
266
  </Carousel.Viewport>
267
267
  </Carousel.Root>
268
268
 
@@ -279,7 +279,7 @@ const [open, setOpen] = useState(false);
279
279
  onPlayingChange={() => {}}
280
280
  >
281
281
  <Carousel.Viewport>
282
- <Carousel.Slide>…</Carousel.Slide>…
282
+ <Carousel.Slide>...</Carousel.Slide>...
283
283
  </Carousel.Viewport>
284
284
  <Carousel.Indicators label="Choose slide" />
285
285
  </Carousel.Root>
@@ -303,7 +303,7 @@ exposes an imperative handle via `ref`:
303
303
  const carouselRef = useRef<CarouselImperativeApi>(null);
304
304
 
305
305
  <Carousel.Root ref={carouselRef} ariaLabel="Featured products">
306
- …
306
+ ...
307
307
  </Carousel.Root>;
308
308
 
309
309
  carouselRef.current?.next();
@@ -410,7 +410,7 @@ browser snap-correcting after the scroll:
410
410
 
411
411
  ```tsx
412
412
  <Carousel.Root ariaLabel="Gallery" snapAlign="start">
413
- …
413
+ ...
414
414
  </Carousel.Root>
415
415
  ```
416
416
 
@@ -455,7 +455,7 @@ ends, rather than always forcing a rest on one:
455
455
 
456
456
  ```tsx
457
457
  <Carousel.Root ariaLabel="Gallery" snapType="proximity">
458
- …
458
+ ...
459
459
  </Carousel.Root>
460
460
  ```
461
461
 
@@ -472,7 +472,7 @@ axis** instead:
472
472
 
473
473
  ```tsx
474
474
  <Carousel.Root ariaLabel="Featured products" orientation="vertical">
475
- …
475
+ ...
476
476
  </Carousel.Root>
477
477
  ```
478
478
 
@@ -621,13 +621,13 @@ pin DOM `id`s on the rendered sub-components via the `ids` bag on
621
621
  indicatorGroup: "promo-indicators",
622
622
  }}
623
623
  >
624
- …
624
+ ...
625
625
  </Carousel.Root>
626
626
  ```
627
627
 
628
628
  Any keys you omit leave the corresponding element unidentified. A
629
629
  direct `id` prop on a sub-component (e.g.
630
- `<Carousel.NextTrigger id="…">`) wins over `ids.*` because it spreads
630
+ `<Carousel.NextTrigger id="...">`) wins over `ids.*` because it spreads
631
631
  last.
632
632
 
633
633
  ### Internationalisation
@@ -649,7 +649,7 @@ accessible names (`"Start automatic slide show"` and
649
649
  stopSlideshow: "Arrêter le diaporama",
650
650
  }}
651
651
  >
652
- …
652
+ ...
653
653
  </Carousel.Root>
654
654
  ```
655
655
 
@@ -700,7 +700,7 @@ With `slidesPerPage={3}` and 5 slides:
700
700
  active).
701
701
 
702
702
  The slide-level `aria-label="N of M"` continues to count individual
703
- slides (so a 5-slide carousel announces "1 of 5", "2 of 5", … even
703
+ slides (so a 5-slide carousel announces "1 of 5", "2 of 5", ... even
704
704
  when grouped into 3-per-page).
705
705
 
706
706
  Pass a numeric `slidesPerMove` to advance the visible window by an
@@ -712,7 +712,7 @@ arbitrary slide count per click instead of a full page:
712
712
  slidesPerPage={3}
713
713
  slidesPerMove={1}
714
714
  >
715
- …
715
+ ...
716
716
  </Carousel.Root>
717
717
  ```
718
718
 
@@ -969,7 +969,7 @@ const carouselRef = useRef<CarouselImperativeApi>(null);
969
969
  // type: "dragging.start" | "dragging" | "dragging.end"
970
970
  }}
971
971
  >
972
- …
972
+ ...
973
973
  </Carousel.Root>;
974
974
 
975
975
  carouselRef.current?.isDragging(); // live snapshot, read on demand
@@ -997,7 +997,7 @@ keyboard, the wheel, or a mouse drag — fires `Carousel.Root`'s
997
997
  // source: "keyboard" | "wheel" | "drag"
998
998
  }}
999
999
  >
1000
- …
1000
+ ...
1001
1001
  </Carousel.Root>;
1002
1002
  ```
1003
1003
 
@@ -1173,7 +1173,7 @@ timer while `playing` is `true`:
1173
1173
  ```tsx
1174
1174
  // Default 4000ms cadence
1175
1175
  <Carousel.Root ariaLabel="Featured products" autoplay defaultPlaying>
1176
- …
1176
+ ...
1177
1177
  </Carousel.Root>
1178
1178
 
1179
1179
  // Custom delay
@@ -1182,7 +1182,7 @@ timer while `playing` is `true`:
1182
1182
  autoplay={{ delay: 6000 }}
1183
1183
  defaultPlaying
1184
1184
  >
1185
- …
1185
+ ...
1186
1186
  </Carousel.Root>
1187
1187
  ```
1188
1188
 
@@ -1241,7 +1241,7 @@ play/pause toggles, but a consumer reacting to the timer itself in JS
1241
1241
  // type: "autoplay.start" | "autoplay" | "autoplay.stop"
1242
1242
  }}
1243
1243
  >
1244
- …
1244
+ ...
1245
1245
  </Carousel.Root>
1246
1246
  ```
1247
1247
 
@@ -1264,14 +1264,14 @@ anatomy part):
1264
1264
 
1265
1265
  ```tsx
1266
1266
  <Carousel.Root ariaLabel="Featured products">
1267
- <Carousel.Viewport>…</Carousel.Viewport>
1267
+ <Carousel.Viewport>...</Carousel.Viewport>
1268
1268
  <Carousel.ProgressText />
1269
1269
  </Carousel.Root>
1270
1270
  ```
1271
1271
 
1272
1272
  Renders `"1 of 3"` by default (1-indexed, matching `slideLabel`); pass a
1273
1273
  `progressText` formatter through `translations` (a
1274
- `({ page, totalPages }) => …` function) on the Root to customise the
1274
+ `({ page, totalPages }) => ...` function) on the Root to customise the
1275
1275
  format, or `children` on `Carousel.ProgressText`
1276
1276
  itself to render something else entirely (an icon alongside the
1277
1277
  count, say) instead of the computed text. `Carousel.ProgressText`
@@ -505,11 +505,11 @@ describe("Carousel infinite — transform engine", () => {
505
505
  await user.click(getByRole("button", { name: "Next" }));
506
506
 
507
507
  // 3 → 0 is +1 forward: offset 300 → 400 (onto the clone of slide 0), a single
508
- // step onward, not a rewind. The glide animates to the clone…
508
+ // step onward, not a rewind. The glide animates to the clone...
509
509
  expect(track!.style.transform).toBe("translate(-400px, 0px)");
510
510
  expect(track!.style.transition).toContain("transform");
511
511
 
512
- // …and once it settles the track re-bases by one period to the REAL slide 0
512
+ // ...and once it settles the track re-bases by one period to the REAL slide 0
513
513
  // at the identical pixels (offset 0), instantly and invisibly.
514
514
  fireEnd(track!);
515
515
  expect(track!.style.transform).toBe("translate(0px, 0px)");
@@ -768,7 +768,7 @@ describe("Carousel infinite — transform engine", () => {
768
768
 
769
769
  await user.click(getByRole("button", { name: "Next" }));
770
770
  // Offset 100 is already within [0, 400): the settle is a no-op, the transform
771
- // stays put (and the transition flips to none on the instant no-op repaint…
771
+ // stays put (and the transition flips to none on the instant no-op repaint...
772
772
  // which doesn't run — the guard returns early, so the transition is untouched).
773
773
  expect(track!.style.transform).toBe("translate(-100px, 0px)");
774
774
  fireEnd(track!);
@@ -848,9 +848,9 @@ describe("Carousel infinite — transform engine", () => {
848
848
  await user.click(getByRole("button", { name: "Next" })); // page 2 (offset 400)
849
849
  await user.click(getByRole("button", { name: "Previous" })); // back to page 1
850
850
 
851
- // Page 2 → 1: the track glides back one page (offset 400 → 200)…
851
+ // Page 2 → 1: the track glides back one page (offset 400 → 200)...
852
852
  expect(track!.style.transform).toBe("translate(-200px, 0px)");
853
- // …and no real slide carries a transform of its own.
853
+ // ...and no real slide carries a transform of its own.
854
854
  for (const slide of track!.querySelectorAll<HTMLElement>(
855
855
  "[data-carousel-slide][data-index]",
856
856
  )) {
@@ -209,7 +209,7 @@ describe("Carousel IntersectionObserver fallback + isInView", () => {
209
209
  ]);
210
210
  });
211
211
 
212
- // The page stays put — scrollsnapchange owns it…
212
+ // The page stays put — scrollsnapchange owns it...
213
213
  expect(screen.getByTestId("slide-0")).toHaveAttribute(
214
214
  "data-state",
215
215
  "active",
@@ -218,7 +218,7 @@ describe("Carousel IntersectionObserver fallback + isInView", () => {
218
218
  "data-state",
219
219
  "inactive",
220
220
  );
221
- // …but isInView still reflects what the observer reported.
221
+ // ...but isInView still reflects what the observer reported.
222
222
  expect(ref.current!.isInView(2)).toBe(true);
223
223
  } finally {
224
224
  delete (window as unknown as { onscrollsnapchange?: unknown })
@@ -272,7 +272,7 @@ describe("Carousel numeric slidesPerMove", () => {
272
272
 
273
273
  await user.click(screen.getByRole("button", { name: "Next" }));
274
274
 
275
- // Clamped move=2 → window [2,3], not the [3,…] a move of 3 would give.
275
+ // Clamped move=2 → window [2,3], not the [3,...] a move of 3 would give.
276
276
  expect(screen.getByTestId("slide-2")).toHaveAttribute(
277
277
  "data-state",
278
278
  "active",
@@ -13,7 +13,7 @@
13
13
  */
14
14
  export function shortestStep(from: number, to: number, count: number): number {
15
15
  if (count <= 0) return 0;
16
- const forward = (((to - from) % count) + count) % count; // 0 … count-1
16
+ const forward = (((to - from) % count) + count) % count; // 0 ... count-1
17
17
  return forward * 2 > count ? forward - count : forward;
18
18
  }
19
19
 
@@ -627,7 +627,7 @@ export type CarouselSlideProps = Omit<ComponentProps<"div">, "aria-label"> & {
627
627
  /** Props for `Carousel.NextTrigger` — the button that advances one page; native `<button>` props plus `asChild`. */
628
628
  export type CarouselNextTriggerProps = ComponentProps<"button"> & {
629
629
  /** Render the child element instead of the default `<button>`. All
630
- * trigger props (onClick, disabled, ids.nextTrigger, …) are merged
630
+ * trigger props (onClick, disabled, ids.nextTrigger, ...) are merged
631
631
  * onto the child via `Slot`. The child must accept a `ref`. */
632
632
  asChild?: boolean;
633
633
  };
@@ -32,7 +32,7 @@ function dataStateOf(checked: CheckedState) {
32
32
  *
33
33
  * Props routing: `className` / `style` style the **box** (the `<label>` you
34
34
  * see — the input is hidden); every other prop (`name`, `value`, `id`,
35
- * `aria-*`, `required`, `disabled`, `ref`, …) spreads onto the input, because
35
+ * `aria-*`, `required`, `disabled`, `ref`, ...) spreads onto the input, because
36
36
  * semantically the Root *is* the checkbox.
37
37
  *
38
38
  * Supports two state modes, statically discriminated at the type level:
@@ -63,7 +63,7 @@ function dataStateOf(checked: CheckedState) {
63
63
  * ```tsx
64
64
  * const [checked, setChecked] = useState<CheckedState>("indeterminate");
65
65
  *
66
- * <Checkbox.Root checked={checked} onCheckedChange={setChecked} aria-label="…">
66
+ * <Checkbox.Root checked={checked} onCheckedChange={setChecked} aria-label="...">
67
67
  * <Checkbox.Indicator />
68
68
  * </Checkbox.Root>
69
69
  * ```
@@ -61,7 +61,7 @@ function dataStateOf(checked: CheckedState) {
61
61
  * ```tsx
62
62
  * const [enabled, setEnabled] = useState<CheckedState>(false);
63
63
  *
64
- * <CheckboxCard.Root checked={enabled} onCheckedChange={setEnabled} aria-label="…">
64
+ * <CheckboxCard.Root checked={enabled} onCheckedChange={setEnabled} aria-label="...">
65
65
  * <CheckboxCard.Indicator />
66
66
  * Feature name
67
67
  * </CheckboxCard.Root>
@@ -46,7 +46,7 @@ Pass `checked` and `onCheckedChange` together. The parent owns the value.
46
46
  ```tsx
47
47
  const [enabled, setEnabled] = useState<CheckedState>(false);
48
48
 
49
- <CheckboxCard.Root checked={enabled} onCheckedChange={setEnabled} aria-label="…">
49
+ <CheckboxCard.Root checked={enabled} onCheckedChange={setEnabled} aria-label="...">
50
50
  <CheckboxCard.Indicator />
51
51
  Dark mode
52
52
  </CheckboxCard.Root>