atlas-react-kit 0.5.0 → 0.6.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 (45) hide show
  1. package/COMPONENTS.md +63 -52
  2. package/DESIGN_RULES.md +32 -15
  3. package/README.md +6 -3
  4. package/dist/accordion.js +2 -1
  5. package/dist/badge.js +7 -2
  6. package/dist/button.d.ts +2 -2
  7. package/dist/button.js +14 -4
  8. package/dist/calendar.js +1 -1
  9. package/dist/checkbox.js +2 -1
  10. package/dist/color-legend.d.ts +22 -0
  11. package/dist/color-legend.js +11 -0
  12. package/dist/combobox.js +2 -1
  13. package/dist/dropdown-menu.js +2 -1
  14. package/dist/form/markdown-field.d.ts +17 -0
  15. package/dist/form/markdown-field.js +27 -0
  16. package/dist/info-tip.d.ts +27 -0
  17. package/dist/info-tip.js +64 -0
  18. package/dist/input.js +2 -1
  19. package/dist/internal/form-control.d.ts +1 -1
  20. package/dist/internal/form-control.js +1 -1
  21. package/dist/internal/styles.d.ts +50 -0
  22. package/dist/internal/styles.js +50 -0
  23. package/dist/internal/tabs-styles.js +6 -3
  24. package/dist/markdown.d.ts +18 -0
  25. package/dist/markdown.js +62 -0
  26. package/dist/meter.d.ts +22 -0
  27. package/dist/meter.js +17 -0
  28. package/dist/pagination.js +2 -1
  29. package/dist/radio-group.js +2 -1
  30. package/dist/search-picker.d.ts +30 -0
  31. package/dist/search-picker.js +101 -0
  32. package/dist/select.js +3 -2
  33. package/dist/slider.d.ts +20 -0
  34. package/dist/slider.js +21 -0
  35. package/dist/stat-tile.d.ts +21 -0
  36. package/dist/stat-tile.js +17 -0
  37. package/dist/switch.js +2 -1
  38. package/dist/table.js +3 -2
  39. package/dist/textarea.js +2 -1
  40. package/dist/toggle-group.js +2 -2
  41. package/dist/toggle.d.ts +7 -1
  42. package/dist/toggle.js +17 -3
  43. package/package.json +37 -1
  44. package/skills/atlas-react-kit/SKILL.md +41 -32
  45. package/theme.css +23 -9
package/COMPONENTS.md CHANGED
@@ -5,52 +5,60 @@ underlying element or Radix props, including `className`, alongside their option
5
5
  Form helpers and `TableBodySkeleton` accept only their documented props. There is
6
6
  no root component barrel.
7
7
 
