atlas-react-kit 1.2.0 → 1.2.2

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 (72) hide show
  1. package/COMPONENTS.md +40 -40
  2. package/DESIGN_RULES.md +14 -40
  3. package/README.md +7 -6
  4. package/dist/alert.js +7 -6
  5. package/dist/calendar.js +5 -4
  6. package/dist/card.js +2 -1
  7. package/dist/combobox.d.ts +4 -3
  8. package/dist/combobox.js +8 -17
  9. package/dist/date-time-picker.js +2 -1
  10. package/dist/dialog.js +2 -1
  11. package/dist/dropdown-menu.js +7 -5
  12. package/dist/field.d.ts +6 -3
  13. package/dist/field.js +3 -2
  14. package/dist/form/checkbox-field.d.ts +3 -5
  15. package/dist/form/checkbox-field.js +13 -19
  16. package/dist/form/combobox-field.d.ts +2 -2
  17. package/dist/form/combobox-field.js +5 -6
  18. package/dist/form/control-field.d.ts +5 -5
  19. package/dist/form/control-field.js +6 -9
  20. package/dist/form/date-picker-field.d.ts +1 -1
  21. package/dist/form/date-picker-field.js +3 -7
  22. package/dist/form/date-time-picker-field.d.ts +1 -1
  23. package/dist/form/date-time-picker-field.js +3 -7
  24. package/dist/form/field-error.d.ts +13 -4
  25. package/dist/form/field-shell.d.ts +63 -47
  26. package/dist/form/field-shell.js +67 -35
  27. package/dist/form/focus-first-invalid.d.ts +7 -4
  28. package/dist/form/focus-first-invalid.js +10 -6
  29. package/dist/form/markdown-field.d.ts +1 -1
  30. package/dist/form/markdown-field.js +8 -4
  31. package/dist/form/select-field.d.ts +2 -8
  32. package/dist/form/select-field.js +5 -7
  33. package/dist/form/text-field.d.ts +2 -8
  34. package/dist/form/text-field.js +9 -6
  35. package/dist/form/toggle-group-field.d.ts +2 -2
  36. package/dist/form/toggle-group-field.js +5 -6
  37. package/dist/info-tip.d.ts +7 -4
  38. package/dist/info-tip.js +12 -4
  39. package/dist/internal/active-link.d.ts +7 -0
  40. package/dist/internal/active-link.js +9 -0
  41. package/dist/internal/calendar-size.d.ts +5 -0
  42. package/dist/internal/calendar-size.js +5 -0
  43. package/dist/internal/form-control.d.ts +7 -1
  44. package/dist/internal/form-control.js +8 -1
  45. package/dist/internal/kanban-layout.js +2 -1
  46. package/dist/internal/listbox-navigation.d.ts +8 -8
  47. package/dist/internal/listbox-navigation.js +23 -23
  48. package/dist/internal/listbox-option.d.ts +16 -3
  49. package/dist/internal/listbox-option.js +22 -5
  50. package/dist/internal/overlay-styles.d.ts +15 -5
  51. package/dist/internal/overlay-styles.js +15 -5
  52. package/dist/internal/search.d.ts +4 -0
  53. package/dist/internal/search.js +6 -0
  54. package/dist/internal/styles.d.ts +21 -0
  55. package/dist/internal/styles.js +21 -0
  56. package/dist/kanban.js +3 -2
  57. package/dist/label.js +2 -1
  58. package/dist/lazy-calendar.js +3 -2
  59. package/dist/nav-link.js +2 -1
  60. package/dist/picker-shell.d.ts +3 -1
  61. package/dist/picker-shell.js +3 -3
  62. package/dist/popover.js +6 -5
  63. package/dist/search-picker.js +8 -16
  64. package/dist/select.js +5 -5
  65. package/dist/skip-link.js +2 -1
  66. package/dist/tabs.js +3 -2
  67. package/dist/toast.js +11 -10
  68. package/dist/toggle-group.d.ts +1 -1
  69. package/dist/toggle-group.js +36 -2
  70. package/dist/tooltip.js +2 -2
  71. package/package.json +1 -1
  72. package/skills/atlas-react-kit/SKILL.md +46 -42
package/COMPONENTS.md CHANGED
@@ -35,22 +35,22 @@ theme from `next-themes`, so toast consumers install that too.
35
35
  | `alert` | `Alert`, `AlertTitle`, `AlertDescription`, `alertVariants`; `variant` `default`, `success`, `warning`, `destructive`; tone on title and icon, body stays muted; destructive is `role="alert"` |
36
36
  | `avatar` | `Avatar`, `AvatarImage`, `AvatarFallback`; `size` `sm`, `default`, `lg`; round by design |
37
37
  | `breadcrumb` | `Breadcrumb`, `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink` (`asChild` for router links), `BreadcrumbPage` (`aria-current`), `BreadcrumbSeparator`, `BreadcrumbEllipsis` |
38
- | `button` | `Button`, `buttonVariants`, `quietButtonInteraction`; `variant`, `size`, `asChild`; outline/ghost hover is brand purple + selected wash, not muted grey |
39
- | `badge` | `Badge`, `badgeVariants`; `variant`, `tone`, `asChild` |
38
+ | `button` | `Button`, `buttonVariants`, `quietButtonInteraction`; `variant`, `size`, `asChild` |
39
+ | `badge` | `Badge`, `badgeVariants`; `variant`, `tone` (`neutral` default, `success`, `warning`, `destructive`; only `variant="soft"` reads it), `asChild` |
40
40
  | `calendar` | `Calendar`, `CalendarDayButton`; React Day Picker props and `buttonVariant` |
