atlas-react-kit 1.1.0 → 1.2.1

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 (67) hide show
  1. package/COMPONENTS.md +22 -26
  2. package/DESIGN_RULES.md +14 -40
  3. package/README.md +7 -6
  4. package/dist/accordion.js +2 -2
  5. package/dist/alert.js +6 -5
  6. package/dist/badge.js +5 -2
  7. package/dist/breadcrumb.js +2 -2
  8. package/dist/button.js +7 -4
  9. package/dist/calendar.js +5 -4
  10. package/dist/combobox.js +7 -13
  11. package/dist/date-time-picker.js +2 -1
  12. package/dist/dropdown-menu.js +4 -4
  13. package/dist/field.d.ts +6 -3
  14. package/dist/field.js +3 -2
  15. package/dist/form/checkbox-field.d.ts +1 -1
  16. package/dist/form/checkbox-field.js +8 -11
  17. package/dist/form/combobox-field.js +2 -2
  18. package/dist/form/control-field.js +2 -7
  19. package/dist/form/date-picker-field.js +5 -2
  20. package/dist/form/date-time-picker-field.js +5 -2
  21. package/dist/form/field-error.d.ts +3 -3
  22. package/dist/form/field-shell.d.ts +36 -31
  23. package/dist/form/field-shell.js +41 -34
  24. package/dist/form/markdown-field.js +2 -2
  25. package/dist/form/select-field.js +2 -2
  26. package/dist/form/text-field.js +2 -4
  27. package/dist/form/toggle-group-field.js +2 -2
  28. package/dist/info-tip.d.ts +5 -4
  29. package/dist/info-tip.js +15 -14
  30. package/dist/internal/active-link.d.ts +7 -0
  31. package/dist/internal/active-link.js +9 -0
  32. package/dist/internal/calendar-size.d.ts +5 -0
  33. package/dist/internal/calendar-size.js +5 -0
  34. package/dist/internal/form-control.d.ts +1 -1
  35. package/dist/internal/form-control.js +2 -1
  36. package/dist/internal/listbox-navigation.d.ts +0 -5
  37. package/dist/internal/listbox-navigation.js +0 -7
  38. package/dist/internal/listbox-option.d.ts +16 -3
  39. package/dist/internal/listbox-option.js +22 -5
  40. package/dist/internal/overlay-styles.d.ts +10 -7
  41. package/dist/internal/overlay-styles.js +10 -7
  42. package/dist/internal/search.d.ts +9 -1
  43. package/dist/internal/search.js +14 -5
  44. package/dist/internal/styles.d.ts +43 -0
  45. package/dist/internal/styles.js +43 -0
  46. package/dist/internal/tabs-styles.d.ts +8 -7
  47. package/dist/internal/tabs-styles.js +15 -13
  48. package/dist/internal/use-media-query.d.ts +5 -0
  49. package/dist/internal/use-media-query.js +5 -0
  50. package/dist/kanban.d.ts +1 -1
  51. package/dist/kanban.js +6 -4
  52. package/dist/label.js +2 -1
  53. package/dist/lazy-calendar.js +3 -2
  54. package/dist/nav-link.js +2 -1
  55. package/dist/popover.js +4 -3
  56. package/dist/search-picker.d.ts +1 -1
  57. package/dist/search-picker.js +7 -12
  58. package/dist/select.js +6 -6
  59. package/dist/skip-link.js +2 -1
  60. package/dist/tabs.d.ts +3 -3
  61. package/dist/tabs.js +7 -6
  62. package/dist/toast.js +11 -10
  63. package/dist/toggle.d.ts +2 -0
  64. package/dist/toggle.js +8 -3
  65. package/dist/tooltip.js +2 -2
  66. package/package.json +1 -1
  67. 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. 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 |
@@ -75,7 +75,7 @@ theme from `next-themes`, so toast consumers install that too.
75
75
  | `table-body-skeleton` | `TableBodySkeleton`; `columns`, optional `rows` (default 5) |
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
- | `toggle` | `Toggle`, `toggleVariants`, `selectedToggleChrome`; `variant`, `size`. Pressed state is the selected wash, text, and border via `data-state=on`, `aria-pressed`, and `aria-checked` |
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
79
  | `toggle-group` | `ToggleGroup`, `ToggleGroupItem`; `variant`, `size`. Items share `toggleVariants`, so single (radio) and multiple selections match a pressed `Toggle` |
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`) |
@@ -91,9 +91,9 @@ theme from `next-themes`, so toast consumers install that too.
91
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` |
92
92
  | `form/feedback-field` | `FormFeedbackField`; nullable `message` |