8
- | Subpath | Main exports and options |
9
- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
10
- | `accordion` | `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent`; Radix `type` `single` (with `collapsible`) or `multiple` |
11
- | `alert` | `Alert`, `AlertTitle`, `AlertDescription`, `alertVariants`; `variant` `default`, `success`, `warning`, `destructive`; tone on title and icon, body stays muted; destructive is `role="alert"` |
12
- | `avatar` | `Avatar`, `AvatarImage`, `AvatarFallback`; `size` `sm`, `default`, `lg`; round by design |
13
- | `breadcrumb` | `Breadcrumb`, `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink` (`asChild` for router links), `BreadcrumbPage` (`aria-current`), `BreadcrumbSeparator`, `BreadcrumbEllipsis` |
14
- | `button` | `Button`, `buttonVariants`, `quietButtonInteraction`; `variant`, `size`, `asChild`; outline/ghost hover is brand purple + selected wash, not muted grey |
15
- | `badge` | `Badge`, `badgeVariants`; `variant`, `tone`, `asChild` |
16
- | `calendar` | `Calendar`, `CalendarDayButton`; React Day Picker props and `buttonVariant` |
17
- | `card` | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardAction`, `CardContent`, `CardFooter`; `Card` supports `flush`; 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 `!` |
18
- | `checkbox` | `Checkbox` |
19
- | `combobox` | `Combobox`; `options` as `{ value, label }[]`, `value`, `onValueChange`; optional placeholder, search, empty text; trigger matches Input/SelectTrigger chrome; keyboard highlight scrolls into view |
20
- | `date-picker` | `DatePickerButton`, `toDate`, `formatLocalDate`, `DateRange`; `mode` `"single"` (default) or `"range"`; single `value` is `Date` or `yyyy-MM-dd`; range `value` is `{ from, to }`; trigger matches Input chrome; optional `numberOfMonths` (range default 1 below `md`, 2 at `md+`) and `side` |
21
- | `date-time-picker` | `DateTimePickerButton`, `resolveTimeStep`, `DEFAULT_TIME_STEP`; controlled `value` is `Date` or `null`; `onChange` emits `Date \ |
22
- | `dialog` | `Dialog`, trigger, portal, overlay, content, close, header, footer, title, description; `DialogContent` supports `showCloseButton` |
23
- | `dropdown-menu` | `DropdownMenu` family (trigger, content, item, checkbox/radio items, label, separator, shortcut, sub menus); item `variant="destructive"` and `inset`; for commands, not values |
24
- | `empty-panel` | `EmptyPanel`; required `children`; optional `size`, `muted`, `className` |
25
- | `error-boundary` | `ErrorBoundary`, `ErrorFallback`; optional `message`; retry resets, reload refreshes the page |
26
- | `field` | Field, label, legend, description, group, set, title, content, separator, error; `Field` supports `orientation` |
27
- | `input`, `textarea`, `label` | `Input`, `Textarea`, `Label` |
28
- | `pagination` | `Pagination`, `PaginationContent`, `PaginationItem`, `PaginationLink` (`isActive` sets `aria-current` and the selected wash), `PaginationPrevious`, `PaginationNext`, `PaginationEllipsis` |
29
- | `popover` | `Popover`, trigger, anchor, content, header, title, description |
30
- | `progress` | `Progress`; Radix props, `value` 0–100; label it with `aria-label` or `aria-labelledby` |
31
- | `radio-group` | `RadioGroup`, `RadioGroupItem`; short visible lists; arrow keys move the selection; round dots |
32
- | `select` | Select, trigger, value, content, group, label, item, separator, scroll buttons; trigger `size`; trigger text is `text-base md:text-sm` like Input |
33
- | `separator`, `skeleton` | `Separator`, `Skeleton` |
34
- | `sortable-table-head` | `SortableTableHead`; `sorted`, `direction` (`asc` \ |
35
- | `sonner` | `Toaster`; Sonner props, with `next-themes` theme defaults |
36
- | `spinner` | `Spinner`; `role="status"` with `aria-label` (default `Loading`) |
37
- | `switch` | `Switch`; settings that apply immediately; square track and thumb |
38
- | `table` | Table, header, body, footer, row, head, cell, caption; root scroller is `min-w-0 overflow-x-auto` with a fade cue; cells are `px-2` below `sm` |
39
- | `table-body-skeleton` | `TableBodySkeleton`; `columns`, optional `rows` (default 5) |
40
- | `tabs` | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, `TabsNav`, `tabsListVariants`, `tabsTriggerVariants`; list `variant` is `default` or `line`; style `TabsNav` links with `tabsTriggerVariants()` |
41
- | `toggle` | `Toggle`, `toggleVariants`; `variant`, `size` |
42
- | `toggle-group` | `ToggleGroup`, `ToggleGroupItem`; `variant`, `size` |
43
- | `theme-provider` | `ThemeProvider`, `useTheme`; optional `storageKey` (default `atlas-theme`), `enableShortcut` for the `d` toggle |
44
- | `tooltip` | `Tooltip`, `TooltipTrigger`, `TooltipContent`, `TooltipProvider`; opens on hover and focus; short labels only |
45
- | `form/text-field` | `FormTextField`, type `TextFieldApi`; controlled field, label, multiline, description, input constraints |
46
- | `form/select-field` | `FormSelectField`; `value`, `onValueChange`, `label`, optional `meta` and placeholder; select item children |
47
- | `form/date-picker-field` | `FormDatePickerField`; `value`, `onValueChange`, `label`, optional `meta`, placeholder, and `className` (forwarded to the trigger) |
48
- | `form/date-time-picker-field` | `FormDateTimePickerField`; `value` is `Date` or `null`; `onValueChange` emits `Date \ |
49
- | `form/field-error` | `FieldError`, `getFieldErrorMessage`; types `FieldErrorMessage`, `FieldMetaState` |
50
- | `form/feedback-field` | `FormFeedbackField`; nullable `message` |
51
- | `next/tabs-nav-link` | `TabsNavLink`; Next Link props and required `active` boolean; requires Next.js 16 |
52
- | `utils` | `cn`, `onInputChange` |
53
- | `theme.css` | Tailwind 4 theme; installation in [README](./README.md) |
8
+ | Subpath | Main exports and options |
9
+ | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
10
+ | `accordion` | `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent`; Radix `type` `single` (with `collapsible`) or `multiple` |
11
+ | `alert` | `Alert`, `AlertTitle`, `AlertDescription`, `alertVariants`; `variant` `default`, `success`, `warning`, `destructive`; tone on title and icon, body stays muted; destructive is `role="alert"` |
12
+ | `avatar` | `Avatar`, `AvatarImage`, `AvatarFallback`; `size` `sm`, `default`, `lg`; round by design |
13
+ | `breadcrumb` | `Breadcrumb`, `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink` (`asChild` for router links), `BreadcrumbPage` (`aria-current`), `BreadcrumbSeparator`, `BreadcrumbEllipsis` |
14
+ | `button` | `Button`, `buttonVariants`, `quietButtonInteraction`; `variant`, `size`, `asChild`; outline/ghost hover is brand purple + selected wash, not muted grey |
15
+ | `badge` | `Badge`, `badgeVariants`; `variant`, `tone`, `asChild` |
16
+ | `calendar` | `Calendar`, `CalendarDayButton`; React Day Picker props and `buttonVariant` |
17
+ | `card` | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardAction`, `CardContent`, `CardFooter`; `Card` supports `flush`; 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 `!` |
18
+ | `checkbox` | `Checkbox` |
19
+ | `color-legend` | `ColorLegend`; `title`, `items` as `{ color, range?, label }[]`, optional `caption`, `children`, `aria-label`, `className` (overlay chrome). No attribution. |
20
+ | `combobox` | `Combobox`; `options` as `{ value, label }[]`, `value`, `onValueChange`; optional placeholder, search, empty text; trigger matches Input/SelectTrigger chrome; keyboard highlight scrolls into view |
21
+ | `date-picker` | `DatePickerButton`, `toDate`, `formatLocalDate`, `DateRange`; `mode` `"single"` (default) or `"range"`; single `value` is `Date` or `yyyy-MM-dd`; range `value` is `{ from, to }`; trigger matches Input chrome; optional `numberOfMonths` (range default 1 below `md`, 2 at `md+`) and `side` |
22
+ | `date-time-picker` | `DateTimePickerButton`, `resolveTimeStep`, `DEFAULT_TIME_STEP`; controlled `value` is `Date` or `null`; `onChange` emits `Date \ |
23
+ | `dialog` | `Dialog`, trigger, portal, overlay, content, close, header, footer, title, description; `DialogContent` supports `showCloseButton` |
24
+ | `dropdown-menu` | `DropdownMenu` family (trigger, content, item, checkbox/radio items, label, separator, shortcut, sub menus); item `variant="destructive"` and `inset`; for commands, not values |
25
+ | `empty-panel` | `EmptyPanel`; required `children`; optional `size`, `muted`, `className` |
26
+ | `error-boundary` | `ErrorBoundary`, `ErrorFallback`; optional `message`; retry resets, reload refreshes the page |
27
+ | `field` | Field, label, legend, description, group, set, title, content, separator, error; `Field` supports `orientation` |
28
+ | `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. |
29
+ | `input`, `textarea`, `label` | `Input`, `Textarea`, `Label` |
30
+ | `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. |
31
+ | `meter` | `Meter`; `value`, `max`, optional `min` (default 0), `fillClassName`, `markers` as `{ position, className? }[]`, required `aria-label`, `className`. `role="meter"`. Not `Progress`. |
32
+ | `pagination` | `Pagination`, `PaginationContent`, `PaginationItem`, `PaginationLink` (`isActive` sets `aria-current`, the selected wash, and a selected border), `PaginationPrevious`, `PaginationNext`, `PaginationEllipsis` |
33
+ | `popover` | `Popover`, trigger, anchor, content, header, title, description |
34
+ | `progress` | `Progress`; Radix props, `value` 0–100; label it with `aria-label` or `aria-labelledby`. For a value against a scale with ticks, use `Meter`. |
35
+ | `radio-group` | `RadioGroup`, `RadioGroupItem`; short visible lists; arrow keys move the selection; round dots |
36
+ | `search-picker` | `SearchPicker<T>`; always-visible combobox + listbox; `items`, `getValue`, `getLabel`, optional `filter` (default casefold + accent-fold includes), `value`, `onSelect`, `label`, `placeholder`, `emptyText`, `listLabel`, `disabled`, `className`. Arrows, Home/End, PageUp/PageDown, Enter, Escape. |
37
+ | `select` | Select, trigger, value, content, group, label, item, separator, scroll buttons; trigger `size`; trigger text is `text-base md:text-sm` like Input |
38
+ | `separator`, `skeleton` | `Separator`, `Skeleton` |
39
+ | `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`. |
40
+ | `sortable-table-head` | `SortableTableHead`; `sorted`, `direction` (`asc` \ |
41
+ | `sonner` | `Toaster`; Sonner props, with `next-themes` theme defaults |
42
+ | `spinner` | `Spinner`; `role="status"` with `aria-label` (default `Loading`) |
43
+ | `stat-tile` | `StatTile`, `statTileGrid`, `sectionLabel`; `label`, `value`, optional `sub`, `children`, `className`. |
44
+ | `switch` | `Switch`; settings that apply immediately; square track and thumb |
45
+ | `table` | Table, header, body, footer, row, head, cell, caption; root scroller is `min-w-0 overflow-x-auto` with a fade cue; cells are `px-2` below `sm` |
46
+ | `table-body-skeleton` | `TableBodySkeleton`; `columns`, optional `rows` (default 5) |
47
+ | `tabs` | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, `TabsNav`, `tabsListVariants`, `tabsTriggerVariants`; list `variant` is `default` or `line`; style `TabsNav` links with `tabsTriggerVariants()` |
48
+ | `toggle` | `Toggle`, `toggleVariants`, `selectedToggleChrome`; `variant`, `size`. Pressed state is the selected wash, text, and border via `data-state=on`, `aria-pressed`, and `aria-checked` |
49
+ | `toggle-group` | `ToggleGroup`, `ToggleGroupItem`; `variant`, `size`. Items share `toggleVariants`, so single (radio) and multiple selections match a pressed `Toggle` |
50
+ | `theme-provider` | `ThemeProvider`, `useTheme`; optional `storageKey` (default `atlas-theme`), `enableShortcut` for the `d` toggle |
51
+ | `tooltip` | `Tooltip`, `TooltipTrigger`, `TooltipContent`, `TooltipProvider`; opens on hover and focus; short labels only |
52
+ | `form/markdown-field` | `FormMarkdownField`; `field` is `TextFieldApi`; `label`, optional `id`, `placeholder`, `rows`, `showError`. Write / preview; preview uses `Markdown`. |
53
+ | `form/text-field` | `FormTextField`, type `TextFieldApi`; controlled field, label, multiline, description, input constraints |
54
+ | `form/select-field` | `FormSelectField`; `value`, `onValueChange`, `label`, optional `meta` and placeholder; select item children |
55
+ | `form/date-picker-field` | `FormDatePickerField`; `value`, `onValueChange`, `label`, optional `meta`, placeholder, and `className` (forwarded to the trigger) |
56
+ | `form/date-time-picker-field` | `FormDateTimePickerField`; `value` is `Date` or `null`; `onValueChange` emits `Date \ |
57
+ | `form/field-error` | `FieldError`, `getFieldErrorMessage`; types `FieldErrorMessage`, `FieldMetaState` |
58
+ | `form/feedback-field` | `FormFeedbackField`; nullable `message` |
59
+ | `next/tabs-nav-link` | `TabsNavLink`; Next Link props and required `active` boolean; requires Next.js 16 |
60
+ | `utils` | `cn`, `onInputChange` |
61
+ | `theme.css` | Tailwind 4 theme; installation in [README](./README.md) |
54
62
 
55
63
  ## Status
56
64
 
@@ -61,8 +69,9 @@ a deprecation notice first, removal in a later minor release.
61
69
 
62
70
  **Beta** components may still change shape in a minor release; the
63
71
  changelog calls out every change: `accordion`, `alert`, `avatar`,
64
- `breadcrumb`, `date-time-picker`, `dropdown-menu`, `pagination`, `progress`,
65
- `radio-group`, `spinner`, `switch`, `tooltip`.
72
+ `breadcrumb`, `color-legend`, `date-time-picker`, `dropdown-menu`, `info-tip`,
73
+ `markdown`, `meter`, `pagination`, `progress`, `radio-group`, `search-picker`,
74
+ `slider`, `spinner`, `stat-tile`, `switch`, `tooltip`.
66
75
 
67
76
  ## Forms
68
77
 
@@ -82,7 +91,8 @@ type TextFieldApi = {
82
91
 
83
92
  `FormTextField` accepts `field` and `label`; use `multiline` and `rows` for a
84
93
  textarea, `inputType` for a specific input, and `showError={false}` to suppress
85
- error text and invalid styling. `FormSelectField`, `FormDatePickerField`, and
94
+ error text and invalid styling. `FormMarkdownField` uses the same `field` shape
95
+ for a Write / Preview GFM body. `FormSelectField`, `FormDatePickerField`, and
86
96
  `FormDateTimePickerField` accept the same error meta shape.
87
97
  `FormDatePickerField` takes a `Date` or `yyyy-MM-dd` string and calls
88
98
  `onValueChange` with `Date | undefined`. `FormDateTimePickerField` takes a
@@ -109,9 +119,10 @@ capped at `min(100vw - 2rem, 24rem)` so it stays on-screen at 390px. Pass
109
119
  `side` to prefer a placement. `FormDateTimePickerField` forwards `className`
110
120
  and `timeStep` to that control.
111
121
 
112
- Outline and ghost `Button` hover and focus use `border-primary`,
113
- `text-selected-foreground`, and `bg-selected`. They do not use a muted grey fill, including
114
- when the control sits on a selected row (for example "Clear selection").
122
+ Outline and ghost `Button` hover uses `border-selected-foreground`,
123
+ `text-selected-foreground`, and `bg-selected`; keyboard focus adds the shared
124
+ solid `ring-ring`. They do not use a muted grey fill, including when the
125
+ control sits on a selected row (for example "Clear selection").
115
126
 
116
127
  ```tsx
117
128
  import { DateTimePickerButton } from "atlas-react-kit/date-time-picker"
package/DESIGN_RULES.md CHANGED
@@ -11,14 +11,16 @@ Use semantic colors, radii, and fonts: `bg-primary`, `text-muted-foreground`,
11
11
  light/dark color pairs; tokens already switch with the theme.
12
12
 
13
13
  Selected or active navigation, tabs, toggles, chips, rows, and labels use
14
- `bg-selected text-selected-foreground`. Outline and ghost buttons hover and
15
- focus with `border-primary`, `text-selected-foreground`, and the selected wash,
16
- not a muted grey fill. Table row hover can stay `hover:bg-muted`. The shipped
17
- controls apply this distinction.
14
+ `bg-selected text-selected-foreground` plus a `border-selected-foreground`
15
+ border or bar: the wash alone is too faint to show state (1.2:1 against the
16
+ page). Outline and ghost buttons hover with `border-selected-foreground`,
17
+ `text-selected-foreground`, and the selected wash, not a muted grey fill.
18
+ Table row hover can stay `hover:bg-muted`. The shipped controls apply this
19
+ distinction.
18
20
 
19
21
  For brand-colored text (links, hovered labels), use `text-selected-foreground`:
20
22
  it is the primary purple in light mode and a lighter purple in dark mode.
21
- `text-primary` as text fails contrast on the dark background (about 2:1).
23
+ `text-primary` as text fails contrast on the dark background (about 3.6:1).
22
24
 
23
25
  Use `success`, `warning`, and `destructive` for good, caution, and bad states.
24
26
  For tinted status chips, use `Badge variant="soft"` with `tone="success"`,
@@ -30,8 +32,11 @@ belong to the application.
30
32
  ## Accessibility
31
33
 
32
34
  The target is WCAG 2.2 AA. CI runs axe-core's WCAG 2.2 A and AA rules against
33
- every Storybook story in light and dark mode (`npm run test:a11y --prefix
34
- site`), and a violation fails the build. Automated checks do not replace
35
+ every Storybook story and every open overlay (menus, listboxes, dialogs,
36
+ popovers, tooltips, toasts) in light and dark mode, checks that focus never
37
+ lands in content an open overlay hides, and checks the theme's token pairs
38
+ for text and non-text contrast in both themes (`npm run test:a11y --prefix
39
+ site`). A violation fails the build. Automated checks do not replace
35
40
  keyboard and screen-reader testing.
36
41
 
37
42
  - Text meets 4.5:1 against its background. The token pairs are tuned for it,
@@ -41,8 +46,16 @@ keyboard and screen-reader testing.
41
46
  `aria-label` on icon-only buttons and an unlabelled `Combobox`.
42
47
  - Visible text is part of the accessible name, so speech users can say what
43
48
  they see (the calendar labels days "September 27", not "27th").
44
- - Keep the focus rings the kit ships. Content that scrolls must be reachable
45
- by keyboard; `Table` adds itself to the tab order while it overflows.
49
+ - Non-text contrast is 3:1 against what the element sits on (WCAG 1.4.11):
50
+ field, checkbox, radio, and switch boundaries (`border-input`), checked
51
+ fills (`bg-primary`), selected borders (`border-selected-foreground`), and
52
+ the focus ring. `border-border` is for dividers and card edges only; never
53
+ make it the only boundary of a control.
54
+ - Focus is a solid `ring-ring` (brand purple), never a translucent ring such
55
+ as `ring-ring/30`, which fades below 3:1. Keyboard-highlighted menu and list
56
+ items show a 2px `ring` bar on their leading edge. Content that scrolls must
57
+ be reachable by keyboard; `Table` adds itself to the tab order while it
58
+ overflows.
46
59
 
47
60
  ## Writing
48
61
 
@@ -93,9 +106,13 @@ Use `Tabs` with actual `TabsContent` panels. For URL view switches, use a labell
93
106
  Next adapter provides `TabsNavLink` with this behavior.
94
107
 
95
108
  Use `FormTextField`, `FormSelectField`, `FormDatePickerField`,
96
- `FormDateTimePickerField`, and `FormFeedbackField` for portable form controls,
97
- and `FieldError` for field messages. `Combobox`, `DatePickerButton` (including
98
- `mode="range"`), `DateTimePickerButton`, `ThemeProvider`, `ErrorBoundary`, and
99
- `SortableTableHead` are first-class kit pieces. Domain data and application shells stay with the application: currency
100
- or timezone option lists, query-library error views, markdown editors, kanban
101
- boards, and dashboard widgets.
109
+ `FormDateTimePickerField`, `FormMarkdownField`, and `FormFeedbackField` for
110
+ portable form controls, and `FieldError` for field messages. `Combobox` (popover)
111
+ and `SearchPicker` (always-visible list), `DatePickerButton` (including
112
+ `mode="range"`), `DateTimePickerButton`, `Slider`, `Meter` (scale with ticks;
113
+ `Progress` is a plain 0–100 job bar), `InfoTip` (longer than `Tooltip`),
114
+ `Markdown`, `ColorLegend`, `StatTile`, `ThemeProvider`, `ErrorBoundary`, and
115
+ `SortableTableHead` are first-class kit pieces. Domain data and application
116
+ shells stay with the application: currency or timezone option lists,
117
+ query-library error views, WYSIWYG editors, kanban boards, and dashboard
118
+ widgets.
package/README.md CHANGED
@@ -1,10 +1,13 @@
1
1
  # Atlas React Kit
2
2
 
3
3
  Shared React components and a Tailwind theme with square corners, semantic
4
- colors, and distinct selected states. React 19 and Tailwind 4 are required.
4
+ colors, and distinct selected states. 0.6.0 added generic pieces apps were
5
+ keeping locally (`InfoTip`, `ColorLegend`, `SearchPicker`, `Markdown`,
6
+ `StatTile`, `Meter`, `Slider`) plus the ToggleGroup selected-state fix.
7
+ React 19 and Tailwind 4 are required.
5
8
 
6
9
  ```sh
7
- npm install --save-exact atlas-react-kit@0.5.0
10
+ npm install --save-exact atlas-react-kit@0.6.1
8
11
  ```
9
12
 
10
13
  Import components by subpath:
@@ -113,7 +116,7 @@ artifact with the package owner's npm login:
113
116
  ```sh
114
117
  mkdir -p release-artifacts
115
118
  npm pack --pack-destination release-artifacts
116
- npm publish ./release-artifacts/atlas-react-kit-0.5.0.tgz --access public
119
+ npm publish ./release-artifacts/atlas-react-kit-0.6.1.tgz --access public
117
120
  ```
118
121
 
119
122
  See [CONTRIBUTING.md](./CONTRIBUTING.md) for adding components, the browser
package/dist/accordion.js CHANGED
@@ -3,6 +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
7
  import { cn } from "./utils.js";
7
8
  /** Stacked sections that expand in place (FAQs, settings groups). */
8
9
  function Accordion({ ...props }) {
@@ -12,7 +13,7 @@ function AccordionItem({ className, ...props }) {
12
13
  return (_jsx(AccordionPrimitive.Item, { "data-slot": "accordion-item", className: cn("border-b last:border-b-0", className), ...props }));
13
14
  }
14
15
  function AccordionTrigger({ className, children, ...props }) {
15
- return (_jsx(AccordionPrimitive.Header, { className: "flex", children: _jsxs(AccordionPrimitive.Trigger, { "data-slot": "accordion-trigger", className: cn("flex flex-1 items-start justify-between gap-4 rounded-none py-4 text-left text-sm font-medium transition-colors outline-none hover:text-selected-foreground focus-visible:text-selected-foreground focus-visible:ring-2 focus-visible:ring-ring/30 disabled:pointer-events-none disabled:opacity-50 [&[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" })] }) }));
16
+ return (_jsx(AccordionPrimitive.Header, { className: "flex", 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" })] }) }));
16
17
  }
17
18
  function AccordionContent({ className, children, ...props }) {
18
19
  return (_jsx(AccordionPrimitive.Content, { "data-slot": "accordion-content", className: "overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down", ...props, children: _jsx("div", { className: cn("pt-0 pb-4", className), children: children }) }));
package/dist/badge.js CHANGED
@@ -2,13 +2,18 @@ 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
6
  import { cn } from "./utils.js";
6
- const badgeVariants = cva("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 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-0 has-data-[icon=inline-start]:pl-0 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", {
7
+ const badgeVariants = cva([
8
+ focusRing,
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]:size-3!",
11
+ ], {
7
12
  variants: {
8
13
  variant: {
9
14
  default: "text-foreground [a]:hover:text-foreground/70",
10
15
  secondary: "text-muted-foreground [a]:hover:text-foreground",
11
- destructive: "text-destructive focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:text-destructive/70",
16
+ destructive: "text-destructive [a]:hover:text-destructive/70",
12
17
  outline: "text-foreground [a]:hover:text-foreground/70",
13
18
  ghost: "text-muted-foreground hover:text-foreground",
14
19
  link: "text-foreground underline-offset-4 hover:underline",
package/dist/button.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  /**
4
- * Outline and ghost hover/focus: brand purple border, selected wash, and
4
+ * Outline and ghost hover/focus: selected-foreground border, selected wash, and
5
5
  * `selected-foreground` text (primary in light mode, a lighter purple that
6
6
  * stays readable on dark), no muted grey fill. Used by quiet actions such as
7
7
  * "Clear selection" sitting on a selected row.
8
8
  */
9
- declare const quietButtonInteraction = "hover:border-primary hover:bg-selected hover:text-selected-foreground focus-visible:border-primary focus-visible:text-selected-foreground focus-visible:ring-primary/30 aria-expanded:border-primary aria-expanded:bg-selected aria-expanded:text-selected-foreground";
9
+ declare const quietButtonInteraction: string;
10
10
  declare const buttonVariants: (props?: ({
11
11
  variant?: "default" | "link" | "destructive" | "secondary" | "outline" | "ghost" | null | undefined;
12
12
  size?: "default" | "sm" | "lg" | "xs" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
package/dist/button.js CHANGED
@@ -2,22 +2,32 @@ 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 { disabledState, focusRing, invalidState, selectedState, } from "./internal/styles.js";
5
6
  import { cn } from "./utils.js";
6
7
  /**
7
- * Outline and ghost hover/focus: brand purple border, selected wash, and
8
+ * Outline and ghost hover/focus: selected-foreground border, selected wash, and
8
9
  * `selected-foreground` text (primary in light mode, a lighter purple that
9
10
  * stays readable on dark), no muted grey fill. Used by quiet actions such as
10
11
  * "Clear selection" sitting on a selected row.
11
12
  */
12
- const quietButtonInteraction = "hover:border-primary hover:bg-selected hover:text-selected-foreground focus-visible:border-primary focus-visible:text-selected-foreground focus-visible:ring-primary/30 aria-expanded:border-primary aria-expanded:bg-selected aria-expanded:text-selected-foreground";
13
- const buttonVariants = cva("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 outline-none select-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", {
13
+ const quietButtonInteraction = [
14
+ selectedState.hover,
15
+ "focus-visible:text-selected-foreground",
16
+ selectedState.expanded,
17
+ ].join(" ");
18
+ const buttonVariants = cva([
19
+ focusRing,
20
+ invalidState,
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",
23
+ ], {
14
24
  variants: {
15
25
  variant: {
16
26
  default: "bg-primary text-primary-foreground hover:bg-primary/80",
17
27
  outline: `border-border bg-transparent ${quietButtonInteraction}`,
18
28
  secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
19
29
  ghost: quietButtonInteraction,
20
- destructive: "bg-destructive/10 text-destructive hover:bg-destructive/15 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
30
+ destructive: "bg-destructive/10 text-destructive hover:bg-destructive/15 dark:bg-destructive/20 dark:hover:bg-destructive/30",
21
31
  link: "text-selected-foreground underline underline-offset-4 hover:underline",
22
32
  },
23
33
  size: {
package/dist/calendar.js CHANGED
@@ -89,6 +89,6 @@ function CalendarDayButton({ className, day, modifiers, locale, ...props }) {
89
89
  return (_jsx(Button, { ref: ref, variant: "ghost", size: "icon", "data-day": day.date.toLocaleDateString(locale?.code), "data-selected-single": modifiers.selected &&
90
90
  !modifiers.range_start &&
91
91
  !modifiers.range_end &&
92
- !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-[3px] group-data-[focused=true]/day:ring-ring/50 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 }));
92
+ !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 }));
93
93
  }
94
94
  export { Calendar, CalendarDayButton };
package/dist/checkbox.js CHANGED
@@ -2,9 +2,10 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Checkbox as CheckboxPrimitive } from "radix-ui";
5
+ import { disabledState, focusRing, invalidState } from "./internal/styles.js";
5
6
  import { cn } from "./utils.js";
6
7
  import { CheckIcon } from "lucide-react";
7
8
  function Checkbox({ className, ...props }) {
8
- return (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", className: cn("peer relative flex size-4.5 shrink-0 items-center justify-center rounded-none border border-input bg-transparent transition-shadow outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary", className), ...props, children: _jsx(CheckboxPrimitive.Indicator, { "data-slot": "checkbox-indicator", className: "grid place-content-center text-current transition-none [&>svg]:size-3.5", children: _jsx(CheckIcon, {}) }) }));
9
+ return (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", className: cn(focusRing, invalidState, disabledState, "peer relative flex size-4.5 shrink-0 items-center justify-center rounded-none border border-input bg-transparent transition-shadow group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 aria-invalid:aria-checked:border-primary data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary", className), ...props, children: _jsx(CheckboxPrimitive.Indicator, { "data-slot": "checkbox-indicator", className: "grid place-content-center text-current transition-none [&>svg]:size-3.5", children: _jsx(CheckIcon, {}) }) }));
9
10
  }
10
11
  export { Checkbox };
@@ -0,0 +1,22 @@
1
+ import type * as React from "react";
2
+ export type ColorLegendItem = {
3
+ /** CSS color from the app (map bins, status scales). The kit does not own it. */
4
+ color: string;
5
+ range?: string;
6
+ label: string;
7
+ };
8
+ export type ColorLegendProps = {
9
+ title?: string;
10
+ items: ColorLegendItem[];
11
+ caption?: React.ReactNode;
12
+ children?: React.ReactNode;
13
+ "aria-label"?: string;
14
+ className?: string;
15
+ };
16
+ /**
17
+ * Key for a color scale: swatch, optional range, and label. Overlay chrome
18
+ * (absolute position, card fill) is the caller's `className`. Attribution
19
+ * and license lines stay in the app.
20
+ */
21
+ declare function ColorLegend({ title, items, caption, children, "aria-label": ariaLabel, className, }: ColorLegendProps): React.JSX.Element;
22
+ export { ColorLegend };
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "./utils.js";
3
+ /**
4
+ * Key for a color scale: swatch, optional range, and label. Overlay chrome
5
+ * (absolute position, card fill) is the caller's `className`. Attribution
6
+ * and license lines stay in the app.
7
+ */
8
+ function ColorLegend({ title, items, caption, children, "aria-label": ariaLabel, className, }) {
9
+ return (_jsxs("figure", { "data-slot": "color-legend", "aria-label": ariaLabel, className: cn("flex flex-col gap-2 text-sm", className), children: [title ? (_jsx("figcaption", { className: "font-heading text-xs font-semibold tracking-widest text-muted-foreground uppercase", children: title })) : null, _jsx("ul", { className: "flex flex-col gap-1.5", children: items.map((item) => (_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { "aria-hidden": true, className: "size-3.5 shrink-0 border border-input", style: { backgroundColor: item.color } }), item.range ? (_jsx("span", { className: "w-16 shrink-0 tabular-nums text-muted-foreground", children: item.range })) : null, _jsx("span", { children: item.label })] }, `${item.color}-${item.range ?? ""}-${item.label}`))) }), caption ? (_jsx("p", { className: "text-xs text-muted-foreground", children: caption })) : null, children] }));
10
+ }
11
+ export { ColorLegend };
package/dist/combobox.js CHANGED
@@ -6,6 +6,7 @@ import { Button } from "./button.js";
6
6
  import { Input } from "./input.js";
7
7
  import { formControlTriggerClassName } from "./internal/form-control.js";
8
8
  import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
9
+ import { optionState } from "./internal/styles.js";
9
10
  import { cn, onInputChange } from "./utils.js";
10
11
  /**
11
12
  * PageUp/PageDown jump size. The option list is `max-h-60`; ten rows is a
@@ -112,6 +113,6 @@ export function Combobox({ options, value, onValueChange, placeholder = "Select"
112
113
  return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { id: id, type: "button", variant: "outline", role: "combobox", "aria-expanded": open, "aria-controls": listId, "aria-invalid": ariaInvalid, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, disabled: disabled, 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: "w-[var(--radix-popover-trigger-width)] gap-0 rounded-none p-0", children: [_jsx("div", { className: "border-b p-2", children: _jsx(Input, { ref: inputRef, value: query, onChange: onQueryChange, onKeyDown: onSearchKeyDown, placeholder: searchPlaceholder, "aria-autocomplete": "list", "aria-controls": listId, "aria-activedescendant": activeOption ? optionId(highlight) : undefined }) }), _jsx("ul", { id: listId, role: "listbox", className: "max-h-60 overflow-y-auto p-1", children: filtered.length === 0 ? (_jsx("li", { className: "px-3 py-4 text-center text-sm text-muted-foreground", children: emptyText })) : (filtered.map((option, index) => {
113
114
  const isSelected = option.value === value;
114
115
  const isActive = index === highlight;
115
- return (_jsx("li", { role: "none", children: _jsxs("button", { ref: isActive ? scrollHighlightedOption : undefined, id: optionId(index), type: "button", role: "option", "aria-selected": isSelected, className: cn("relative flex w-full cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-3 text-left text-sm outline-hidden select-none", isSelected && "bg-selected text-selected-foreground", isActive && "bg-accent text-accent-foreground"), onMouseEnter: () => setHighlight(index), onClick: () => selectValue(option.value), children: [_jsx("span", { className: "truncate", children: option.label }), isSelected ? (_jsx(CheckIcon, { className: "pointer-events-none absolute right-2 size-3.5" })) : null] }) }, option.value));
116
+ return (_jsx("li", { role: "none", children: _jsxs("button", { ref: isActive ? scrollHighlightedOption : undefined, id: optionId(index), type: "button", role: "option", "aria-selected": isSelected, className: cn("relative flex w-full cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-3 text-left text-sm outline-hidden select-none", isSelected && optionState.selected, isActive && optionState.active), onMouseEnter: () => setHighlight(index), onClick: () => selectValue(option.value), children: [_jsx("span", { className: "truncate", children: option.label }), isSelected ? (_jsx(CheckIcon, { className: "pointer-events-none absolute right-2 size-3.5" })) : null] }) }, option.value));
116
117
  })) })] })] }));
117
118
  }
@@ -3,9 +3,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
5
5
  import { CheckIcon, ChevronRightIcon } from "lucide-react";
6
+ import { dataDisabledState, optionState } from "./internal/styles.js";
6
7
  import { cn } from "./utils.js";
7
8
  /** Item highlight and selected chrome shared with `SelectItem`. */
8
- const menuItemClassName = "relative flex cursor-default items-center gap-2.5 rounded-none px-3 py-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-9 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5";
9
+ const menuItemClassName = cn(optionState.focus, dataDisabledState, "relative flex cursor-default items-center gap-2.5 rounded-none px-3 py-2 text-sm outline-hidden select-none data-inset:pl-9 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5");
9
10
  const menuContentClassName = "z-50 min-w-40 overflow-x-hidden overflow-y-auto rounded-none bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95";
10
11
  /** Action menu for commands (row actions, overflow menus). Use `Select` to pick a value. */
11
12
  function DropdownMenu({ ...props }) {
@@ -0,0 +1,17 @@
1
+ import type { TextFieldApi } from "./text-field.js";
2
+ /**
3
+ * GFM body field: Write is a textarea, Preview is the sanitized Markdown
4
+ * renderer. String-in/string-out — not a WYSIWYG document model.
5
+ */
6
+ export declare function FormMarkdownField({ field, id, label, placeholder, rows, showError, }: {
7
+ field: TextFieldApi;
8
+ /**
9
+ * Control id the label points at. Defaults to `field.name`; pass a unique
10
+ * id when two forms on one page share a field name.
11
+ */
12
+ id?: string;
13
+ label: string;
14
+ placeholder?: string;
15
+ rows?: number;
16
+ showError?: boolean;
17
+ }): import("react").JSX.Element;
@@ -0,0 +1,27 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { Label } from "../label.js";
5
+ import { Markdown } from "../markdown.js";
6
+ import { Textarea } from "../textarea.js";
7
+ import { ToggleGroup, ToggleGroupItem } from "../toggle-group.js";
8
+ import { fieldSurface } from "../internal/styles.js";
9
+ import { cn, onInputChange } from "../utils.js";
10
+ import { FieldError, getFieldErrorMessage } from "./field-error.js";
11
+ /**
12
+ * GFM body field: Write is a textarea, Preview is the sanitized Markdown
13
+ * renderer. String-in/string-out — not a WYSIWYG document model.
14
+ */
15
+ export function FormMarkdownField({ field, id = field.name, label, placeholder = "Markdown supported", rows = 5, showError = true, }) {
16
+ const [mode, setMode] = useState("write");
17
+ const onChange = onInputChange(field.handleChange);
18
+ const errorText = showError
19
+ ? getFieldErrorMessage(field.state.meta.errors[0])
20
+ : undefined;
21
+ const trimmed = field.state.value.trim();
22
+ function handleModeChange(value) {
23
+ if (value === "write" || value === "preview")
24
+ setMode(value);
25
+ }
26
+ return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx(Label, { htmlFor: id, children: label }), _jsxs(ToggleGroup, { type: "single", value: mode, onValueChange: handleModeChange, variant: "outline", size: "sm", spacing: 0, "aria-label": `${label} mode`, children: [_jsx(ToggleGroupItem, { value: "write", children: "Write" }), _jsx(ToggleGroupItem, { value: "preview", children: "Preview" })] })] }), mode === "write" ? (_jsx(Textarea, { id: id, value: field.state.value, onChange: onChange, onBlur: field.handleBlur, placeholder: placeholder, rows: rows, "aria-invalid": errorText ? true : undefined })) : (_jsx("div", { className: cn(fieldSurface, "min-h-16"), "aria-live": "polite", children: trimmed ? (_jsx(Markdown, { content: trimmed })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nothing to preview" })) })), showError ? _jsx(FieldError, { text: errorText }) : null] }));
27
+ }
@@ -0,0 +1,27 @@
1
+ import * as React from "react";
2
+ import { PopoverContent } from "./popover.js";
3
+ export type InfoTipLayout = "box" | "inline";
4
+ export type InfoTipProps = {
5
+ /** Required name for the icon button (the glyph has no visible text). */
6
+ label: string;
7
+ title?: string;
8
+ description?: React.ReactNode;
9
+ /** Extra body after the title and description. */
10
+ children?: React.ReactNode;
11
+ /** `inline` keeps the surrounding line height; the 44px hit area overflows. */
12
+ layout?: InfoTipLayout;
13
+ side?: React.ComponentProps<typeof PopoverContent>["side"];
14
+ align?: React.ComponentProps<typeof PopoverContent>["align"];
15
+ badge?: React.ReactNode;
16
+ icon?: React.ReactNode;
17
+ className?: string;
18
+ };
19
+ /**
20
+ * Longer help than a Tooltip: a Popover with a 20px glyph and a 44px hit
21
+ * area. Hover opens only on a fine pointer; click, tap, and keyboard always
22
+ * toggle it. Not a glossary or "coming soon" chip — pass that copy in.
23
+ */
24
+ declare function InfoTip({ label, title, description, children, layout, side, align, badge, icon, className, }: InfoTipProps): React.JSX.Element;
25
+ /** InfoTip that sits in a line of text without stretching the line box. */
26
+ declare function InlineTip(props: Omit<InfoTipProps, "layout">): React.JSX.Element;
27
+ export { InfoTip, InlineTip };