41
41
  | `card` | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardAction`, `CardContent`, `CardFooter`; `Card` supports `flush`; `CardTitle as="h2"` (up to `h4`) makes it a section heading; root uses `min-h-min shrink-0` so flex columns do not collapse it, and `min-w-0` so nested tables scroll; `CardContent className="px-0"` overrides padding without `!` |
42
42
  | `chart` | `chartFrame(height, { width?, padding? })` (viewBox and plot box: `left`, `right`, `top`, `bottom`, `plotWidth`, `plotHeight`, `viewBox`), `niceCeil`, `niceTicks`, `scaleLinear`, `linePath`, `roundedTopBarPath`, `compactNumber`, `indexAtPlotX`, `stepIndex`, `useChartScrub({ frame, steps, min, max })` (pointer and arrow-key crosshair; Escape clears), `ChartYAxisGrid` (`frame`, `yMax`, optional `tickCount`, `format`), `ChartTooltip` (`xPct`, `yPct`; `role="status"`; flips left past 55%), `ChartTooltipRow`, `ChartLegend`, `ChartLegendKey` (`swatchClassName`, `dashed`). No chart library; draw the series in your own `<svg>`. |
43
43
  | `checkbox` | `Checkbox` |
44
44
  | `color-legend` | `ColorLegend`; `title`, `items` as `{ color, range?, label }[]`, optional `caption`, `children`, `aria-label`, `className` (overlay chrome). No attribution. |
45
45
  | `combobox` | `Combobox`; `options` as `{ value, label }[]`, `value`, `onValueChange`; optional placeholder, search, empty text, `creatable`, `normalizeCreateValue` (query → value or `null`), `createLabel`; trigger matches Input/SelectTrigger chrome; keyboard highlight scrolls into view; create is a listbox option selected with Enter |
46
- | `date-picker` | `DatePickerButton`, `toDate`, `formatLocalDate`, and the kit's own `DateRange`, `DateMatcher`, `DateLike`, and `DatePickerCalendarProps` types (no Day Picker types); `mode` `"single"` (default) or `"range"`; single `value` is `Date` or `yyyy-MM-dd` (`clearable` adds a Clear row that emits `undefined`); range `value` is `{ from, to }`; trigger matches Input chrome; optional `numberOfMonths` (range default 1 below `md`, 2 at `md+`), `minDate` / `maxDate` (days outside are disabled), `side`, `aria-labelledby`, and `aria-describedby` |
47
- | `date-time-picker` | `DateTimePickerButton`, `resolveTimeStep`, `DEFAULT_TIME_STEP`; controlled `value` is `Date` or `null`; `onValueChange` emits `Date \ |
46
+ | `date-picker` | `DatePickerButton`, `toDate`, `formatLocalDate`, and the kit's own `DateRange`, `DateMatcher`, `DateLike`, and `DatePickerCalendarProps` types (no Day Picker types); `mode` `"single"` (default) or `"range"`; single `value` is `Date` or `yyyy-MM-dd` (`clearable` adds a Clear row that emits `undefined`); range `value` is `{ from, to }`; trigger matches Input chrome; optional `numberOfMonths` (range default 1 below `md`, 2 at `md+`), `minDate` / `maxDate` (days outside are disabled), `side`, `aria-labelledby`, and `aria-describedby`; `onChange` is a deprecated alias of `onValueChange` |
47
+ | `date-time-picker` | `DateTimePickerButton`, `resolveTimeStep`, `DEFAULT_TIME_STEP`; controlled `value` is `Date` or `null`; `onValueChange` emits `Date` or `null`; `onChange` is a deprecated alias of `onValueChange` |
48
48
  | `dialog` | `Dialog`, trigger, portal, overlay, content, close, header, footer, title, description; `DialogContent` supports `showCloseButton` |
49
49
  | `dropdown-menu` | `DropdownMenu` family (trigger, content, item, checkbox/radio items, label, separator, shortcut, sub menus); item `variant="destructive"` and `inset`; for commands, not values |
50
- | `empty-panel` | `EmptyPanel`; required `children`; optional `size` (`sm`, `default`, `lg`), `muted`, `className` |
50
+ | `empty-panel` | `EmptyPanel`; required `children`; optional `size` (`sm`, `default`, `lg`; `md` is a deprecated alias of `default`), `muted`, `className` |
51
51
  | `error-boundary` | `ErrorBoundary`, `ErrorFallback`; optional `message`; retry resets, reload refreshes the page |
52
52
  | `field` | Field, label, legend, description, group, set, title, content, separator, error; `Field` supports `orientation` |