93
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 |
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
 
@@ -124,7 +124,7 @@ each part carries a `data-slot` for parent selectors such as
124
124
  | `Combobox` | `contentClassName` (panel, default trigger width), `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
- | `KanbanBoard` | `scrollerClassName`, `columnClassName` (default `w-72 min-w-72` and a drop-area min height), `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` |
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` |
128
128
  | `chart` pieces | `ChartTooltip` and `ChartLegend` take div props; `ChartLegendKey` `className` | `chart-y-axis`, `chart-tooltip`, `chart-tooltip-row`, `chart-legend`, `chart-legend-key` |
129
129
  | `PageHeader`, `PeriodStepper`, `SkipLink` | `PageHeader` `titleClassName`; `PeriodStepper` `labelClassName` | `page-header`, `page-header-heading`, `page-header-title`, `page-header-description`, `page-header-actions`, `period-stepper`, `period-stepper-label`, `skip-link` |
130
130
  | `FormCheckboxGroupField` | `className` (the options box), `fieldClassName` (the fieldset) | `form-checkbox-group-options` |
@@ -132,7 +132,7 @@ each part carries a `data-slot` for parent selectors such as
132
132
  | `DatePickerButton`, `DateTimePickerButton` | `contentClassName`, `calendarProps` (`disabled` / `hidden` days, `startMonth`, `endMonth`, `weekStartsOn`, date-fns `locale`, `showOutsideDays`, `fixedWeeks`, `showWeekNumber`, `captionLayout`, `today`, `className`); `minDate` / `maxDate` add to its disabled days | `date-picker-value`, `date-picker-footer`, `date-time-picker-time` |
133
133
  | `form/*` fields | `fieldClassName` (wrapper); `className` styles the control (on `FormControlField`, the wrapper); `FormSelectField` `contentClassName`; `FormMarkdownField` `previewClassName` | `form-field`, `form-field-header`, `form-field-description`, `form-field-error`, `form-feedback`, `form-markdown-preview` |
134
134
  | `Table` | `containerClassName` (the scroll box, e.g. `max-h-96 overflow-y-auto`); `className` styles the `<table>` | |
135
- | `TabsList`, `TabsNav` | `scrollerClassName`; `TabsNav` `listClassName`; a list `className` height replaces `h-11`; `TabsNav` sizes to its links in a flex row, so add `w-full` to stretch it | `tabs-scroller` |
135
+ | `TabsList`, `TabsNav` | `scrollerClassName`; `TabsNav` `listClassName`; a list `className` height replaces `h-7`; default scroller is `h-9` (`p-1` + list `h-7`) to match `Button` `size="sm"`; `TabsNav` sizes to its links in a flex row, so add `w-full` to stretch it | `tabs-scroller` |
136
136
  | `SortableTableHead` | `buttonClassName` | `sortable-table-head-button` |
137
137
  | `AccordionTrigger`, `AccordionContent` | `headerClassName`; `AccordionContent` `className` styles the inner box | `accordion-header`, `accordion-content-inner` |
138
138
  | `Progress` | `indicatorClassName` | `progress-indicator` |
@@ -191,9 +191,11 @@ submit. Meta without `isTouched` shows errors as soon as they exist.
191
191
  `FormSelectField` and both date fields included, takes `submitted` and
192
192
  `showError`.
193
193
 
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:
194
+ `FormControlField` gives a control the kit does not ship (a colour picker,
195
+ say) the same label, required marker, hint, error, ids, and error timing.
196
+ Use it rather than `Field` and `FieldLabel`, whose `gap-3` spacing does not
197
+ match the form fields. `children` is a function that receives the props to
198
+ spread on the control:
197
199
 
198
200
  ```tsx
199
201
  <div className="grid grid-cols-2 items-start gap-3">
@@ -211,8 +213,10 @@ function that receives the props to spread on the control:
211
213
  </div>
212
214
  ```
