atlas-react-kit 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/COMPONENTS.md CHANGED
@@ -38,7 +38,7 @@ no root component barrel.
38
38
  | `separator`, `skeleton` | `Separator`, `Skeleton` |
39
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
40
  | `sortable-table-head` | `SortableTableHead`; `sorted`, `direction` (`asc` \ |
41
- | `sonner` | `Toaster`; Sonner props, with `next-themes` theme defaults |
41
+ | `sonner` | `Toaster`, `toast`; kit-styled toasts (status icons, kit buttons, close button); import `toast` from here, not `sonner` |
42
42
  | `spinner` | `Spinner`; `role="status"` with `aria-label` (default `Loading`) |
43
43
  | `stat-tile` | `StatTile`, `statTileGrid`, `sectionLabel`; `label`, `value`, optional `sub`, `children`, `className`. |
44
44
  | `switch` | `Switch`; settings that apply immediately; square track and thumb |
package/DESIGN_RULES.md CHANGED
@@ -32,9 +32,11 @@ belong to the application.
32
32
  ## Accessibility
33
33
 
34
34
  The target is WCAG 2.2 AA. CI runs axe-core's WCAG 2.2 A and AA rules against
35
- every Storybook story in light and dark mode, and checks the theme's token
36
- pairs for text and non-text contrast in both themes (`npm run test:a11y
37
- --prefix site`). 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
38
40
  keyboard and screen-reader testing.
39
41
 
40
42
  - Text meets 4.5:1 against its background. The token pairs are tuned for it,
package/README.md CHANGED
@@ -1,13 +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. 0.6.0 adds generic pieces apps were
4
+ colors, and distinct selected states. 0.6.0 added generic pieces apps were
5
5
  keeping locally (`InfoTip`, `ColorLegend`, `SearchPicker`, `Markdown`,
6
6
  `StatTile`, `Meter`, `Slider`) plus the ToggleGroup selected-state fix.
7
7
  React 19 and Tailwind 4 are required.
8
8
 
9
9
  ```sh
10
- npm install --save-exact atlas-react-kit@0.6.0
10
+ npm install --save-exact atlas-react-kit@0.7.0
11
11
  ```
12
12
 
13
13
  Import components by subpath:
@@ -116,7 +116,7 @@ artifact with the package owner's npm login:
116
116
  ```sh
117
117
  mkdir -p release-artifacts
118
118
  npm pack --pack-destination release-artifacts
119
- npm publish ./release-artifacts/atlas-react-kit-0.6.0.tgz --access public
119
+ npm publish ./release-artifacts/atlas-react-kit-0.7.0.tgz --access public
120
120
  ```
121
121
 
122
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 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-2 focus-visible:ring-ring 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
@@ -6,7 +6,7 @@ import { type VariantProps } from "class-variance-authority";
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-selected-foreground hover:bg-selected hover:text-selected-foreground focus-visible:text-selected-foreground aria-expanded:border-selected-foreground 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,6 +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 { disabledState, focusRing, invalidState, selectedState, } from "./internal/styles.js";
5
6
  import { cn } from "./utils.js";
6
7
  /**
7
8
  * Outline and ghost hover/focus: selected-foreground border, selected wash, and
@@ -9,8 +10,17 @@ import { cn } from "./utils.js";
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-selected-foreground hover:bg-selected hover:text-selected-foreground focus-visible:text-selected-foreground aria-expanded:border-selected-foreground 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 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",
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 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 };
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,7 +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 &&
116
- "bg-accent text-accent-foreground shadow-[inset_2px_0_0_var(--color-ring)]"), 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));
117
117
  })) })] })] }));
118
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 focus:shadow-[inset_2px_0_0_var(--color-ring)] 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 }) {
@@ -5,7 +5,8 @@ import { Label } from "../label.js";
5
5
  import { Markdown } from "../markdown.js";
6
6
  import { Textarea } from "../textarea.js";
7
7
  import { ToggleGroup, ToggleGroupItem } from "../toggle-group.js";
8
- import { onInputChange } from "../utils.js";
8
+ import { fieldSurface } from "../internal/styles.js";
9
+ import { cn, onInputChange } from "../utils.js";
9
10
  import { FieldError, getFieldErrorMessage } from "./field-error.js";
10
11
  /**
11
12
  * GFM body field: Write is a textarea, Preview is the sanitized Markdown
@@ -22,5 +23,5 @@ export function FormMarkdownField({ field, id = field.name, label, placeholder =
22
23
  if (value === "write" || value === "preview")
23
24
  setMode(value);
24
25
  }
25
- 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: "min-h-16 rounded-none border border-input bg-background px-3 py-2", "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] }));
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] }));
26
27
  }
package/dist/info-tip.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 { CircleHelpIcon } from "lucide-react";
5
5
  import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, } from "./popover.js";
6
+ import { focusRing } from "./internal/styles.js";
6
7
  import { cn } from "./utils.js";
7
8
  /**
8
9
  * True when a mouse can hover. Touch and coarse pointers must use click;
@@ -52,7 +53,7 @@ function InfoTip({ label, title, description, children, layout = "box", side = "
52
53
  }
53
54
  hoverOpened.current = false;
54
55
  setOpen(next);
55
- }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": label, "data-slot": "info-tip-trigger", "data-layout": layout, className: cn("relative inline-flex shrink-0 items-center justify-center rounded-none text-muted-foreground outline-none hover:text-selected-foreground focus-visible:border-ring focus-visible:text-selected-foreground focus-visible:ring-2 focus-visible:ring-ring", inline
56
+ }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": label, "data-slot": "info-tip-trigger", "data-layout": layout, className: cn(focusRing, "relative inline-flex shrink-0 items-center justify-center rounded-none text-muted-foreground hover:text-selected-foreground focus-visible:text-selected-foreground", inline
56
57
  ? "size-5 align-middle after:absolute after:top-1/2 after:left-1/2 after:size-11 after:-translate-x-1/2 after:-translate-y-1/2"
57
58
  : "size-11", className), onMouseEnter: openFromHover, onMouseLeave: closeFromHover, children: icon ?? (_jsx(CircleHelpIcon, { className: "size-5", "data-icon": "info-tip", "aria-hidden": true })) }) }), _jsxs(PopoverContent, { side: side, align: align, className: "w-72", onMouseEnter: openFromHover, onMouseLeave: closeFromHover, children: [_jsxs(PopoverHeader, { children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [title ? _jsx(PopoverTitle, { children: title }) : null, badge] }), description ? (_jsx(PopoverDescription, { children: description })) : null] }), children] })] }));
58
59
  }
package/dist/input.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
+ import { disabledState, fieldSurface, focusRing, invalidState, } from "./internal/styles.js";
3
4
  import { cn } from "./utils.js";
4
5
  function Input({ className, type, ...props }) {
5
- return (_jsx("input", { type: type, "data-slot": "input", className: cn("h-10 w-full min-w-0 rounded-none border border-input bg-background px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 md:text-sm dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", className), ...props }));
6
+ return (_jsx("input", { type: type, "data-slot": "input", className: cn(fieldSurface, focusRing, invalidState, disabledState, "h-10 file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground", className), ...props }));
6
7
  }
7
8
  export { Input };
@@ -6,4 +6,4 @@
6
6
  * control next to real form fields. Hover stays Input-like and cancels the
7
7
  * outline Button's purple selected wash.
8
8
  */
9
- export declare const formControlTriggerClassName = "h-10 w-full border-input bg-background px-3 font-normal text-base tracking-normal shadow-xs normal-case md:text-sm hover:border-input hover:bg-background hover:text-foreground focus-visible:border-ring focus-visible:text-foreground focus-visible:ring-ring aria-expanded:border-input aria-expanded:bg-background aria-expanded:text-foreground has-data-[icon=inline-start]:pl-3 has-data-[icon=inline-end]:pr-3 dark:hover:bg-background dark:aria-expanded:bg-background";
9
+ export declare const formControlTriggerClassName = "h-10 w-full border-input bg-background px-3 font-normal text-base tracking-normal shadow-xs normal-case md:text-sm hover:border-input hover:bg-background hover:text-foreground focus-visible:text-foreground aria-expanded:border-input aria-expanded:bg-background aria-expanded:text-foreground has-data-[icon=inline-start]:pl-3 has-data-[icon=inline-end]:pr-3 dark:hover:bg-background dark:aria-expanded:bg-background";
@@ -6,4 +6,4 @@
6
6
  * control next to real form fields. Hover stays Input-like and cancels the
7
7
  * outline Button's purple selected wash.
8
8
  */
9
- export const formControlTriggerClassName = "h-10 w-full border-input bg-background px-3 font-normal text-base tracking-normal shadow-xs normal-case md:text-sm hover:border-input hover:bg-background hover:text-foreground focus-visible:border-ring focus-visible:text-foreground focus-visible:ring-ring aria-expanded:border-input aria-expanded:bg-background aria-expanded:text-foreground has-data-[icon=inline-start]:pl-3 has-data-[icon=inline-end]:pr-3 dark:hover:bg-background dark:aria-expanded:bg-background";
9
+ export const formControlTriggerClassName = "h-10 w-full border-input bg-background px-3 font-normal text-base tracking-normal shadow-xs normal-case md:text-sm hover:border-input hover:bg-background hover:text-foreground focus-visible:text-foreground aria-expanded:border-input aria-expanded:bg-background aria-expanded:text-foreground has-data-[icon=inline-start]:pl-3 has-data-[icon=inline-end]:pr-3 dark:hover:bg-background dark:aria-expanded:bg-background";
@@ -0,0 +1,50 @@
1
+ /**
2
+ * State classes shared by the kit's controls. Components add these with
3
+ * `cn()` instead of retyping them, so a state looks the same everywhere and
4
+ * a change (such as the WCAG focus ring) happens here once. Tailwind only
5
+ * generates class names it finds as literals, so each state attribute gets
6
+ * its own complete string. `test/dry.test.ts` fails when a component types
7
+ * one of these by hand.
8
+ */
9
+ /** Keyboard focus: a ring-colored border plus a solid 2px ring (WCAG 1.4.11). */
10
+ export declare const focusRing = "outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring";
11
+ /** `aria-invalid`: destructive border and a soft glow; the error text carries the message. */
12
+ export declare const invalidState = "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";
13
+ /** Native `disabled`: dimmed, with no hover or click. */
14
+ export declare const disabledState = "disabled:pointer-events-none disabled:opacity-50";
15
+ /** Radix `data-disabled` (menu and list items, slider). */
16
+ export declare const dataDisabledState = "data-disabled:pointer-events-none data-disabled:opacity-50";
17
+ /** The text-field surface, so Input, Textarea, and SelectTrigger line up. */
18
+ export declare const fieldSurface = "w-full min-w-0 rounded-none border border-input bg-background px-3 py-2 text-base shadow-xs transition-[color,box-shadow] placeholder:text-muted-foreground md:text-sm";
19
+ /**
20
+ * Selected chrome: the wash, selected text, and a selected border, because
21
+ * the wash alone is 1.2:1 against the page. One string per state attribute.
22
+ */
23
+ export declare const selectedState: {
24
+ /**
25
+ * Toggle and ToggleGroupItem. Radix Toggle sets `data-state=on` and
26
+ * `aria-pressed`; group items always set `data-state=on`, add
27
+ * `aria-pressed` in multiple mode, and are radios with `aria-checked` in
28
+ * single mode. All three get the same chrome.
29
+ */
30
+ toggle: string;
31
+ /** Tab trigger and TabsNav link (`data-state="active"`). */
32
+ active: string;
33
+ /** Current page (`data-active="true"`). */
34
+ current: string;
35
+ /** Trigger whose menu or popover is open. */
36
+ expanded: string;
37
+ /** Quiet buttons and toggles on hover. */
38
+ hover: string;
39
+ };
40
+ /** Options in menus and listboxes. */
41
+ export declare const optionState: {
42
+ /** Highlight as focus moves: grey fill plus a 2px ring bar (the fill alone is 1.1:1). */
43
+ focus: string;
44
+ /** The same highlight for lists that track the active option in state. */
45
+ active: string;
46
+ /** The chosen value (`data-state="checked"`); a check icon marks it too. */
47
+ checked: string;
48
+ /** The chosen value in lists that track selection in state. */
49
+ selected: string;
50
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * State classes shared by the kit's controls. Components add these with
3
+ * `cn()` instead of retyping them, so a state looks the same everywhere and
4
+ * a change (such as the WCAG focus ring) happens here once. Tailwind only
5
+ * generates class names it finds as literals, so each state attribute gets
6
+ * its own complete string. `test/dry.test.ts` fails when a component types
7
+ * one of these by hand.
8
+ */
9
+ /** Keyboard focus: a ring-colored border plus a solid 2px ring (WCAG 1.4.11). */
10
+ export const focusRing = "outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring";
11
+ /** `aria-invalid`: destructive border and a soft glow; the error text carries the message. */
12
+ export const invalidState = "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";
13
+ /** Native `disabled`: dimmed, with no hover or click. */
14
+ export const disabledState = "disabled:pointer-events-none disabled:opacity-50";
15
+ /** Radix `data-disabled` (menu and list items, slider). */
16
+ export const dataDisabledState = "data-disabled:pointer-events-none data-disabled:opacity-50";
17
+ /** The text-field surface, so Input, Textarea, and SelectTrigger line up. */
18
+ export const fieldSurface = "w-full min-w-0 rounded-none border border-input bg-background px-3 py-2 text-base shadow-xs transition-[color,box-shadow] placeholder:text-muted-foreground md:text-sm";
19
+ /**
20
+ * Selected chrome: the wash, selected text, and a selected border, because
21
+ * the wash alone is 1.2:1 against the page. One string per state attribute.
22
+ */
23
+ export const selectedState = {
24
+ /**
25
+ * Toggle and ToggleGroupItem. Radix Toggle sets `data-state=on` and
26
+ * `aria-pressed`; group items always set `data-state=on`, add
27
+ * `aria-pressed` in multiple mode, and are radios with `aria-checked` in
28
+ * single mode. All three get the same chrome.
29
+ */
30
+ toggle: "data-on:border-selected-foreground data-on:bg-selected data-on:text-selected-foreground aria-pressed:border-selected-foreground aria-pressed:bg-selected aria-pressed:text-selected-foreground aria-checked:border-selected-foreground aria-checked:bg-selected aria-checked:text-selected-foreground",
31
+ /** Tab trigger and TabsNav link (`data-state="active"`). */
32
+ active: "data-[state=active]:border-selected-foreground data-[state=active]:bg-selected data-[state=active]:text-selected-foreground",
33
+ /** Current page (`data-active="true"`). */
34
+ current: "data-[active=true]:border-selected-foreground data-[active=true]:bg-selected data-[active=true]:text-selected-foreground",
35
+ /** Trigger whose menu or popover is open. */
36
+ expanded: "aria-expanded:border-selected-foreground aria-expanded:bg-selected aria-expanded:text-selected-foreground",
37
+ /** Quiet buttons and toggles on hover. */
38
+ hover: "hover:border-selected-foreground hover:bg-selected hover:text-selected-foreground",
39
+ };
40
+ /** Options in menus and listboxes. */
41
+ export const optionState = {
42
+ /** Highlight as focus moves: grey fill plus a 2px ring bar (the fill alone is 1.1:1). */
43
+ focus: "focus:bg-accent focus:text-accent-foreground focus:shadow-[inset_2px_0_0_var(--color-ring)]",
44
+ /** The same highlight for lists that track the active option in state. */
45
+ active: "bg-accent text-accent-foreground shadow-[inset_2px_0_0_var(--color-ring)]",
46
+ /** The chosen value (`data-state="checked"`); a check icon marks it too. */
47
+ checked: "data-[state=checked]:bg-selected data-[state=checked]:text-selected-foreground",
48
+ /** The chosen value in lists that track selection in state. */
49
+ selected: "bg-selected text-selected-foreground",
50
+ };
@@ -1,4 +1,5 @@
1
1
  import { cva } from "class-variance-authority";
2
+ import { disabledState, focusRing, selectedState } from "./styles.js";
2
3
  const tabsListVariants = cva('group/tabs-list inline-flex w-fit items-stretch justify-center p-0 text-muted-foreground group-data-[orientation="horizontal"]/tabs:h-11 group-data-[orientation="vertical"]/tabs:h-fit group-data-[orientation="vertical"]/tabs:flex-col', {
3
4
  variants: {
4
5
  variant: {
@@ -11,9 +12,11 @@ const tabsListVariants = cva('group/tabs-list inline-flex w-fit items-stretch ju
11
12
  },
12
13
  });
13
14
  const tabsTriggerVariants = cva([
14
- 'relative inline-flex h-full min-w-0 flex-1 items-center justify-center gap-2 border border-transparent px-4 py-1.5 text-xs font-semibold tracking-wider whitespace-nowrap text-foreground/60 uppercase transition-all group-data-[orientation="vertical"]/tabs:w-full group-data-[orientation="vertical"]/tabs:justify-start group-data-[orientation="vertical"]/tabs:px-4 group-data-[orientation="vertical"]/tabs:py-2 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-3.5',
15
+ focusRing,
16
+ disabledState,
17
+ 'relative inline-flex h-full min-w-0 flex-1 items-center justify-center gap-2 border border-transparent px-4 py-1.5 text-xs font-semibold tracking-wider whitespace-nowrap text-foreground/60 uppercase transition-all group-data-[orientation="vertical"]/tabs:w-full group-data-[orientation="vertical"]/tabs:justify-start group-data-[orientation="vertical"]/tabs:px-4 group-data-[orientation="vertical"]/tabs:py-2 hover:text-foreground has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-3.5',
15
18
  'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state="active"]:border-transparent group-data-[variant=line]/tabs-list:data-[state="active"]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state="active"]:bg-transparent',
16
- 'data-[state="active"]:border-selected-foreground data-[state="active"]:bg-selected data-[state="active"]:text-selected-foreground',
19
+ selectedState.active,
17
20
  'after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation="horizontal"]/tabs:after:inset-x-0 group-data-[orientation="horizontal"]/tabs:after:bottom-[-5px] group-data-[orientation="horizontal"]/tabs:after:h-0.5 group-data-[orientation="vertical"]/tabs:after:inset-y-0 group-data-[orientation="vertical"]/tabs:after:-right-1 group-data-[orientation="vertical"]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state="active"]:after:opacity-100',
18
21
  ]);
19
22
  export { tabsListVariants, tabsTriggerVariants };
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon, } from "lucide-react";
4
4
  import { buttonVariants } from "./button.js";
5
+ import { selectedState } from "./internal/styles.js";
5
6
  import { cn } from "./utils.js";
6
7
  /**
7
8
  * Page links for long lists and tables. Links are plain anchors so the page
@@ -21,7 +22,7 @@ function PaginationLink({ className, isActive, size = "icon", ...props }) {
21
22
  return (_jsx("a", { "aria-current": isActive ? "page" : undefined, "data-slot": "pagination-link", "data-active": isActive, className: cn(buttonVariants({
22
23
  variant: isActive ? "outline" : "ghost",
23
24
  size: size === "icon" ? "icon-sm" : "sm",
24
- }), "font-medium tracking-normal normal-case tabular-nums data-[active=true]:border-selected-foreground data-[active=true]:bg-selected data-[active=true]:text-selected-foreground", className), ...props }));
25
+ }), selectedState.current, "font-medium tracking-normal normal-case tabular-nums", className), ...props }));
25
26
  }
26
27
  function PaginationPrevious({ className, children = "Previous", ...props }) {
27
28
  return (_jsxs(PaginationLink, { "aria-label": "Go to previous page", size: "default", className: cn("gap-1 px-2.5 sm:pl-2", className), ...props, children: [_jsx(ChevronLeftIcon, { "data-icon": "inline-start" }), _jsx("span", { className: "hidden sm:block", children: children })] }));
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { RadioGroup as RadioGroupPrimitive } from "radix-ui";
5
+ import { disabledState, focusRing, invalidState } from "./internal/styles.js";
5
6
  import { cn } from "./utils.js";
6
7
  /**
7
8
  * One choice from a short, visible list. Arrow keys move the selection.
@@ -15,6 +16,6 @@ function RadioGroup({ className, ...props }) {
15
16
  * differently from square checkboxes.
16
17
  */
17
18
  function RadioGroupItem({ className, ...props }) {
18
- return (_jsx(RadioGroupPrimitive.Item, { "data-slot": "radio-group-item", className: cn("peer relative flex aspect-square size-4.5 shrink-0 items-center justify-center rounded-full border border-input bg-transparent shadow-xs transition-shadow outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 data-checked:border-primary", className), ...props, children: _jsx(RadioGroupPrimitive.Indicator, { "data-slot": "radio-group-indicator", className: "flex items-center justify-center", children: _jsx("span", { className: "size-2 rounded-full bg-primary" }) }) }));
19
+ return (_jsx(RadioGroupPrimitive.Item, { "data-slot": "radio-group-item", className: cn(focusRing, invalidState, disabledState, "peer relative flex aspect-square size-4.5 shrink-0 items-center justify-center rounded-full border border-input bg-transparent shadow-xs transition-shadow after:absolute after:-inset-x-3 after:-inset-y-2 data-checked:border-primary", className), ...props, children: _jsx(RadioGroupPrimitive.Indicator, { "data-slot": "radio-group-indicator", className: "flex items-center justify-center", children: _jsx("span", { className: "size-2 rounded-full bg-primary" }) }) }));
19
20
  }
20
21
  export { RadioGroup, RadioGroupItem };
@@ -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 { Input } from "./input.js";
5
5
  import { Label } from "./label.js";
6
+ import { optionState } from "./internal/styles.js";
6
7
  import { cn } from "./utils.js";
7
8
  const LISTBOX_PAGE_SIZE = 10;
8
9
  /**
@@ -92,7 +93,7 @@ function SearchPicker({ items, getValue, getLabel, filter = defaultSearchFilter,
92
93
  const itemValue = getValue(item);
93
94
  const isSelected = itemValue === value;
94
95
  const isActive = index === highlight;
95
- return (_jsx("li", { role: "none", children: _jsx("button", { ref: isActive ? scrollHighlightedOption : undefined, id: optionId(index), type: "button", role: "option", "aria-selected": isSelected, disabled: disabled, className: cn("relative flex w-full cursor-default items-center rounded-none py-2 pr-3 pl-3 text-left text-sm outline-hidden select-none", isSelected && "bg-selected text-selected-foreground", isActive && "shadow-[inset_2px_0_0_var(--color-ring)]", isActive &&
96
+ return (_jsx("li", { role: "none", children: _jsx("button", { ref: isActive ? scrollHighlightedOption : undefined, id: optionId(index), type: "button", role: "option", "aria-selected": isSelected, disabled: disabled, className: cn("relative flex w-full cursor-default items-center rounded-none py-2 pr-3 pl-3 text-left text-sm outline-hidden select-none", isSelected && optionState.selected, isActive && optionState.active, isActive &&
96
97
  !isSelected &&
97
98
  "bg-accent text-accent-foreground"), onMouseEnter: () => setHighlight(index), onClick: () => selectItem(item), children: _jsx("span", { className: "truncate", children: getLabel(item) }) }) }, itemValue));
98
99
  })) })] }));
package/dist/select.js CHANGED
@@ -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 { Select as SelectPrimitive } from "radix-ui";
5
+ import { dataDisabledState, disabledState, fieldSurface, focusRing, invalidState, optionState, } from "./internal/styles.js";
5
6
  import { cn } from "./utils.js";
6
7
  import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react";
7
8
  function Select({ ...props }) {
@@ -14,7 +15,7 @@ function SelectValue({ ...props }) {
14
15
  return _jsx(SelectPrimitive.Value, { "data-slot": "select-value", ...props });
15
16
  }
16
17
  function SelectTrigger({ className, size = "default", children, ...props }) {
17
- return (_jsxs(SelectPrimitive.Trigger, { "data-slot": "select-trigger", "data-size": size, className: cn("flex w-full min-w-0 items-center justify-between gap-1.5 rounded-none border border-input bg-background px-3 py-2 text-base whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-10 data-[size=sm]:h-9 md:text-sm *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 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", className), ...props, children: [children, _jsx(SelectPrimitive.Icon, { asChild: true, children: _jsx(ChevronDownIcon, { className: "pointer-events-none size-3.5 text-muted-foreground" }) })] }));
18
+ return (_jsxs(SelectPrimitive.Trigger, { "data-slot": "select-trigger", "data-size": size, className: cn(fieldSurface, focusRing, invalidState, disabledState, "flex items-center justify-between gap-1.5 whitespace-nowrap data-placeholder:text-muted-foreground data-[size=default]:h-10 data-[size=sm]:h-9 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", className), ...props, children: [children, _jsx(SelectPrimitive.Icon, { asChild: true, children: _jsx(ChevronDownIcon, { className: "pointer-events-none size-3.5 text-muted-foreground" }) })] }));
18
19
  }
19
20
  function SelectContent({ className, children, position = "popper", align = "center", sideOffset = 4, collisionPadding = 8, ...props }) {
20
21
  return (_jsx(SelectPrimitive.Portal, { children: _jsxs(SelectPrimitive.Content, { "data-slot": "select-content", "data-align-trigger": position === "item-aligned", className: cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-(--radix-select-trigger-width) origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-none bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none 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", position === "popper" &&
@@ -24,7 +25,7 @@ function SelectLabel({ className, ...props }) {
24
25
  return (_jsx(SelectPrimitive.Label, { "data-slot": "select-label", className: cn("px-3 py-2 text-xs font-semibold tracking-wider text-muted-foreground uppercase", className), ...props }));
25
26
  }
26
27
  function SelectItem({ className, children, ...props }) {
27
- return (_jsxs(SelectPrimitive.Item, { "data-slot": "select-item", className: cn("relative flex w-full cursor-default items-center gap-2.5 rounded-none py-2 pr-8 pl-3 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:shadow-[inset_2px_0_0_var(--color-ring)] not-data-[variant=destructive]:focus:**:text-accent-foreground data-[state=checked]:bg-selected data-[state=checked]:text-selected-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", className), ...props, children: [_jsx("span", { className: "pointer-events-none absolute right-2 flex size-4 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(CheckIcon, { className: "pointer-events-none" }) }) }), _jsx(SelectPrimitive.ItemText, { children: children })] }));
28
+ return (_jsxs(SelectPrimitive.Item, { "data-slot": "select-item", className: cn(optionState.focus, optionState.checked, dataDisabledState, "relative flex w-full cursor-default items-center gap-2.5 rounded-none py-2 pr-8 pl-3 text-sm outline-hidden select-none not-data-[variant=destructive]:focus:**:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", className), ...props, children: [_jsx("span", { className: "pointer-events-none absolute right-2 flex size-4 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(CheckIcon, { className: "pointer-events-none" }) }) }), _jsx(SelectPrimitive.ItemText, { children: children })] }));
28
29
  }
29
30
  function SelectSeparator({ className, ...props }) {
30
31
  return (_jsx(SelectPrimitive.Separator, { "data-slot": "select-separator", className: cn("pointer-events-none -mx-1.5 my-1.5 h-px bg-border/50", className), ...props }));
package/dist/slider.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Slider as SliderPrimitive } from "radix-ui";
4
+ import { dataDisabledState, focusRing } from "./internal/styles.js";
4
5
  import { cn } from "./utils.js";
5
6
  /**
6
7
  * Single-thumb range on Radix Slider. Square track and thumb, primary fill,
@@ -15,6 +16,6 @@ function Slider({ value, onValueChange, onValueCommit, min = 0, max = 100, step
15
16
  const amount = next[0];
16
17
  if (amount !== undefined)
17
18
  onValueCommit?.(amount);
18
- }, className: cn("relative flex w-full touch-none items-center select-none data-disabled:opacity-50", className), children: [_jsx(SliderPrimitive.Track, { "data-slot": "slider-track", className: "relative h-1.5 w-full grow overflow-hidden rounded-none bg-muted", children: _jsx(SliderPrimitive.Range, { "data-slot": "slider-range", className: "absolute h-full bg-primary" }) }), _jsx(SliderPrimitive.Thumb, { id: id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "data-slot": "slider-thumb", className: "relative block size-4 shrink-0 rounded-none border border-primary bg-background shadow-xs outline-none after:absolute after:top-1/2 after:left-1/2 after:size-11 after:-translate-x-1/2 after:-translate-y-1/2 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring" })] }));
19
+ }, className: cn(dataDisabledState, "relative flex w-full touch-none items-center select-none", className), children: [_jsx(SliderPrimitive.Track, { "data-slot": "slider-track", className: "relative h-1.5 w-full grow overflow-hidden rounded-none bg-muted", children: _jsx(SliderPrimitive.Range, { "data-slot": "slider-range", className: "absolute h-full bg-primary" }) }), _jsx(SliderPrimitive.Thumb, { id: id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "data-slot": "slider-thumb", className: cn(focusRing, "relative block size-4 shrink-0 rounded-none border border-primary bg-background shadow-xs after:absolute after:top-1/2 after:left-1/2 after:size-11 after:-translate-x-1/2 after:-translate-y-1/2") })] }));
19
20
  }
20
21
  export { Slider };
package/dist/sonner.d.ts CHANGED
@@ -1,4 +1,8 @@
1
- import * as React from "react";
2
- import { type ToasterProps } from "sonner";
3
- declare const Toaster: ({ ...props }: ToasterProps) => React.JSX.Element;
4
- export { Toaster };
1
+ import { toast, type ToasterProps } from "sonner";
2
+ /**
3
+ * Mount one near the app root and call `toast()` from this module (not from
4
+ * `sonner` directly), so the app and the toaster always share one Sonner.
5
+ */
6
+ declare const Toaster: ({ toastOptions, ...props }: ToasterProps) => import("react").JSX.Element;
7
+ export { Toaster, toast };
8
+ export type { ToasterProps };
package/dist/sonner.js CHANGED
@@ -1,15 +1,61 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import * as React from "react";
3
+ import { CircleAlertIcon, CircleCheckIcon, InfoIcon, LoaderCircleIcon, XIcon, } from "lucide-react";
4
4
  import { useTheme } from "next-themes";
5
- import { Toaster as Sonner } from "sonner";
6
- const Toaster = ({ ...props }) => {
5
+ import { Toaster as Sonner, toast } from "sonner";
6
+ import { buttonVariants } from "./button.js";
7
+ import { cn } from "./utils.js";
8
+ /**
9
+ * Toasts drawn entirely with kit classes. Sonner's own styles are unlayered
10
+ * CSS that Tailwind utilities cannot override, so the toaster runs Sonner
11
+ * `unstyled` and styles every part here: the popover surface, Alert-style
12
+ * status icons (the tone colors the icon, title, and border, not the body
13
+ * text), kit buttons for actions, and a labelled close button with the
14
+ * shared focus ring.
15
+ */
16
+ const classNames = {
17
+ toast: "flex w-(--width) flex-wrap items-start justify-end gap-3 rounded-none border border-border bg-popover p-4 pr-10 text-sm text-popover-foreground shadow-lg",
18
+ // Fills the row beside the icon, so action buttons wrap to their own row
19
+ // (aligned right by the toast's justify-end).
20
+ content: "flex min-w-0 flex-1 basis-[calc(100%-1.75rem)] flex-col gap-0.5",
21
+ title: "font-medium leading-5",
22
+ description: "leading-5 text-muted-foreground",
23
+ icon: "mt-0.5 flex size-4 shrink-0 items-center justify-center [&_svg]:size-4",
24
+ actionButton: cn(buttonVariants({ size: "xs" }), "shrink-0"),
25
+ cancelButton: cn(buttonVariants({ variant: "outline", size: "xs" }), "shrink-0"),
26
+ closeButton: cn(buttonVariants({ variant: "ghost", size: "icon-xs" }), "absolute top-2 right-2 text-muted-foreground"),
27
+ success: "border-success/40 [&_[data-title]]:text-success",
28
+ warning: "border-warning/40 [&_[data-title]]:text-warning",
29
+ error: "border-destructive/40 [&_[data-title]]:text-destructive",
30
+ };
31
+ const icons = {
32
+ success: _jsx(CircleCheckIcon, { "aria-hidden": true, className: "text-success" }),
33
+ info: _jsx(InfoIcon, { "aria-hidden": true, className: "text-foreground" }),
34
+ warning: _jsx(CircleAlertIcon, { "aria-hidden": true, className: "text-warning" }),
35
+ error: _jsx(CircleAlertIcon, { "aria-hidden": true, className: "text-destructive" }),
36
+ loading: (_jsx(LoaderCircleIcon, { "aria-hidden": true, className: "animate-spin text-muted-foreground" })),
37
+ close: _jsx(XIcon, { "aria-hidden": true }),
38
+ };
39
+ /** Merge app class names onto the kit's, part by part. */
40
+ function mergeClassNames(extra) {
41
+ if (!extra)
42
+ return classNames;
43
+ const merged = { ...classNames };
44
+ for (const [part, value] of Object.entries(extra)) {
45
+ merged[part] = cn(classNames[part], value);
46
+ }
47
+ return merged;
48
+ }
49
+ /**
50
+ * Mount one near the app root and call `toast()` from this module (not from
51
+ * `sonner` directly), so the app and the toaster always share one Sonner.
52
+ */
53
+ const Toaster = ({ toastOptions, ...props }) => {
7
54
  const { theme = "system" } = useTheme();
8
- return (_jsx(Sonner, { theme: theme, className: "toaster group", style: {
9
- "--normal-bg": "var(--popover)",
10
- "--normal-text": "var(--popover-foreground)",
11
- "--normal-border": "var(--border)",
12
- "--border-radius": "0px",
13
- }, ...props }));
55
+ return (_jsx(Sonner, { theme: theme, className: "toaster group", closeButton: true, icons: icons, ...props, toastOptions: {
56
+ ...toastOptions,
57
+ unstyled: true,
58
+ classNames: mergeClassNames(toastOptions?.classNames),
59
+ } }));
14
60
  };
15
- export { Toaster };
61
+ export { Toaster, toast };
package/dist/switch.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Switch as SwitchPrimitive } from "radix-ui";
5
+ import { disabledState, focusRing, invalidState } from "./internal/styles.js";
5
6
  import { cn } from "./utils.js";
6
7
  /**
7
8
  * On/off setting that applies immediately (notifications, dark mode). Use
@@ -9,6 +10,6 @@ import { cn } from "./utils.js";
9
10
  * primitives; checked uses the primary fill, as `Checkbox` does.
10
11
  */
11
12
  function Switch({ className, ...props }) {
12
- return (_jsx(SwitchPrimitive.Root, { "data-slot": "switch", className: cn("peer inline-flex h-5 w-9 shrink-0 items-center rounded-none border border-transparent bg-input p-0.5 shadow-xs transition-colors outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 data-checked:bg-primary dark:data-checked:bg-primary", className), ...props, children: _jsx(SwitchPrimitive.Thumb, { "data-slot": "switch-thumb", className: "pointer-events-none block size-3.5 rounded-none bg-background shadow-sm ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground" }) }));
13
+ return (_jsx(SwitchPrimitive.Root, { "data-slot": "switch", className: cn(focusRing, invalidState, disabledState, "peer inline-flex h-5 w-9 shrink-0 items-center rounded-none border border-transparent bg-input p-0.5 shadow-xs transition-colors data-checked:bg-primary dark:data-checked:bg-primary", className), ...props, children: _jsx(SwitchPrimitive.Thumb, { "data-slot": "switch-thumb", className: "pointer-events-none block size-3.5 rounded-none bg-background shadow-sm ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground" }) }));
13
14
  }
14
15
  export { Switch };
package/dist/table.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
+ import { focusRing } from "./internal/styles.js";
4
5
  import { cn } from "./utils.js";
5
6
  /**
6
7
  * True while the element's content is wider than its box. Re-checked when
@@ -32,7 +33,7 @@ function useHorizontalOverflow(ref) {
32
33
  function Table({ className, ...props }) {
33
34
  const containerRef = React.useRef(null);
34
35
  const scrollable = useHorizontalOverflow(containerRef);
35
- return (_jsx("div", { ref: containerRef, "data-slot": "table-container", tabIndex: scrollable ? 0 : undefined, className: "relative w-full min-w-0 max-w-full overflow-x-auto overscroll-x-contain outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx("table", { "data-slot": "table", className: cn("w-full min-w-full caption-bottom text-sm", className), ...props }) }));
36
+ return (_jsx("div", { ref: containerRef, "data-slot": "table-container", tabIndex: scrollable ? 0 : undefined, className: cn(focusRing, "relative w-full min-w-0 max-w-full overflow-x-auto overscroll-x-contain"), children: _jsx("table", { "data-slot": "table", className: cn("w-full min-w-full caption-bottom text-sm", className), ...props }) }));
36
37
  }
37
38
  function TableHeader({ className, ...props }) {
38
39
  return (_jsx("thead", { "data-slot": "table-header", className: cn("[&_tr]:border-b", className), ...props }));
package/dist/textarea.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
+ import { disabledState, fieldSurface, focusRing, invalidState, } from "./internal/styles.js";
3
4
  import { cn } from "./utils.js";
4
5
  function Textarea({ className, ...props }) {
5
- return (_jsx("textarea", { "data-slot": "textarea", className: cn("flex field-sizing-content min-h-16 w-full resize-none rounded-none border border-input bg-background px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 md:text-sm dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", className), ...props }));
6
+ return (_jsx("textarea", { "data-slot": "textarea", className: cn(fieldSurface, focusRing, invalidState, disabledState, "flex field-sizing-content min-h-16 resize-none", className), ...props }));
6
7
  }
7
8
  export { Textarea };
package/dist/toggle.d.ts CHANGED
@@ -2,15 +2,11 @@ import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  import { Toggle as TogglePrimitive } from "radix-ui";
4
4
  /**
5
- * Selected chrome shared by standalone Toggle and ToggleGroupItem.
6
- *
7
- * Radix Toggle sets `data-state=on` and `aria-pressed`. ToggleGroup items
8
- * always set `data-state=on`; in multiple mode they also set `aria-pressed`,
9
- * but in single mode they are radios with `aria-checked` and no
10
- * `aria-pressed`. All three selectors apply the same wash, text, and
11
- * selected-foreground border so the state meets WCAG 1.4.11 in every mode.
5
+ * Selected chrome shared by Toggle and ToggleGroupItem (see
6
+ * `selectedState.toggle`): the wash, selected text, and a selected border
7
+ * for `data-state=on`, `aria-pressed`, and `aria-checked`.
12
8
  */
13
- declare const selectedToggleChrome = "data-on:border-selected-foreground data-on:bg-selected data-on:text-selected-foreground aria-pressed:border-selected-foreground aria-pressed:bg-selected aria-pressed:text-selected-foreground aria-checked:border-selected-foreground aria-checked:bg-selected aria-checked:text-selected-foreground";
9
+ declare const selectedToggleChrome: string;
14
10
  declare const toggleVariants: (props?: ({
15
11
  variant?: "default" | "outline" | null | undefined;
16
12
  size?: "default" | "sm" | "lg" | null | undefined;
package/dist/toggle.js CHANGED
@@ -3,18 +3,22 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { cva } from "class-variance-authority";
5
5
  import { Toggle as TogglePrimitive } from "radix-ui";
6
+ import { disabledState, focusRing, invalidState, selectedState, } from "./internal/styles.js";
6
7
  import { cn } from "./utils.js";
7
8
  /**
8
- * Selected chrome shared by standalone Toggle and ToggleGroupItem.
9
- *
10
- * Radix Toggle sets `data-state=on` and `aria-pressed`. ToggleGroup items
11
- * always set `data-state=on`; in multiple mode they also set `aria-pressed`,
12
- * but in single mode they are radios with `aria-checked` and no
13
- * `aria-pressed`. All three selectors apply the same wash, text, and
14
- * selected-foreground border so the state meets WCAG 1.4.11 in every mode.
9
+ * Selected chrome shared by Toggle and ToggleGroupItem (see
10
+ * `selectedState.toggle`): the wash, selected text, and a selected border
11
+ * for `data-state=on`, `aria-pressed`, and `aria-checked`.
15
12
  */
16
- const selectedToggleChrome = "data-on:border-selected-foreground data-on:bg-selected data-on:text-selected-foreground aria-pressed:border-selected-foreground aria-pressed:bg-selected aria-pressed:text-selected-foreground aria-checked:border-selected-foreground aria-checked:bg-selected aria-checked:text-selected-foreground";
17
- const toggleVariants = cva(`group/toggle inline-flex items-center justify-center gap-1.5 rounded-none text-xs font-semibold tracking-widest whitespace-nowrap uppercase transition-colors outline-none hover:border-selected-foreground hover:bg-selected hover:text-selected-foreground focus-visible:border-ring focus-visible:text-selected-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 ${selectedToggleChrome} [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5`, {
13
+ const selectedToggleChrome = selectedState.toggle;
14
+ const toggleVariants = cva([
15
+ focusRing,
16
+ selectedState.hover,
17
+ selectedToggleChrome,
18
+ invalidState,
19
+ disabledState,
20
+ "group/toggle inline-flex items-center justify-center gap-1.5 rounded-none text-xs font-semibold tracking-widest whitespace-nowrap uppercase transition-colors focus-visible:text-selected-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
21
+ ], {
18
22
  variants: {
19
23
  variant: {
20
24
  default: "border border-transparent bg-transparent",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas-react-kit",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Shared React components and Tailwind theme for Atlas",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -245,6 +245,7 @@
245
245
  "build": "tsc",
246
246
  "check": "npm run build && prettier --check .",
247
247
  "test": "npm run build && vitest run",
248
+ "test:coverage": "npm run build && vitest run --coverage",
248
249
  "prepack": "npm run build"
249
250
  },
250
251
  "engines": {
@@ -286,6 +287,7 @@
286
287
  "@types/node": "^25.5.0",
287
288
  "@types/react": "^19.2.14",
288
289
  "@types/react-dom": "^19.2.3",
290
+ "@vitest/coverage-v8": "4.1.11",
289
291
  "jsdom": "^29.1.1",
290
292
  "next": "16.2.6",
291
293
  "prettier": "^3.8.1",
@@ -39,8 +39,8 @@ and tells Tailwind to scan the kit's compiled files:
39
39
 
40
40
  Dark mode needs `ThemeProvider` from `atlas-react-kit/theme-provider` near the
41
41
  root (or a `dark` class on an ancestor). Toasts need one `<Toaster />` from
42
- `atlas-react-kit/sonner`; call `toast()` from the `sonner` package, which the
43
- app should list as its own dependency. Fix missing setup rather than working
42
+ `atlas-react-kit/sonner`; import `toast` from `atlas-react-kit/sonner` too,
43
+ never from `sonner` (a second copy of Sonner never reaches the kit's toaster). Fix missing setup rather than working
44
44
  around unstyled components.
45
45
 
46
46
  ## 3. Pick the kit component for the job