53
- | `info-tip` | `InfoTip`, `InlineTip`; required `label` (aria-label); `title`, `description`, `children`, `layout` `box` \| `inline`, `side`, `align`, `badge`, `icon`, `className`. Popover, 20px glyph, 44px hit. Hover opens only on a fine pointer. Not a Tooltip. |
53
+ | `info-tip` | `InfoTip`, `InlineTip`; required `label` (aria-label); `title`, `description`, `children`, `layout` `box` \| `inline`, `side`, `align`, `badge`, `icon`, `className`. Popover, 20px glyph (1em inline, on the text's cap height), 44px hit. After a label the glyph sits a quarter em from the text in both layouts, so leave the gap off the parent. Hover opens only on a fine pointer. Not a Tooltip. |
54
54
  | `kanban` | `KanbanBoard<T>`, `KanbanSkeleton`, `KanbanColumn` and `KanbanDropTarget` types; `columns` (`{ id, label, color?, badge?, emptyLabel? }`), `items` (`{ id, position }`), `getColumnId`, `onMove(item, toColumnId, { beforePosition, afterPosition })` for every column change or reorder, `renderCard`; optional `onCardClick` (body becomes a button), `onComplete` / `onDelete` (Finish / Remove drop zones while dragging), `isLoading` (skeleton cards on a first load, dimmed board on a refetch), `emptyLabel`, `completeLabel`, `deleteLabel`. Layout comes from `items`, so an optimistic cache update is the only state. Keep `renderCard` and `onCardClick` stable (`useCallback`) and only the changed card re-renders. Cards drag by the grip (`Drag to move`); dnd-kit edge-scrolls while dragging, and off-screen columns get fades and scroll buttons. `KanbanSkeleton` takes `columnCount` (default 4). |
55
55
  | `input`, `textarea`, `label` | `Input`, `Textarea`, `Label`; `Textarea` starts at `min-h-16` (override with `className`); `Label` `required` shows a visible marker (set `required` on the control too) |
56
56
  | `markdown` | `Markdown`, `MarkdownContent`; `content`, optional `className`. GFM via `react-markdown` + `remark-gfm` in this subpath only. Sanitized (no raw HTML; http/https/mailto/#/`/` URLs). Token styles, no typography plugin. |
@@ -67,7 +67,7 @@ theme from `next-themes`, so toast consumers install that too.
67
67
  | `separator`, `skeleton` | `Separator`, `Skeleton` |
68
68
  | `skip-link` | `SkipLink`; `href` (default `#main-content`), children (default "Skip to content"). Visually hidden until focused. |
69
69
  | `slider` | `Slider`; `value` number, `onValueChange`, optional `onValueCommit`, `min`, `max`, `step`, `disabled`, `id`, `aria-labelledby`, `className`. Radix Slider; 44px thumb hit; square track; primary fill. Name it with `aria-labelledby`. |
70
- | `sortable-table-head` | `SortableTableHead`; `sorted`, `direction` (`asc` \ |
70
+ | `sortable-table-head` | `SortableTableHead`, type `SortDirection`; `sorted` (default `false`), `direction` `asc` or `desc` (default `asc`), `onClick`; sets `aria-sort` and wraps the header text in a button. Column keys and comparators stay in the app. |
71
71
  | `spinner` | `Spinner`; `role="status"` with `aria-label` (default `Loading`) |
72
72
  | `stat-tile` | `StatTile`, `statTileGrid`, `sectionLabel`; `label`, `value`, optional `sub`, `children`, `className`. |
73
73
  | `switch` | `Switch`; settings that apply immediately; square track and thumb |
@@ -76,24 +76,24 @@ theme from `next-themes`, so toast consumers install that too.
76
76
  | `tabs` | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, `TabsNav`, `TabsNavLink`, `tabsListVariants`, `tabsTriggerVariants`; list `variant` is `default` or `line`; `TabsNavLink` is the link inside `TabsNav` (required `active`, `asChild` for a router link); `TabsNav` / `TabsList` scroll horizontally only (`overflow-y` hidden, `p-1` counted in the scroller height, edge fade, active item into view, mouse drag scrolls the row) — do not wrap them in `overflow-x-auto` |
77
77
  | `toast` | `Toaster`, `toast`; mount one `Toaster` (`position`, `closeButton` (default on), `expand`, `visibleToasts`, `duration`), then `toast(message, options)`. The stack expands on hover and while keyboard focus is inside it; collapsed toasts behind the front one are hidden, `.success`, `.info`, `.warning`, `.error`, `.loading`, `.dismiss(id?)`. Options: `id` (replaces that toast), `description`, `duration`, `action` / `cancel` as `{ label, onClick }`. Kit-owned API: no toast library is exposed |
78
78
  | `toggle` | `Toggle`, `toggleVariants`; `variant`, `size`. `selectedToggleChrome` is deprecated: `toggleVariants()` already includes it. Pressed state is the selected wash, text, and border via `data-state=on`, `aria-pressed`, and `aria-checked` |
79
- | `toggle-group` | `ToggleGroup`, `ToggleGroupItem`; `variant`, `size`. Items share `toggleVariants`, so single (radio) and multiple selections match a pressed `Toggle` |
79
+ | `toggle-group` | `ToggleGroup`, `ToggleGroupItem`; `variant`, `size`. Items share `toggleVariants`, so single (radio) and multiple selections match a pressed `Toggle`. In a single group the arrow keys select as they move focus, like radios; Tab and clicks do not. A multiple group only moves focus |
80
80
  | `theme-provider` | `ThemeProvider`, `useTheme`; optional `storageKey` (default `atlas-theme`), `enableShortcut` for the `d` toggle |
81
81
  | `tooltip` | `Tooltip`, `TooltipTrigger`, `TooltipContent`, `TooltipProvider`; opens on hover and focus; short labels only; content keeps a `max-w-xs` reading width (override with `className`) |
82
- | `form/checkbox-field` | `FormCheckboxField` (`checked`, `onCheckedChange(boolean)`, `label`, optional `description`, `controlPosition` `start` \| `end`, `meta`, `required`, `disabled`, `onBlur`) and `FormCheckboxGroupField<T>` (`label` as legend, `options` `{ value, label }[]`, `value: T[]`, `onValueChange` in option order, `className` lays out the options). |
83
- | `form/combobox-field` | `FormComboboxField`; every `Combobox` prop plus `label`, `description`, `meta`, `required`; pass the field's `onBlur`. |
84
- | `form/control-field` | `FormControlField`, type `FormControlProps`; `label`, optional `id`, `description`, `required`, `meta`, `submitted`, `showError`; `children` renders the control from `{ id, aria-invalid, aria-describedby, aria-required }` |
85
- | `form/markdown-field` | `FormMarkdownField`; `field` is `TextFieldApi`; `label`, optional `id`, `placeholder`, `rows`, `showError`, `required`. Write / preview; preview uses `Markdown`. |
82
+ | `form/checkbox-field` | `FormCheckboxField` (`checked`, `onCheckedChange(boolean)`, `label`, optional `description`, `controlPosition` `start` \| `end`, `meta`, `fieldName`, `required`, `disabled`, `onBlur`) and `FormCheckboxGroupField<T>` (`label` as legend, `options` `{ value, label }[]`, `value: T[]`, `onValueChange` in option order, `className` lays out the options). |
83
+ | `form/combobox-field` | `FormComboboxField`; every `Combobox` prop (but `id`) plus `label`, `description`, `meta`, `fieldName`, `required`; pass the field's `onBlur`. |
84
+ | `form/control-field` | `FormControlField`, type `FormControlProps`; `label`, optional `fieldName`, `description`, `required`, `meta`, `submitted`, `showError`; `children` renders the control from `{ id, data-field-name, aria-invalid, aria-describedby, aria-required }` |
85
+ | `form/markdown-field` | `FormMarkdownField`; `field` is `TextFieldApi`; `label`, `placeholder`, `rows`, `showError`, `required`. Write / preview; preview uses `Markdown`. |
86
86
  | `form/text-field` | `FormTextField`, type `TextFieldApi`; controlled field, label, multiline, description, input constraints; `required` also marks the label |
87
- | `form/select-field` | `FormSelectField`; `value`, `onValueChange`, `label`, optional `id` (default generated; pass the field name for `focusFirstInvalid`), `meta`, placeholder, and `required`; select item children |
88
- | `form/date-picker-field` | `FormDatePickerField`; `value`, `onValueChange`, `label`, optional `id` (default generated), `meta`, placeholder, `required`, `clearable` (a Clear row that emits `undefined`), `minDate` / `maxDate`, and `className` (forwarded to the trigger) |
89
- | `form/date-time-picker-field` | `FormDateTimePickerField`; `value` is `Date` or `null`; `onValueChange` emits `Date` or `null`; optional `id` (default generated), `meta`, placeholder, `timeStep`, `required`, `minDate` / `maxDate`, and `className` (forwarded to the trigger) |
87
+ | `form/select-field` | `FormSelectField`; `value`, `onValueChange`, `label`, optional `fieldName` (for `focusFirstInvalid`), `meta`, placeholder, and `required`; select item children |
88
+ | `form/date-picker-field` | `FormDatePickerField`; `value`, `onValueChange`, `label`, optional `fieldName`, `meta`, placeholder, `required`, `clearable` (a Clear row that emits `undefined`), `minDate` / `maxDate`, and `className` (forwarded to the trigger) |
89
+ | `form/date-time-picker-field` | `FormDateTimePickerField`; `value` is `Date` or `null`; `onValueChange` emits `Date` or `null`; optional `fieldName`, `meta`, placeholder, `timeStep`, `required`, `minDate` / `maxDate`, and `className` (forwarded to the trigger) |
90
90
  | `form/field-error` | `FieldError` (optional `id`), `getFieldErrorMessage`; types `FieldErrorMessage`, `FieldMetaState`, `ErrorTimingProps` |
91
- | `form/focus-first-invalid` | `focusFirstInvalid(formId)`: an `onSubmitInvalid` handler that focuses the first control in the form whose field (matched by control id, so pass `id={field.name}`) has errors; type `InvalidSubmitEvent` |
91
+ | `form/focus-first-invalid` | `focusFirstInvalid(formId)`: an `onSubmitInvalid` handler that focuses the first control in the form whose field has errors, matched by the control's `data-field-name` (`field.name` for the text and Markdown fields, the `fieldName` prop on the others) and never outside that form; type `InvalidSubmitEvent` |
92
92
  | `form/feedback-field` | `FormFeedbackField`; nullable `message` |
93
- | `form/toggle-group-field` | `FormToggleGroupField<T>`; one choice from `options` `{ value, label }[]` as a joined outline toggle group named by its label; `value` (`""` for none), `onValueChange(T)` (pressing the active item keeps it), optional `description`, `meta`, `size` (`sm` default). |
94
- | `next/nav-link` | `NavLink`; Next Link props and required `active` boolean; the `nav-link` pill with `asChild` around a Next `Link`; requires Next.js 16 |
93
+ | `form/toggle-group-field` | `FormToggleGroupField<T>`; one choice from `options` `{ value, label }[]` as a joined outline toggle group named by its label; `value` (`""` for none), `onValueChange(T)` (pressing the active item keeps it), optional `description`, `meta`, `fieldName`, `size` (`sm` default). |
94
+ | `next/nav-link` | `NavLink`; Next Link props and required `active` boolean; the `nav-link` pill with `asChild` around a Next `Link` |
95
95
  | `next/period-stepper` | `PeriodStepper` with prefetched Next links: `previousHref`, `nextHref`, optional `todayHref`. |
96
- | `next/tabs-nav-link` | `TabsNavLink`; Next Link props and required `active` boolean; the `tabs` link with `asChild` around a Next `Link`; requires Next.js 16 |
96
+ | `next/tabs-nav-link` | `TabsNavLink`; Next Link props and required `active` boolean; the `tabs` link with `asChild` around a Next `Link` |
97
97
  | `utils` | `cn`, `onInputChange` |
98
98
  | `theme.css` | Tailwind 4 theme; installation in [README](./README.md) |
99
99
 
@@ -121,7 +121,7 @@ each part carries a `data-slot` for parent selectors such as
121
121
  | `DialogContent` | `overlayClassName`, `closeButtonClassName` | `dialog-close-button` (the built-in X; your own `DialogClose` keeps `dialog-close`) |
122
122
  | `SelectTrigger` | a `className` height replaces `size` | `select-icon`, `select-viewport`, `select-item-indicator` |
123
123
  | `TooltipContent` | `arrowClassName` | `tooltip-arrow` |
124
- | `Combobox` | `contentClassName` (panel, default trigger width), `listClassName` (default `max-h-60`); button props go to trigger | `combobox-search`, `combobox-list`, `combobox-option` |
124
+ | `Combobox` | `contentClassName` (panel, default at least the trigger width, wider for a long option), `listClassName` (default `max-h-60`); button props go to trigger | `combobox-search`, `combobox-list`, `combobox-option` |
125
125
  | `SearchPicker` | `listClassName` (default `max-h-60`); div props go to the root | `search-picker-option`, `search-picker-empty` |
126
126
  | `InfoTip` | `contentClassName` (default `w-72`); button props go to the trigger | |
127
127
  | `KanbanBoard` | `scrollerClassName`, `columnClassName` (default `flex-1 basis-72 min-w-72` and a drop-area min height; columns grow to fill the board and keep an 18rem minimum so a long row still scrolls), `cardClassName` (cards and the drag overlay); div props go to the root | `kanban-board`, `kanban-scroller`, `kanban-column`, `kanban-column-header`, `kanban-column-empty`, `kanban-card`, `kanban-card-handle`, `kanban-card-body`, `kanban-drag-overlay`, `kanban-action-rail`, `kanban-action-zone`, `kanban-scroll-button`, `kanban-overflow-fade`, `kanban-skeleton` |
@@ -168,12 +168,16 @@ for a Write / Preview GFM body. `FormSelectField`, `FormDatePickerField`, and
168
168
  `FormDateTimePickerField` accept the same error meta shape.
169
169
  `FormDatePickerField` takes a `Date` or `yyyy-MM-dd` string and calls
170
170
  `onValueChange` with `Date | undefined`. `FormDateTimePickerField` takes a
171
- `Date` or `null` and calls `onValueChange` with `Date | null`. Pass
172
- `id={field.name}` to the select and date fields (the text field defaults to
173
- it) so a field is addressable by name, for example by `focusFirstInvalid`
174
- from `form/focus-first-invalid`, which focuses the first field with errors
175
- when a submit fails. `FieldError`
176
- accepts either `message` or pre-resolved `text`, and an `id` to point the
171
+ `Date` or `null` and calls `onValueChange` with `Date | null`. Every
172
+ `form/*` field generates its own control id, so two forms on a page can
173
+ share a field name (login and sign-up `email`) and each label, hint, and
174
+ error still points at its own control; there is no `id` prop. To make a
175
+ field addressable by name, for example by `focusFirstInvalid` from
176
+ `form/focus-first-invalid`, which focuses the first field with errors when
177
+ a submit fails, pass `fieldName={field.name}` to every field that is not
178
+ bound to a `field` (the text and Markdown fields read `field.name`
179
+ themselves); the name is put on the control as `data-field-name`.
180
+ `FieldError` accepts either `message` or pre-resolved `text`, and an `id` to point the
177
181
  control's `aria-describedby` at. Every `form/*` field does that for you: the
178
182
  error text (and `FormTextField`'s `description`) is the control's accessible
179
183
  description. The date fields label their trigger with `aria-labelledby` (the
@@ -191,9 +195,11 @@ submit. Meta without `isTouched` shows errors as soon as they exist.
191
195
  `FormSelectField` and both date fields included, takes `submitted` and
192
196
  `showError`.
193
197
 
194
- `FormControlField` gives a control the kit does not ship the same label,
195
- required marker, hint, error, ids, and error timing. `children` is a
196
- function that receives the props to spread on the control:
198
+ `FormControlField` gives a control the kit does not ship (a colour picker,
199
+ say) the same label, required marker, hint, error, ids, and error timing.
200
+ Use it rather than `Field` and `FieldLabel`, whose `gap-3` spacing does not
201
+ match the form fields. `children` is a function that receives the props to
202
+ spread on the control:
197
203
 
198
204
  ```tsx
199
205
  <div className="grid grid-cols-2 items-start gap-3">
@@ -211,8 +217,10 @@ function that receives the props to spread on the control:
211
217
  </div>
212
218
  ```
213
219
 
214
- Errors sit below the control and take no space until they show, so use
215
- `items-start` on a row of fields to keep the controls on one top edge.
220
+ Each `form/*` field stacks label, hint, control, and error with `gap-1.5`.
221
+ The error sits below the control and takes no space until it shows, so use
222
+ `items-start` on a row of fields, as above, to keep the controls on one top
223
+ edge; the default `stretch` is fine for a single column.
216
224
 
217
225
  Set `required` on a `form/*` field to mark its label with a visible `*`.
218
226
  The marker is hidden from screen readers, which announce the control's own
@@ -246,9 +254,6 @@ the trigger text's id (a `<label for>` alone names the button by the label
246
254
  only).
247
255
  `FormDatePickerField` forwards `className` to that trigger.
248
256
 
249
- Both pickers took `onChange` before 1.0; it still works as a deprecated
250
- alias until 2.0.
251
-
252
257
  `DateTimePickerButton` is the date-and-time control. `value` is a `Date` or
253
258
  `null`; `onValueChange` receives `Date | null`. The trigger matches the same
254
259
  form-control chrome. The popover is one month, a labelled Hour and Minute
@@ -261,11 +266,6 @@ named dialog ("Choose date and time") that focuses the selected day or
261
266
  today, and the trigger takes `aria-labelledby` and `aria-describedby`. `FormDateTimePickerField` forwards `className`
262
267
  and `timeStep` to that control.
263
268
 
264
- Outline and ghost `Button` hover uses `border-selected-foreground`,
265
- `text-selected-foreground`, and `bg-selected`; keyboard focus adds the shared
266
- solid `ring-ring`. They do not use a muted grey fill, including when the
267
- control sits on a selected row (for example "Clear selection").
268
-
269
269
  ```tsx
270
270
  import { DateTimePickerButton } from "atlas-react-kit/date-time-picker"
271
271
 
@@ -370,7 +370,7 @@ export function AppNav({ pathname }: { pathname: string }) {
370
370
  ```
371
371
 
372
372
  In-page view switches that still belong in the URL use `TabsNav`, not
373
- `NavLink`.
373
+ `NavLink`; `Tabs` is for `TabsContent` panels that stay on one page.
374
374
 
375
375
  ## URL navigation in Next.js
376
376
 
package/DESIGN_RULES.md CHANGED
@@ -17,7 +17,8 @@ Selected or active navigation, tabs, toggles, chips, rows, and labels use
17
17
  `bg-selected text-selected-foreground` plus a `border-selected-foreground`
18
18
  border or bar: the wash alone is too faint to show state (1.2:1 against the
19
19
  page). Outline and ghost buttons hover with `border-selected-foreground`,
20
- `text-selected-foreground`, and the selected wash, not a muted grey fill.
20
+ `text-selected-foreground`, and the selected wash, not a muted grey fill,
21
+ even on a selected row (a "Clear selection" button, for example).
21
22
  Table row hover can stay `hover:bg-muted`. The shipped controls apply this
22
23
  distinction.
23
24
 
@@ -26,8 +27,8 @@ it is the primary purple in light mode and a lighter purple in dark mode.
26
27
  `text-primary` as text fails contrast on the dark background (about 3.6:1).
27
28
 
28
29
  Use `success`, `warning`, and `destructive` for good, caution, and bad states.
29
- For tinted status chips, use `Badge variant="soft"` with `tone="success"`,
30
- `"warning"`, or `"destructive"`; each meets AA as text on the page, a card, and
30
+ For tinted status chips, use `Badge variant="soft"` with a `tone` (listed in
31
+ its COMPONENTS.md row); each tone meets AA as text on the page, a card, and
31
32
  its own soft tint in both themes. Status tones have no paired foreground token;
32
33
  avoid placing text on solid status fills. Application-specific identity colors
33
34
  belong to the application.
@@ -59,8 +60,8 @@ in COMPONENTS.md.
59
60
  - `variant="default"` is the plain look; other variants name what they mean
60
61
  (`outline`, `ghost`, `destructive`).
61
62
 
62
- Deprecated aliases (`onChange` on the date pickers, `EmptyPanel`
63
- `size="md"`) keep working until 2.0.
63
+ Deprecated props and exports say so, with the replacement, in their JSDoc
64
+ and their row in [COMPONENTS.md](./COMPONENTS.md).
64
65
 
65
66
  ## Accessibility
66
67
 
@@ -118,14 +119,11 @@ still read normal words.
118
119
  Primitives have square corners. Reserve `rounded-full` for dots, avatars,
119
120
  other circles, and the toast close badge. Card and dialog titles already use
120
121
  uppercase, tracked `font-heading` styling. Toasts are overlay popovers with
121
- the same square corners and Alert left/vertical padding (`pl-4 py-3`). Right
122
- padding is `pr-6` so copy clears the floating close badge. Icon-to-text is
123
- `gap-x-2.5` so the status icon and copy read as one unit. The close control is
124
- a circular badge centred on the top-right corner (half outside the border),
122
+ the same square corners and Alert's left padding. The close control is a
123
+ circular badge centred on the top-right corner (half outside the border),
125
124
  not a control in the text row. A plain `toast()` has no icon — status icons
126
125
  belong to `success`, `info`, `warning`, `error`, and `loading`. Tone colors
127
- the icon, title, and border, not the body. Width is the content up to 356px,
128
- or the viewport minus gutters on small screens.
126
+ the icon, title, and border, not the body.
129
127
 
130
128
  Use `EmptyPanel` for empty states, `TableBodySkeleton` for loading table bodies,
131
129
  and `<Card flush>` for an edge-to-edge list or table. `Table` scrolls wide
@@ -163,32 +161,8 @@ or its `data-slot`.
163
161
  `text-base md:text-sm`, `border-input`, and `shadow-xs`. Do not restyle those
164
162
  triggers as outline buttons in application code.
165
163
 
166
- `form/*` fields stack label, hint, control, and error with `gap-1.5`, and the
167
- error sits below the control. It takes no space until it shows, so a row of
168
- fields keeps its controls on one top edge only with `items-start`
169
- (`grid grid-cols-2 items-start gap-3`); the default `stretch` is fine for a
170
- single column. Wrap a control the kit has no form field for (a colour
171
- picker) in `FormControlField` instead of `Field` and `FieldLabel`, whose
172
- `gap-3` spacing does not match. A field shows its error once the person has
173
- touched it (`meta.isTouched`), never on a field they have not reached yet.
174
- `submitted` is for form libraries that do not mark every field touched on
175
- submit; TanStack Form does, so its users pass `meta` only.
176
-
177
- Use `Tabs` with actual `TabsContent` panels. For URL view switches, use a labelled
178
- `TabsNav` of `TabsNavLink`s with `active` from the app. App-shell destinations
179
- (Calendar, Tasks, Settings) use `NavLink` from `nav-link`: a pill with `active`
180
- from the app, not tabs. Both links take `asChild` for a router link, or come
181
- as Next adapters (`next/tabs-nav-link`, `next/nav-link`); `isNavActive`
182
- compares the pathname with a link's `href`. `TabsNav` and `TabsList` scroll overflowing
183
- labels inside themselves, like `Table`. Do not wrap them in another
184
- horizontal scroller: that pairing adds a stray vertical scrollbar.
185
-
186
- Use the `form/*` fields (one row each in COMPONENTS.md) for portable form
187
- controls, and `FieldError` for field messages. `Combobox` (popover)
188
- and `SearchPicker` (always-visible list), `DatePickerButton` (including
189
- `mode="range"`), `DateTimePickerButton`, `Slider`, `Meter` (scale with ticks;
190
- `Progress` is a plain 0–100 job bar), `InfoTip` (longer than `Tooltip`),
191
- `Markdown`, `ColorLegend`, `StatTile`, `ThemeProvider`, `ErrorBoundary`, and
192
- `SortableTableHead` are first-class kit pieces. Domain data and application
193
- shells stay with the application: currency or timezone option lists,
194
- query-library error views, and WYSIWYG editors.
164
+ Build forms from the `form/*` fields; [Forms](./COMPONENTS.md#forms) in
165
+ COMPONENTS.md says how they lay out and when they show errors. Pick other
166
+ components by their rows there. Domain data and application shells stay
167
+ with the application: currency or timezone option lists, query-library
168
+ error views, and WYSIWYG editors.
package/README.md CHANGED
@@ -4,11 +4,11 @@ Shared React components and a Tailwind theme with square corners, semantic
4
4
  colors, and distinct selected states. From 1.0 the API is stable: breaking
5
5
  changes to stable components only ship in a major release (see
6
6
  [CONTRIBUTING.md](./CONTRIBUTING.md#versions-and-releases)).
7
- React 19, Tailwind 4, and Node >=20.19 are required. Next 16 is needed only
8
- when using `next/nav-link`, `next/period-stepper`, or `next/tabs-nav-link`.
7
+ The React, Tailwind, and Node versions it supports are `peerDependencies`
8
+ and `engines` in `package.json`.
9
9
 
10
10
  ```sh
11
- npm install --save-exact atlas-react-kit@1.2.0
11
+ npm install --save-exact atlas-react-kit
12
12
  ```
13
13
 
14
14
  Import components by subpath:
@@ -42,9 +42,10 @@ this example assumes it is one directory below the project root.
42
42
  ```
43
43
 
44
44
  The theme includes animations and shared light/dark tokens. Wrap the tree with
45
- `ThemeProvider` from `atlas-react-kit/theme-provider` (install
46
- `next-themes@^0.4.6` first), or set the `dark` class on an ancestor to activate
47
- dark colors. Supply your own font configuration;
45
+ `ThemeProvider` from `atlas-react-kit/theme-provider` (install `next-themes`
46
+ from the [install table](./COMPONENTS.md#optional-peer-dependencies) first),
47
+ or set the `dark` class on an ancestor to activate dark colors. Supply your
48
+ own font configuration;
48
49
  `font-heading` uses the configured sans font. Application-specific tokens stay
49
50
  with the application.
50
51
 
package/dist/alert.js CHANGED
@@ -1,19 +1,20 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { cva } from "class-variance-authority";
4
+ import { descriptionText, statusTone } from "./internal/styles.js";
4
5
  import { cn } from "./utils.js";
5
6
  /**
6
7
  * Status tone sits on the title and icon. The description stays
7
8
  * `text-muted-foreground` on the card face, so body text keeps its contrast
8
9
  * (the destructive tint is too faint for text in light mode).
9
10
  */
10
- const alertVariants = cva("relative grid grid-cols-[0_1fr] items-start gap-y-1 rounded-none border bg-card px-4 py-3 text-sm text-card-foreground has-[>svg]:grid-cols-[--spacing(4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5", {
11
+ const alertVariants = cva("relative grid grid-cols-[0_1fr] items-start gap-y-1 rounded-none border bg-card px-4 py-3 text-sm text-card-foreground has-[>svg]:grid-cols-[--spacing(4)_1fr] has-[>svg]:gap-x-3 *:data-[slot=alert-title]:text-(--status-tone) [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-(--status-tone)", {
11
12
  variants: {
12
13
  variant: {
13
- default: "[&>svg]:text-foreground",
14
- success: "border-success/40 *:data-[slot=alert-title]:text-success [&>svg]:text-success",
15
- warning: "border-warning/40 *:data-[slot=alert-title]:text-warning [&>svg]:text-warning",
16
- destructive: "border-destructive/40 *:data-[slot=alert-title]:text-destructive [&>svg]:text-destructive",
14
+ default: statusTone.neutral,
15
+ success: statusTone.success,
16
+ warning: statusTone.warning,
17
+ destructive: statusTone.destructive,
17
18
  },
18
19
  },
19
20
  defaultVariants: {
@@ -32,6 +33,6 @@ function AlertTitle({ className, ...props }) {
32
33
  return (_jsx("div", { "data-slot": "alert-title", className: cn("col-start-2 min-h-4 text-xs font-semibold tracking-wider uppercase", className), ...props }));
33
34
  }
34
35
  function AlertDescription({ className, ...props }) {
35
- return (_jsx("div", { "data-slot": "alert-description", className: cn("col-start-2 grid justify-items-start gap-1 text-sm leading-relaxed text-muted-foreground [&_p]:leading-relaxed", className), ...props }));
36
+ return (_jsx("div", { "data-slot": "alert-description", className: cn("col-start-2 grid justify-items-start gap-1", descriptionText, "[&_p]:leading-relaxed", className), ...props }));
36
37
  }
37
38
  export { Alert, AlertDescription, AlertTitle, alertVariants };
package/dist/calendar.js CHANGED
@@ -4,7 +4,8 @@ import * as React from "react";
4
4
  import { DayPicker, getDefaultClassNames, useDayPicker, } from "react-day-picker";
5
5
  import { cn } from "./utils.js";
6
6
  import { Button, buttonVariants } from "./button.js";
7
- import { ariaDisabledState } from "./internal/styles.js";
7
+ import { calendarSize } from "./internal/calendar-size.js";
8
+ import { ariaDisabledState, focusRingWithin } from "./internal/styles.js";
8
9
  import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon, } from "lucide-react";
9
10
  /** Previous / next month buttons share one look. */
10
11
  const navButtonClassName = cn("size-(--cell-size) p-0 select-none", ariaDisabledState);
@@ -38,7 +39,7 @@ const calendarComponents = {
38
39
  function Calendar({ className, classNames, showOutsideDays = true, captionLayout = "label", buttonVariant = "ghost", locale, formatters, labels, components, ...props }) {
39
40
  const defaultClassNames = getDefaultClassNames();
40
41
  const navButton = cn(buttonVariants({ variant: buttonVariant }), navButtonClassName);
41
- return (_jsx(DayPicker, { showOutsideDays: showOutsideDays, className: cn("group/calendar bg-background p-3 [--cell-radius:0] [--cell-size:--spacing(8)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent", String.raw `rtl:**:[.rdp-button\_next>svg]:rotate-180`, String.raw `rtl:**:[.rdp-button\_previous>svg]:rotate-180`, className), captionLayout: captionLayout, locale: locale, formatters: {
42
+ return (_jsx(DayPicker, { showOutsideDays: showOutsideDays, className: cn("group/calendar bg-background p-(--calendar-padding) [--cell-radius:0] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent", calendarSize, String.raw `rtl:**:[.rdp-button\_next>svg]:rotate-180`, String.raw `rtl:**:[.rdp-button\_previous>svg]:rotate-180`, className), captionLayout: captionLayout, locale: locale, formatters: {
42
43
  formatMonthDropdown: (date) => date.toLocaleString(locale?.code, { month: "short" }),
43
44
  ...formatters,
44
45
  }, labels: {
@@ -70,7 +71,7 @@ function Calendar({ className, classNames, showOutsideDays = true, captionLayout
70
71
  dropdowns: cn("flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium", defaultClassNames.dropdowns),
71
72
  // The native select is invisible over the caption, so its wrapper
72
73
  // shows keyboard focus with the kit's solid ring.
73
- dropdown_root: cn("relative rounded-(--cell-radius) has-focus-visible:ring-2 has-focus-visible:ring-ring", defaultClassNames.dropdown_root),
74
+ dropdown_root: cn("relative rounded-(--cell-radius)", focusRingWithin, defaultClassNames.dropdown_root),
74
75
  dropdown: cn("absolute inset-0 bg-popover opacity-0", defaultClassNames.dropdown),
75
76
  caption_label: cn("font-medium select-none", captionLayout === "label"
76
77
  ? "text-sm"
@@ -107,6 +108,6 @@ function CalendarDayButton({ className, day, modifiers, locale, ...props }) {
107
108
  return (_jsx(Button, { ref: ref, variant: "ghost", size: "icon", "data-day": day.date.toLocaleDateString(locale?.code), "data-selected-single": modifiers.selected &&
108
109
  !modifiers.range_start &&
109
110
  !modifiers.range_end &&
110
- !modifiers.range_middle, "data-range-start": modifiers.range_start, "data-range-end": modifiers.range_end, "data-range-middle": modifiers.range_middle, className: cn("relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-2 group-data-[focused=true]/day:ring-ring data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-selected data-[range-middle=true]:text-selected-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70", defaultClassNames.day, className), ...props }));
111
+ !modifiers.range_middle, "data-range-start": modifiers.range_start, "data-range-end": modifiers.range_end, "data-range-middle": modifiers.range_middle, className: cn("relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-selected data-[range-middle=true]:text-selected-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70", defaultClassNames.day, className), ...props }));
111
112
  }
112
113
  export { Calendar, CalendarDayButton };
package/dist/card.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
+ import { descriptionText } from "./internal/styles.js";
3
4
  import { cn } from "./utils.js";
4
5
  /**
5
6
  * Size and flush padding live in `--card-p` (set here, read from `theme.css`
@@ -32,7 +33,7 @@ function CardTitle({ as: Component = "div", className, ...props }) {
32
33
  return (_jsx(Component, { "data-slot": "card-title", className: cn("font-heading text-base font-semibold tracking-wider uppercase", className), ...props }));
33
34
  }
34
35
  function CardDescription({ className, ...props }) {
35
- return (_jsx("div", { "data-slot": "card-description", className: cn("text-sm leading-relaxed text-muted-foreground", className), ...props }));
36
+ return (_jsx("div", { "data-slot": "card-description", className: cn(descriptionText, className), ...props }));
36
37
  }
37
38
  function CardAction({ className, ...props }) {
38
39
  return (_jsx("div", { "data-slot": "card-action", className: cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className), ...props }));
@@ -21,11 +21,12 @@ type ComboboxProps = {
21
21
  disabled?: boolean;
22
22
  /** Goes on the trigger, so a `<Label htmlFor>` names it. Generated when omitted. */
23
23
  id?: string;
24
- /** Classes for the trigger, e.g. its width; the list matches it. */
24
+ /** Classes for the trigger, e.g. its width; the list is at least as wide. */
25
25
  className?: string;
26
26
  /**
27
- * Classes for the popover panel, e.g. a width other than the trigger's.
28
- * Defaults to the trigger width.
27
+ * Classes for the popover panel, e.g. a fixed width. By default the panel
28
+ * is at least as wide as the trigger and grows to fit the longest option,
29
+ * up to the space the viewport leaves.
29
30
  */
30
31
  contentClassName?: string;
31
32
  /** Marks the trigger invalid, drawing the error border. */
package/dist/combobox.js CHANGED
@@ -5,10 +5,10 @@ import { ChevronDownIcon } from "lucide-react";
5
5
  import { Button } from "./button.js";
6
6
  import { Input } from "./input.js";
7
7
  import { formControlTriggerClassName } from "./internal/form-control.js";
8
- import { handleListboxKey, listboxOptionId, } from "./internal/listbox-navigation.js";
9
- import { ListboxEmptyStatus, PickerOption } from "./internal/listbox-option.js";
8
+ import { handleListboxKey } from "./internal/listbox-navigation.js";
9
+ import { Listbox, ListboxEmptyStatus, listboxInputAria, PickerOption, } from "./internal/listbox-option.js";
10
10
  import { createSearchIndex, filterSearchIndex, foldSearchText, normalizeSearchQuery, } from "./internal/search.js";
11
- import { listboxScroll } from "./internal/styles.js";
11
+ import { emptyValueText } from "./internal/styles.js";
12
12
  import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
13
13
  import { cn, onInputChange } from "./utils.js";
14
14
  const CREATE_OPTION_KEY = "__atlas-combobox-create";
@@ -50,7 +50,7 @@ function resolveCreateValue(query, existingValues, normalize) {
50
50
  * Opt-in `creatable` appends a create action when the query is not already
51
51
  * an option; choosing it calls `onValueChange` like a normal selection.
52
52
  */
53
- export function Combobox({ options, value, onValueChange, placeholder = "Select", searchPlaceholder = "Search", emptyText = "No results", disabled, id, className, listClassName, contentClassName, "aria-invalid": ariaInvalid, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, creatable = false, normalizeCreateValue = defaultNormalizeCreateValue, createLabel = defaultCreateLabel, ...props }) {
53
+ export function Combobox({ options, value, onValueChange, placeholder = "Select", searchPlaceholder = "Search", emptyText, disabled, id, className, listClassName, contentClassName, "aria-invalid": ariaInvalid, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, creatable = false, normalizeCreateValue = defaultNormalizeCreateValue, createLabel = defaultCreateLabel, ...props }) {
54
54
  const [open, setOpen] = React.useState(false);
55
55
  const [query, setQuery] = React.useState("");
56
56
  const [highlight, setHighlight] = React.useState(0);
@@ -74,12 +74,7 @@ export function Combobox({ options, value, onValueChange, placeholder = "Select"
74
74
  label,
75
75
  ])
76
76
  : []), [creatable, searchIndex]);
77
- const filtered = React.useMemo(() => {
78
- const needle = normalizeSearchQuery(query);
79
- if (!needle)
80
- return resolvedOptions;
81
- return filterSearchIndex(searchIndex, needle);
82
- }, [resolvedOptions, searchIndex, query]);
77
+ const filtered = React.useMemo(() => filterSearchIndex(searchIndex, normalizeSearchQuery(query)), [searchIndex, query]);
83
78
  const createOption = React.useMemo(() => {
84
79
  const createValue = creatable
85
80
  ? resolveCreateValue(query, existingValues, normalizeCreateValue)
@@ -125,17 +120,13 @@ export function Combobox({ options, value, onValueChange, placeholder = "Select"
125
120
  };
126
121
  const onSearchKeyDown = (event) => {
127
122
  handleListboxKey(event, {
123
+ items: list,
128
124
  highlight,
129
- length: list.length,
130
125
  setHighlight,
131
- onEnter: (index) => {
132
- const option = list[index];
133
- if (option)
134
- selectOption(option);
135
- },
126
+ onPick: selectOption,
136
127
  });
137
128
  };
138
- return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { ...props, id: triggerId, type: "button", variant: "outline", role: "combobox", "aria-expanded": open, "aria-controls": listId, "aria-invalid": ariaInvalid, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, disabled: disabled, onKeyDown: onTriggerKeyDown, className: cn(formControlTriggerClassName, "justify-between", !selected && "text-muted-foreground", className), children: [_jsx("span", { className: "truncate", children: selected?.label ?? placeholder }), _jsx(ChevronDownIcon, { "data-icon": "inline-end", className: "opacity-50" })] }) }), _jsxs(PopoverContent, { align: "start", className: cn("w-[var(--radix-popover-trigger-width)] gap-0 rounded-none p-0", contentClassName), children: [_jsx("div", { "data-slot": "combobox-search", className: "border-b p-2", children: _jsx(Input, { value: query, onChange: onQueryChange, onKeyDown: onSearchKeyDown, placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-autocomplete": "list", "aria-controls": listId, "aria-activedescendant": activeOption ? listboxOptionId(listId, highlight) : undefined }) }), _jsx("ul", { id: listId, role: "listbox", "data-slot": "combobox-list", "aria-label": ariaLabelledBy ? undefined : ariaLabel, "aria-labelledby": ariaLabelledBy ?? (ariaLabel ? undefined : triggerId), className: empty ? "sr-only" : cn(listboxScroll, "p-1", listClassName), children: list.map((option, index) => {
129
+ return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { ...props, id: triggerId, type: "button", variant: "outline", role: "combobox", "aria-expanded": open, "aria-controls": listId, "aria-invalid": ariaInvalid, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "data-empty": !selected, disabled: disabled, onKeyDown: onTriggerKeyDown, className: cn(formControlTriggerClassName, "justify-between", emptyValueText, className), children: [_jsx("span", { className: "truncate", children: selected?.label ?? placeholder }), _jsx(ChevronDownIcon, { "data-icon": "inline-end", className: "text-muted-foreground" })] }) }), _jsxs(PopoverContent, { align: "start", className: cn("w-max min-w-(--radix-popover-trigger-width) gap-0 rounded-none p-0", contentClassName), children: [_jsx("div", { "data-slot": "combobox-search", className: "border-b p-2", children: _jsx(Input, { value: query, onChange: onQueryChange, onKeyDown: onSearchKeyDown, placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listId, ...listboxInputAria(listId, activeOption ? highlight : undefined) }) }), _jsx(Listbox, { id: listId, empty: empty, "data-slot": "combobox-list", "aria-label": ariaLabelledBy ? undefined : ariaLabel, "aria-labelledby": ariaLabelledBy ?? (ariaLabel ? undefined : triggerId), className: cn("p-1", listClassName), children: list.map((option, index) => {
139
130
  const isCreate = createOption != null && option === createOption;
140
131
  const isSelected = option.value === value && !isCreate;
141
132
  return (_jsx(PickerOption, { "data-slot": "combobox-option", listId: listId, index: index, active: index === highlight, selected: isSelected, showCheck: true, item: option, getLabel: getOptionLabel, onHighlight: setHighlight, onSelect: selectOption }, isCreate ? CREATE_OPTION_KEY : option.value));