213
215
 
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.
216
+ Each `form/*` field stacks label, hint, control, and error with `gap-1.5`.
217
+ The error sits below the control and takes no space until it shows, so use
218
+ `items-start` on a row of fields, as above, to keep the controls on one top
219
+ edge; the default `stretch` is fine for a single column.
216
220
 
217
221
  Set `required` on a `form/*` field to mark its label with a visible `*`.
218
222
  The marker is hidden from screen readers, which announce the control's own
@@ -246,9 +250,6 @@ the trigger text's id (a `<label for>` alone names the button by the label
246
250
  only).
247
251
  `FormDatePickerField` forwards `className` to that trigger.
248
252
 
249
- Both pickers took `onChange` before 1.0; it still works as a deprecated
250
- alias until 2.0.
251
-
252
253
  `DateTimePickerButton` is the date-and-time control. `value` is a `Date` or
253
254
  `null`; `onValueChange` receives `Date | null`. The trigger matches the same
254
255
  form-control chrome. The popover is one month, a labelled Hour and Minute
@@ -261,11 +262,6 @@ named dialog ("Choose date and time") that focuses the selected day or
261
262
  today, and the trigger takes `aria-labelledby` and `aria-describedby`. `FormDateTimePickerField` forwards `className`
262
263
  and `timeStep` to that control.
263
264
 
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
265
  ```tsx
270
266
  import { DateTimePickerButton } from "atlas-react-kit/date-time-picker"
271
267
 
@@ -370,7 +366,7 @@ export function AppNav({ pathname }: { pathname: string }) {
370
366
  ```
371
367
 
372
368
  In-page view switches that still belong in the URL use `TabsNav`, not
373
- `NavLink`.
369
+ `NavLink`; `Tabs` is for `TabsContent` panels that stay on one page.
374
370
 
375
371
  ## URL navigation in Next.js
376
372
 
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.1.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/accordion.js CHANGED
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Accordion as AccordionPrimitive } from "radix-ui";
5
5
  import { ChevronDownIcon } from "lucide-react";
6
- import { disabledState, focusRing } from "./internal/styles.js";
6
+ import { disabledState, focusRing, quietTextHover } from "./internal/styles.js";
7
7
  import { cn } from "./utils.js";
8
8
  /** Stacked sections that expand in place (FAQs, settings groups). */
9
9
  function Accordion({ ...props }) {
@@ -14,7 +14,7 @@ function AccordionItem({ className, ...props }) {
14
14
  }
15
15
  /** `headerClassName` styles the heading wrapper (`data-slot="accordion-header"`). */
16
16
  function AccordionTrigger({ className, headerClassName, children, ...props }) {
17
- return (_jsx(AccordionPrimitive.Header, { "data-slot": "accordion-header", className: cn("flex", headerClassName), children: _jsxs(AccordionPrimitive.Trigger, { "data-slot": "accordion-trigger", className: cn(focusRing, disabledState, "flex flex-1 items-start justify-between gap-4 rounded-none py-4 text-left text-sm font-medium transition-colors hover:text-selected-foreground focus-visible:text-selected-foreground [&[data-state=open]>svg]:rotate-180", className), ...props, children: [children, _jsx(ChevronDownIcon, { className: "pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" })] }) }));
17
+ return (_jsx(AccordionPrimitive.Header, { "data-slot": "accordion-header", className: cn("flex", headerClassName), children: _jsxs(AccordionPrimitive.Trigger, { "data-slot": "accordion-trigger", className: cn(focusRing, disabledState, "flex flex-1 items-start justify-between gap-4 rounded-none py-4 text-left text-sm font-medium transition-colors", quietTextHover, "[&[data-state=open]>svg]:rotate-180", className), ...props, children: [children, _jsx(ChevronDownIcon, { className: "pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" })] }) }));
18
18
  }
19
19
  /**
20
20
  * `className` styles the padded inner box (`data-slot="accordion-content-inner"`);
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 { 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: {
package/dist/badge.js CHANGED
@@ -2,12 +2,15 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { cva } from "class-variance-authority";
4
4
  import { Slot } from "radix-ui";
5
- import { focusRing, invalidState } from "./internal/styles.js";
5
+ import { controlText, focusRing, invalidState } from "./internal/styles.js";
6
6
  import { cn } from "./utils.js";
7
7
  const badgeVariants = cva([
8
8
  focusRing,
9
9
  invalidState,
10
- "group/badge inline-flex w-fit shrink-0 items-center justify-center gap-1.5 overflow-hidden rounded-none border-0 bg-transparent px-0 py-0 text-[0.625rem] font-semibold tracking-widest whitespace-nowrap uppercase transition-colors has-data-[icon=inline-end]:pr-0 has-data-[icon=inline-start]:pl-0 [&>svg]:pointer-events-none [&>svg:not([class*='size-'])]:size-3",
10
+ "group/badge inline-flex w-fit shrink-0 items-center justify-center gap-1.5 overflow-hidden rounded-none border-0 bg-transparent px-0 py-0",
11
+ controlText,
12
+ // Smaller than `controlText`'s `text-xs`; tailwind-merge keeps this one.
13
+ "text-[0.625rem] transition-colors has-data-[icon=inline-end]:pr-0 has-data-[icon=inline-start]:pl-0 [&>svg]:pointer-events-none [&>svg:not([class*='size-'])]:size-3",
11
14
  ], {
12
15
  variants: {
13
16
  variant: {
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { Slot } from "radix-ui";
4
4
  import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react";
5
- import { focusRing } from "./internal/styles.js";
5
+ import { focusRing, quietTextHover } from "./internal/styles.js";
6
6
  import { cn } from "./utils.js";
7
7
  /**
8
8
  * Where the current page sits in a hierarchy. The last item is
@@ -20,7 +20,7 @@ function BreadcrumbItem({ className, ...props }) {
20
20
  /** Pass `asChild` to render a router link (e.g. Next `Link`) with these styles. */
21
21
  function BreadcrumbLink({ asChild, className, ...props }) {
22
22
  const Comp = asChild ? Slot.Root : "a";
23
- return (_jsx(Comp, { "data-slot": "breadcrumb-link", className: cn(focusRing, "transition-colors hover:text-selected-foreground focus-visible:text-selected-foreground", className), ...props }));
23
+ return (_jsx(Comp, { "data-slot": "breadcrumb-link", className: cn(focusRing, "transition-colors", quietTextHover, className), ...props }));
24
24
  }
25
25
  function BreadcrumbPage({ className, ...props }) {
26
26
  return (_jsx("span", { "data-slot": "breadcrumb-page", "aria-current": "page", className: cn("font-medium text-foreground", className), ...props }));
package/dist/button.js CHANGED
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { cva } from "class-variance-authority";
4
4
  import { Slot } from "radix-ui";
5
- import { controlSize, disabledState, focusRing, invalidState, selectedState, } from "./internal/styles.js";
5
+ import { controlSize, controlText, disabledState, focusRing, inlineIcon, invalidState, selectedState, } from "./internal/styles.js";
6
6
  import { cn } from "./utils.js";
7
7
  /**
8
8
  * Outline and ghost hover/focus: selected-foreground border, selected wash, and
@@ -19,7 +19,10 @@ const buttonVariants = cva([
19
19
  focusRing,
20
20
  invalidState,
21
21
  disabledState,
22
- "group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-semibold tracking-widest whitespace-nowrap uppercase transition-all select-none active:not-aria-[haspopup]:translate-y-px [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
22
+ "group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding",
23
+ controlText,
24
+ "transition-all select-none active:not-aria-[haspopup]:translate-y-px",
25
+ inlineIcon.default,
23
26
  ], {
24
27
  variants: {
25
28
  variant: {
@@ -32,11 +35,11 @@ const buttonVariants = cva([
32
35
  },
33
36
  size: {
34
37
  default: `${controlSize.default} gap-1.5`,
35
- xs: "h-7 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3",
38
+ xs: `h-7 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 ${inlineIcon.sm}`,
36
39
  sm: `${controlSize.sm} gap-1`,
37
40
  lg: `${controlSize.lg} gap-1.5`,
38
41
  icon: "size-10",
39
- "icon-xs": "size-7 [&_svg:not([class*='size-'])]:size-3",
42
+ "icon-xs": `size-7 ${inlineIcon.sm}`,
40
43
  "icon-sm": "size-9",
41
44
  "icon-lg": "size-11",
42
45
  },
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/combobox.js CHANGED
@@ -5,9 +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 { foldSearchText, handleListboxKey, listboxOptionId, } from "./internal/listbox-navigation.js";
9
- import { ListboxEmptyStatus, PickerOption } from "./internal/listbox-option.js";
10
- import { createSearchIndex, filterSearchIndex, normalizeSearchQuery, } from "./internal/search.js";
8
+ import { handleListboxKey } from "./internal/listbox-navigation.js";
9
+ import { Listbox, ListboxEmptyStatus, listboxInputAria, PickerOption, } from "./internal/listbox-option.js";
10
+ import { createSearchIndex, filterSearchIndex, foldSearchText, normalizeSearchQuery, } from "./internal/search.js";
11
+ import { emptyValueText } from "./internal/styles.js";
11
12
  import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
12
13
  import { cn, onInputChange } from "./utils.js";
13
14
  const CREATE_OPTION_KEY = "__atlas-combobox-create";
@@ -49,7 +50,7 @@ function resolveCreateValue(query, existingValues, normalize) {
49
50
  * Opt-in `creatable` appends a create action when the query is not already
50
51
  * an option; choosing it calls `onValueChange` like a normal selection.
51
52
  */
52
- 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 }) {
53
54
  const [open, setOpen] = React.useState(false);
54
55
  const [query, setQuery] = React.useState("");
55
56
  const [highlight, setHighlight] = React.useState(0);
@@ -73,12 +74,7 @@ export function Combobox({ options, value, onValueChange, placeholder = "Select"
73
74
  label,
74
75
  ])
75
76
  : []), [creatable, searchIndex]);
76
- const filtered = React.useMemo(() => {
77
- const needle = normalizeSearchQuery(query);
78
- if (!needle)
79
- return resolvedOptions;
80
- return filterSearchIndex(searchIndex, needle);
81
- }, [resolvedOptions, searchIndex, query]);
77
+ const filtered = React.useMemo(() => filterSearchIndex(searchIndex, normalizeSearchQuery(query)), [searchIndex, query]);
82
78
  const createOption = React.useMemo(() => {
83
79
  const createValue = creatable
84
80
  ? resolveCreateValue(query, existingValues, normalizeCreateValue)
@@ -134,9 +130,7 @@ export function Combobox({ options, value, onValueChange, placeholder = "Select"
134
130
  },
135
131
  });
136
132
  };
137
- 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
138
- ? "sr-only"
139
- : cn("max-h-60 overflow-y-auto p-1", listClassName), children: list.map((option, index) => {
133
+ 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-[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-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) => {
140
134
  const isCreate = createOption != null && option === createOption;
141
135
  const isSelected = option.value === value && !isCreate;
142
136
  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));
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { format } from "date-fns";
5
+ import { labelText } from "./internal/styles.js";
5
6
  import { PickerShell } from "./picker-shell.js";
6
7
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "./select.js";
7
8
  /** Default clock increment, in minutes. Must divide 60. */
@@ -71,7 +72,7 @@ export function DateTimePickerButton({ value, onValueChange, onChange, timeStep,
71
72
  return;
72
73
  emit?.(combineDateAndTime(next, date?.getHours() ?? 0, date?.getMinutes() ?? 0));
73
74
  },
74
- }, onClear: () => emit?.(null), clearDisabled: empty, children: _jsxs("div", { role: "group", "aria-label": "Time", "data-slot": "date-time-picker-time", className: "flex min-w-0 flex-col gap-2 border-t p-3", children: [_jsx("p", { className: "text-xs font-semibold tracking-wide uppercase", children: "Time" }), _jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsxs(Select, { value: date ? padTimePart(date.getHours()) : undefined, onValueChange: (nextHour) => {
75
+ }, onClear: () => emit?.(null), clearDisabled: empty, children: _jsxs("div", { role: "group", "aria-label": "Time", "data-slot": "date-time-picker-time", className: "flex min-w-0 flex-col gap-2 border-t p-3", children: [_jsx("p", { className: labelText, children: "Time" }), _jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsxs(Select, { value: date ? padTimePart(date.getHours()) : undefined, onValueChange: (nextHour) => {
75
76
  if (!date)
76
77
  return;
77
78
  emit?.(combineDateAndTime(date, Number(nextHour), date.getMinutes()));