atlas-react-kit 0.2.1 → 0.4.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
@@ -5,39 +5,39 @@ 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
- | `button` | `Button`, `buttonVariants`; `variant`, `size`, `asChild` |
11
- | `badge` | `Badge`, `badgeVariants`; `variant`, `tone`, `asChild` |
12
- | `calendar` | `Calendar`, `CalendarDayButton`; React Day Picker props and `buttonVariant` |
13
- | `card` | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardAction`, `CardContent`, `CardFooter`; `Card` supports `flush` |
14
- | `checkbox` | `Checkbox` |
15
- | `combobox` | `Combobox`; `options` as `{ value, label }[]`, `value`, `onValueChange`; optional placeholder, search, empty text |
16
- | `date-picker` | `DatePickerButton`, `toDate`, `formatLocalDate`; `value` is `Date` or `yyyy-MM-dd` |
17
- | `dialog` | `Dialog`, trigger, portal, overlay, content, close, header, footer, title, description; `DialogContent` supports `showCloseButton` |
18
- | `empty-panel` | `EmptyPanel`; required `children`; optional `size`, `muted`, `className` |
19
- | `error-boundary` | `ErrorBoundary`, `ErrorFallback`; optional `message`; retry resets, reload refreshes the page |
20
- | `field` | Field, label, legend, description, group, set, title, content, separator, error; `Field` supports `orientation` |
21
- | `input`, `textarea`, `label` | `Input`, `Textarea`, `Label` |
22
- | `popover` | `Popover`, trigger, anchor, content, header, title, description |
23
- | `select` | Select, trigger, value, content, group, label, item, separator, scroll buttons; trigger `size` |
24
- | `separator`, `skeleton` | `Separator`, `Skeleton` |
25
- | `sortable-table-head` | `SortableTableHead`; `sorted`, `direction` (`asc` \| `desc`), `onClick`; sets `aria-sort` |
26
- | `sonner` | `Toaster`; Sonner props, with `next-themes` theme defaults |
27
- | `table` | Table, header, body, footer, row, head, cell, caption |
28
- | `table-body-skeleton` | `TableBodySkeleton`; `columns`, optional `rows` (default 5) |
29
- | `tabs` | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, `TabsNav`, `tabsListVariants`; list `variant` is `default` or `line` |
30
- | `toggle` | `Toggle`, `toggleVariants`; `variant`, `size` |
31
- | `toggle-group` | `ToggleGroup`, `ToggleGroupItem`; `variant`, `size` |
32
- | `theme-provider` | `ThemeProvider`, `useTheme`; optional `storageKey` (default `atlas-theme`), `enableShortcut` for the `d` toggle |
33
- | `form/text-field` | `FormTextField`, type `TextFieldApi`; controlled field, label, multiline, description, input constraints |
34
- | `form/select-field` | `FormSelectField`; `value`, `onValueChange`, `label`, optional `meta` and placeholder; select item children |
35
- | `form/date-picker-field` | `FormDatePickerField`; `value`, `onValueChange`, `label`, optional `meta` and placeholder |
36
- | `form/field-error` | `FieldError`, `getFieldErrorMessage`; types `FieldErrorMessage`, `FieldMetaState` |
37
- | `form/feedback-field` | `FormFeedbackField`; nullable `message` |
38
- | `next/tabs-nav-link` | `TabsNavLink`; Next Link props and required `active` boolean; requires Next.js 16 |
39
- | `utils` | `cn`, `onInputChange` |
40
- | `theme.css` | Tailwind 4 theme; installation in [README](./README.md) |
8
+ | Subpath | Main exports and options |
9
+ | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
10
+ | `button` | `Button`, `buttonVariants`, `quietButtonInteraction`; `variant`, `size`, `asChild`; outline/ghost hover is brand purple + selected wash, not muted grey |
11
+ | `badge` | `Badge`, `badgeVariants`; `variant`, `tone`, `asChild` |
12
+ | `calendar` | `Calendar`, `CalendarDayButton`; React Day Picker props and `buttonVariant` |
13
+ | `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; `CardContent className="px-0"` overrides padding without `!` |
14
+ | `checkbox` | `Checkbox` |
15
+ | `combobox` | `Combobox`; `options` as `{ value, label }[]`, `value`, `onValueChange`; optional placeholder, search, empty text; trigger matches Input/SelectTrigger chrome; keyboard highlight scrolls into view |
16
+ | `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` |
17
+ | `dialog` | `Dialog`, trigger, portal, overlay, content, close, header, footer, title, description; `DialogContent` supports `showCloseButton` |
18
+ | `empty-panel` | `EmptyPanel`; required `children`; optional `size`, `muted`, `className` |
19
+ | `error-boundary` | `ErrorBoundary`, `ErrorFallback`; optional `message`; retry resets, reload refreshes the page |
20
+ | `field` | Field, label, legend, description, group, set, title, content, separator, error; `Field` supports `orientation` |
21
+ | `input`, `textarea`, `label` | `Input`, `Textarea`, `Label` |
22
+ | `popover` | `Popover`, trigger, anchor, content, header, title, description |
23
+ | `select` | Select, trigger, value, content, group, label, item, separator, scroll buttons; trigger `size`; trigger text is `text-base md:text-sm` like Input |
24
+ | `separator`, `skeleton` | `Separator`, `Skeleton` |
25
+ | `sortable-table-head` | `SortableTableHead`; `sorted`, `direction` (`asc` \| `desc`), `onClick`; sets `aria-sort` |
26
+ | `sonner` | `Toaster`; Sonner props, with `next-themes` theme defaults |
27
+ | `table` | Table, header, body, footer, row, head, cell, caption |
28
+ | `table-body-skeleton` | `TableBodySkeleton`; `columns`, optional `rows` (default 5) |
29
+ | `tabs` | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, `TabsNav`, `tabsListVariants`; list `variant` is `default` or `line` |
30
+ | `toggle` | `Toggle`, `toggleVariants`; `variant`, `size` |
31
+ | `toggle-group` | `ToggleGroup`, `ToggleGroupItem`; `variant`, `size` |
32
+ | `theme-provider` | `ThemeProvider`, `useTheme`; optional `storageKey` (default `atlas-theme`), `enableShortcut` for the `d` toggle |
33
+ | `form/text-field` | `FormTextField`, type `TextFieldApi`; controlled field, label, multiline, description, input constraints |
34
+ | `form/select-field` | `FormSelectField`; `value`, `onValueChange`, `label`, optional `meta` and placeholder; select item children |
35
+ | `form/date-picker-field` | `FormDatePickerField`; `value`, `onValueChange`, `label`, optional `meta`, placeholder, and `className` (forwarded to the trigger) |
36
+ | `form/field-error` | `FieldError`, `getFieldErrorMessage`; types `FieldErrorMessage`, `FieldMetaState` |
37
+ | `form/feedback-field` | `FormFeedbackField`; nullable `message` |
38
+ | `next/tabs-nav-link` | `TabsNavLink`; Next Link props and required `active` boolean; requires Next.js 16 |
39
+ | `utils` | `cn`, `onInputChange` |
40
+ | `theme.css` | Tailwind 4 theme; installation in [README](./README.md) |
41
41
 
42
42
  ## Forms
43
43
 
@@ -62,6 +62,42 @@ accept the same error meta shape. `FormDatePickerField` takes a `Date` or
62
62
  `yyyy-MM-dd` string and calls `onValueChange` with `Date | undefined`.
63
63
  `FieldError` accepts either `message` or pre-resolved `text`.
64
64
 
65
+ `DatePickerButton` is single-day by default. For a from/to range, set
66
+ `mode="range"`; `value` is `{ from, to }` (each side `Date` or `yyyy-MM-dd`)
67
+ and `onChange` receives `DateRange | undefined`. The trigger uses the same
68
+ form-control chrome as `Input` and `SelectTrigger`. The range popover shows
69
+ one month below `md` and two months at `md+` unless `numberOfMonths` is set,
70
+ clamps to the viewport with collision padding and scroll, stays `w-auto` so
71
+ it does not collapse to the default popover width, and includes a Clear
72
+ action. Pass `side` to prefer a placement; Radix still flips on collision.
73
+ `FormDatePickerField` forwards `className` to that trigger.
74
+
75
+ Outline and ghost `Button` hover and focus use `border-primary`,
76
+ `text-primary`, and `bg-selected`. They do not use a muted grey fill, including
77
+ when the control sits on a selected row (for example "Clear selection").
78
+
79
+ ```tsx
80
+ import { DatePickerButton, type DateRange } from "atlas-react-kit/date-picker"
81
+
82
+ export function ExportRange({
83
+ value,
84
+ onChange,
85
+ }: {
86
+ value: DateRange | undefined
87
+ onChange: (range: DateRange | undefined) => void
88
+ }) {
89
+ return (
90
+ <DatePickerButton
91
+ mode="range"
92
+ value={value}
93
+ onChange={onChange}
94
+ numberOfMonths={2}
95
+ placeholder="Export range"
96
+ />
97
+ )
98
+ }
99
+ ```
100
+
65
101
  ## Theme
66
102
 
67
103
  ```tsx
package/DESIGN_RULES.md CHANGED
@@ -11,8 +11,10 @@ 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`. Hover stays neutral with `hover:bg-muted`
15
- or `hover:bg-accent`. The shipped controls apply this distinction.
14
+ `bg-selected text-selected-foreground`. Outline and ghost buttons hover and
15
+ focus with `border-primary text-primary` and the selected wash, not a muted
16
+ grey fill. Table row hover can stay `hover:bg-muted`. The shipped controls
17
+ apply this distinction.
16
18
 
17
19
  Use `success`, `warning`, and `destructive` for good, caution, and bad states.
18
20
  For tinted status chips, use `Badge variant="soft" tone="success"` or
@@ -28,8 +30,16 @@ other circles. Card and dialog titles already use uppercase, tracked
28
30
  `font-heading` styling.
29
31
 
30
32
  Use `EmptyPanel` for empty states, `TableBodySkeleton` for loading table bodies,
31
- and `<Card flush>` for an edge-to-edge list or table. `Dialog` already scrolls
32
- inside the viewport on small screens; avoid additional height limits.
33
+ and `<Card flush>` for an edge-to-edge list or table. `CardContent className="px-0"`
34
+ overrides body padding without `!px-0` when only the body should go flush. Cards
35
+ keep `min-h-min shrink-0` so they do not collapse inside a bounded flex
36
+ column. `Dialog` already scrolls inside the viewport on small screens; avoid
37
+ additional height limits.
38
+
39
+ `Input`, `SelectTrigger`, `DatePickerButton`, and `Combobox` share form-control
40
+ chrome: 40px height, `px-3`, `text-base md:text-sm`, `border-input`, and
41
+ `shadow-xs`. Do not restyle those triggers as outline buttons in application
42
+ code.
33
43
 
34
44
  Use `Tabs` with actual `TabsContent` panels. For URL view switches, use a labelled
35
45
  `TabsNav` with links and `aria-current="page"` on the active link. The optional
@@ -37,7 +47,8 @@ Next adapter provides `TabsNavLink` with this behavior.
37
47
 
38
48
  Use `FormTextField`, `FormSelectField`, `FormDatePickerField`, and
39
49
  `FormFeedbackField` for portable form controls, and `FieldError` for field
40
- messages. `Combobox`, `DatePickerButton`, `ThemeProvider`, `ErrorBoundary`, and
41
- `SortableTableHead` are first-class kit pieces. Domain data and application
42
- shells stay with the application: currency or timezone option lists, query-library
43
- error views, markdown editors, kanban boards, and dashboard widgets.
50
+ messages. `Combobox`, `DatePickerButton` (including `mode="range"`),
51
+ `ThemeProvider`, `ErrorBoundary`, and `SortableTableHead` are first-class kit
52
+ pieces. Domain data and application shells stay with the application: currency
53
+ or timezone option lists, query-library error views, markdown editors, kanban
54
+ boards, and dashboard widgets.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Shared React components and a Tailwind theme with square corners, semantic
4
4
  colors, and distinct selected states. React 19 and Tailwind 4 are required.
5
5
 
6
6
  ```sh
7
- npm install --save-exact atlas-react-kit@0.2.1
7
+ npm install --save-exact atlas-react-kit@0.4.0
8
8
  ```
9
9
 
10
10
  Import components by subpath:
@@ -59,7 +59,7 @@ without compilation and processed by the consuming application's Tailwind build.
59
59
  The optional Next.js 16 adapter requires Next only when imported. The package
60
60
  does not bundle React.
61
61
 
62
- 0.1.0 and 0.2.0 are already on npm. Bumping the version in `package.json` and
62
+ 0.3.0 is already on npm. Bumping the version in `package.json` and
63
63
  merging to `main` (path-filtered to package files) or running
64
64
  `workflow_dispatch` publishes through npm OIDC trusted publishing after
65
65
  check, test, and pack gates. Versions already on npm are skipped.
@@ -70,7 +70,7 @@ artifact with the package owner's npm login:
70
70
  ```sh
71
71
  mkdir -p release-artifacts
72
72
  npm pack --pack-destination release-artifacts
73
- npm publish ./release-artifacts/atlas-react-kit-0.2.1.tgz --access public
73
+ npm publish ./release-artifacts/atlas-react-kit-0.4.0.tgz --access public
74
74
  ```
75
75
 
76
76
  Pin an exact package version in consuming applications. Roll back an upgrade by
package/dist/button.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
+ /**
4
+ * Outline and ghost hover/focus: brand purple border and text, optional
5
+ * selected wash, no muted grey fill. Used by quiet actions such as
6
+ * "Clear selection" sitting on a selected row.
7
+ */
8
+ declare const quietButtonInteraction = "hover:border-primary hover:bg-selected hover:text-primary focus-visible:border-primary focus-visible:text-primary focus-visible:ring-primary/30 aria-expanded:border-primary aria-expanded:bg-selected aria-expanded:text-primary";
3
9
  declare const buttonVariants: (props?: ({
4
10
  variant?: "default" | "secondary" | "destructive" | "outline" | "ghost" | "link" | null | undefined;
5
11
  size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
@@ -7,4 +13,4 @@ declare const buttonVariants: (props?: ({
7
13
  declare function Button({ className, variant, size, asChild, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
8
14
  asChild?: boolean;
9
15
  }): React.JSX.Element;
10
- export { Button, buttonVariants };
16
+ export { Button, buttonVariants, quietButtonInteraction };
package/dist/button.js CHANGED
@@ -3,13 +3,19 @@ import * as React from "react";
3
3
  import { cva } from "class-variance-authority";
4
4
  import { Slot } from "radix-ui";
5
5
  import { cn } from "./utils.js";
6
+ /**
7
+ * Outline and ghost hover/focus: brand purple border and text, optional
8
+ * selected wash, no muted grey fill. Used by quiet actions such as
9
+ * "Clear selection" sitting on a selected row.
10
+ */
11
+ const quietButtonInteraction = "hover:border-primary hover:bg-selected hover:text-primary focus-visible:border-primary focus-visible:text-primary focus-visible:ring-primary/30 aria-expanded:border-primary aria-expanded:bg-selected aria-expanded:text-primary";
6
12
  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", {
7
13
  variants: {
8
14
  variant: {
9
15
  default: "bg-primary text-primary-foreground hover:bg-primary/80",
10
- outline: "border-border bg-transparent hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-input/30",
16
+ outline: `border-border bg-transparent ${quietButtonInteraction}`,
11
17
  secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
12
- ghost: "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
18
+ ghost: quietButtonInteraction,
13
19
  destructive: "bg-destructive/10 text-destructive hover:bg-destructive/20 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",
14
20
  link: "text-primary underline underline-offset-4 hover:underline",
15
21
  },
@@ -33,4 +39,4 @@ function Button({ className, variant = "default", size = "default", asChild = fa
33
39
  const Comp = asChild ? Slot.Root : "button";
34
40
  return (_jsx(Comp, { "data-slot": "button", "data-variant": variant, "data-size": size, className: cn(buttonVariants({ variant, size, className })), ...props }));
35
41
  }
36
- export { Button, buttonVariants };
42
+ export { Button, buttonVariants, quietButtonInteraction };
package/dist/card.d.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import * as React from "react";
2
2
  /**
3
- * Padding lives in `--card-p` (set here, read by the sections) so plain
4
- * `py-*` / `px-*` classes on Card or its sections override it normally.
5
- * `flush` drops all padding so a list or table runs edge to edge.
3
+ * Size and flush padding live in `--card-p` (set here, read from `theme.css`
4
+ * `@layer components`) so a utility `px-0` / `py-0` on Card or its sections
5
+ * overrides it without `!`. `flush` sets the variable to 0 so lists and
6
+ * tables can run edge to edge. `min-h-min shrink-0` keeps the card from
7
+ * collapsing inside a bounded flex column while `overflow-hidden` still
8
+ * clips media.
6
9
  */
7
10
  declare function Card({ className, size, flush, ...props }: React.ComponentProps<"div"> & {
8
11
  size?: "default" | "sm";
package/dist/card.js CHANGED
@@ -2,17 +2,20 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { cn } from "./utils.js";
4
4
  /**
5
- * Padding lives in `--card-p` (set here, read by the sections) so plain
6
- * `py-*` / `px-*` classes on Card or its sections override it normally.
7
- * `flush` drops all padding so a list or table runs edge to edge.
5
+ * Size and flush padding live in `--card-p` (set here, read from `theme.css`
6
+ * `@layer components`) so a utility `px-0` / `py-0` on Card or its sections
7
+ * overrides it without `!`. `flush` sets the variable to 0 so lists and
8
+ * tables can run edge to edge. `min-h-min shrink-0` keeps the card from
9
+ * collapsing inside a bounded flex column while `overflow-hidden` still
10
+ * clips media.
8
11
  */
9
12
  function Card({ className, size = "default", flush = false, ...props }) {
10
- return (_jsx("div", { "data-slot": "card", "data-size": size, className: cn("group/card flex flex-col overflow-hidden bg-card py-(--card-p) text-sm text-card-foreground shadow-sm ring-1 ring-foreground/5 has-[>img:first-child]:pt-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none", size === "sm"
13
+ return (_jsx("div", { "data-slot": "card", "data-size": size, className: cn("group/card flex min-h-min shrink-0 flex-col overflow-hidden bg-card text-sm text-card-foreground shadow-sm ring-1 ring-foreground/5 has-[>img:first-child]:pt-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none", size === "sm"
11
14
  ? "gap-5 [--card-p:--spacing(5)]"
12
15
  : "gap-8 [--card-p:--spacing(8)]", flush && "[--card-p:0px]", className), ...props }));
13
16
  }
14
17
  function CardHeader({ className, ...props }) {
15
- return (_jsx("div", { "data-slot": "card-header", className: cn("group/card-header @container/card-header grid auto-rows-min items-start gap-1.5 rounded-none px-(--card-p) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-p)", className), ...props }));
18
+ return (_jsx("div", { "data-slot": "card-header", className: cn("group/card-header @container/card-header grid auto-rows-min items-start gap-1.5 rounded-none has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]", className), ...props }));
16
19
  }
17
20
  function CardTitle({ as: Component = "div", className, ...props }) {
18
21
  return (_jsx(Component, { "data-slot": "card-title", className: cn("font-heading text-base font-semibold tracking-wider uppercase", className), ...props }));
@@ -24,9 +27,9 @@ function CardAction({ className, ...props }) {
24
27
  return (_jsx("div", { "data-slot": "card-action", className: cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className), ...props }));
25
28
  }
26
29
  function CardContent({ className, ...props }) {
27
- return (_jsx("div", { "data-slot": "card-content", className: cn("px-(--card-p)", className), ...props }));
30
+ return _jsx("div", { "data-slot": "card-content", className: className, ...props });
28
31
  }
29
32
  function CardFooter({ className, ...props }) {
30
- return (_jsx("div", { "data-slot": "card-footer", className: cn("flex items-center px-(--card-p) [.border-t]:pt-(--card-p)", className), ...props }));
33
+ return (_jsx("div", { "data-slot": "card-footer", className: cn("flex items-center", className), ...props }));
31
34
  }
32
35
  export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent, };
@@ -17,7 +17,10 @@ type ComboboxProps = {
17
17
  };
18
18
  /**
19
19
  * Searchable single-select built from Button, Popover, and Input.
20
+ * The trigger uses the same form-control chrome as Input and SelectTrigger.
20
21
  * Options are a plain `{ value, label }` list; domain data stays in the app.
22
+ * Keyboard highlight (arrows, Home/End, PageUp/PageDown) scrolls the active
23
+ * option into view inside the overflow list.
21
24
  */
22
25
  export declare function Combobox({ options, value, onValueChange, placeholder, searchPlaceholder, emptyText, disabled, id, className, "aria-invalid": ariaInvalid, }: ComboboxProps): React.JSX.Element;
23
26
  export {};
package/dist/combobox.js CHANGED
@@ -4,11 +4,21 @@ import * as React from "react";
4
4
  import { CheckIcon, ChevronDownIcon } from "lucide-react";
5
5
  import { Button } from "./button.js";
6
6
  import { Input } from "./input.js";
7
+ import { formControlTriggerClassName } from "./internal/form-control.js";
7
8
  import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
8
9
  import { cn, onInputChange } from "./utils.js";
10
+ /**
11
+ * PageUp/PageDown jump size. The option list is `max-h-60`; ten rows is a
12
+ * bit more than one viewport so long lists (timezones, currencies) move
13
+ * without requiring an End key.
14
+ */
15
+ const LISTBOX_PAGE_SIZE = 10;
9
16
  /**
10
17
  * Searchable single-select built from Button, Popover, and Input.
18
+ * The trigger uses the same form-control chrome as Input and SelectTrigger.
11
19
  * Options are a plain `{ value, label }` list; domain data stays in the app.
20
+ * Keyboard highlight (arrows, Home/End, PageUp/PageDown) scrolls the active
21
+ * option into view inside the overflow list.
12
22
  */
13
23
  export function Combobox({ options, value, onValueChange, placeholder = "Select", searchPlaceholder = "Search", emptyText = "No results", disabled, id, className, "aria-invalid": ariaInvalid, }) {
14
24
  const [open, setOpen] = React.useState(false);
@@ -23,18 +33,30 @@ export function Combobox({ options, value, onValueChange, placeholder = "Select"
23
33
  return options;
24
34
  return options.filter((option) => option.label.toLowerCase().includes(needle));
25
35
  }, [options, query]);
26
- // Highlight and search reset when the popover opens or closes; options/value
27
- // are read for the initial highlight rather than subscribed as deps.
28
- React.useEffect(() => {
29
- if (!open) {
36
+ const onOpenChange = (next) => {
37
+ setOpen(next);
38
+ if (!next) {
30
39
  setQuery("");
31
40
  return;
32
41
  }
42
+ // Set the highlight in the same turn as open so the first painted list
43
+ // already points at the selected option (a timezone at the end of 400
44
+ // rows) instead of flashing index 0 and scrolling twice.
33
45
  const selectedIndex = options.findIndex((option) => option.value === value);
34
46
  setHighlight(selectedIndex >= 0 ? selectedIndex : 0);
47
+ };
48
+ React.useEffect(() => {
49
+ if (!open)
50
+ return;
35
51
  const frame = window.requestAnimationFrame(() => inputRef.current?.focus());
36
52
  return () => window.cancelAnimationFrame(frame);
37
53
  }, [open]);
54
+ // Callback ref (not an effect) so scroll runs when Radix mounts the list
55
+ // and when the highlight moves. A layout effect on `open` is too early:
56
+ // the option node does not exist until the popover content is in the DOM.
57
+ const scrollHighlightedOption = React.useCallback((node) => {
58
+ node?.scrollIntoView({ block: "nearest" });
59
+ }, []);
38
60
  const onQueryChange = onInputChange((next) => {
39
61
  setQuery(next);
40
62
  setHighlight(0);
@@ -59,7 +81,8 @@ export function Combobox({ options, value, onValueChange, placeholder = "Select"
59
81
  if (option)
60
82
  selectValue(option.value);
61
83
  }
62
- // Home/End jump the highlight; preventDefault so the search caret stays put.
84
+ // Home/End/Page jump the highlight; preventDefault so the search caret
85
+ // stays put and the page behind the list does not scroll.
63
86
  if (event.key === "Home") {
64
87
  event.preventDefault();
65
88
  setHighlight(0);
@@ -68,12 +91,22 @@ export function Combobox({ options, value, onValueChange, placeholder = "Select"
68
91
  event.preventDefault();
69
92
  setHighlight(filtered.length === 0 ? 0 : filtered.length - 1);
70
93
  }
94
+ if (event.key === "PageDown") {
95
+ event.preventDefault();
96
+ setHighlight((index) => filtered.length === 0
97
+ ? 0
98
+ : Math.min(index + LISTBOX_PAGE_SIZE, filtered.length - 1));
99
+ }
100
+ if (event.key === "PageUp") {
101
+ event.preventDefault();
102
+ setHighlight((index) => Math.max(index - LISTBOX_PAGE_SIZE, 0));
103
+ }
71
104
  };
72
- return (_jsxs(Popover, { open: open, onOpenChange: setOpen, 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, disabled: disabled, className: cn("h-10 w-full justify-between font-normal tracking-normal normal-case", !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
105
+ 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, 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
73
106
  ? `${listId}-option-${activeOption.value}`
74
107
  : 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) => {
75
108
  const isSelected = option.value === value;
76
109
  const isActive = index === highlight;
77
- return (_jsx("li", { role: "none", children: _jsxs("button", { id: `${listId}-option-${option.value}`, 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));
110
+ return (_jsx("li", { role: "none", children: _jsxs("button", { ref: isActive ? scrollHighlightedOption : undefined, id: `${listId}-option-${option.value}`, 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));
78
111
  })) })] })] }));
79
112
  }
@@ -1,5 +1,13 @@
1
1
  import * as React from "react";
2
+ import type { DateRange } from "react-day-picker";
3
+ import { PopoverContent } from "./popover.js";
4
+ export type { DateRange };
2
5
  export type DateLike = Date | string | number | null | undefined;
6
+ /** From/to value for `DatePickerButton` in range mode; each side is `DateLike`. */
7
+ export type DatePickerRangeValue = {
8
+ from?: DateLike;
9
+ to?: DateLike;
10
+ };
3
11
  /**
4
12
  * Coerce a date-like value to a local `Date`.
5
13
  * Plain calendar dates (`yyyy-MM-dd`) parse as local midnight so they do not
@@ -8,18 +16,41 @@ export type DateLike = Date | string | number | null | undefined;
8
16
  export declare function toDate(value: DateLike): Date | undefined;
9
17
  /** Format a Date as a timezone-safe calendar date (`yyyy-MM-dd`). */
10
18
  export declare function formatLocalDate(date: Date): string;
11
- type DatePickerButtonProps = {
19
+ type DatePickerSharedProps = {
12
20
  id?: string;
13
- value?: DateLike;
14
- onChange: (date: Date | undefined) => void;
15
21
  placeholder?: string;
16
22
  disabled?: boolean;
17
23
  className?: string;
18
24
  "aria-invalid"?: boolean;
25
+ /**
26
+ * Visible months in the popover calendar. Defaults to 1 in single mode.
27
+ * In range mode, defaults to 1 below the `md` breakpoint and 2 at `md`
28
+ * and above so two stacked months do not open off-screen on mobile.
29
+ */
30
+ numberOfMonths?: number;
31
+ /** Popover side. Defaults to `bottom`; Radix still flips on collision. */
32
+ side?: React.ComponentProps<typeof PopoverContent>["side"];
33
+ /** Month shown when the popover opens; falls back to the selected date. */
34
+ defaultMonth?: Date;
35
+ };
36
+ export type DatePickerSingleProps = DatePickerSharedProps & {
37
+ mode?: "single";
38
+ value?: DateLike;
39
+ onChange: (date: Date | undefined) => void;
40
+ };
41
+ export type DatePickerRangeProps = DatePickerSharedProps & {
42
+ mode: "range";
43
+ value?: DatePickerRangeValue;
44
+ onChange: (range: DateRange | undefined) => void;
19
45
  };
46
+ export type DatePickerButtonProps = DatePickerSingleProps | DatePickerRangeProps;
20
47
  /**
21
- * Calendar popover trigger. Accepts `Date` or `yyyy-MM-dd` strings; emits
22
- * `Date | undefined`. Square corners follow the shared shape rules.
48
+ * Calendar popover trigger styled as a form control (same chrome as Input
49
+ * and SelectTrigger). `mode="single"` (default) accepts `Date` or
50
+ * `yyyy-MM-dd` and emits `Date | undefined`. `mode="range"` accepts
51
+ * `{ from, to }` and emits `DateRange | undefined`. Range mode shows one
52
+ * month below `md` and two at `md+` unless `numberOfMonths` is set. The
53
+ * popover clamps to the viewport (`side` is overridable). Square corners
54
+ * follow the shared shape rules.
23
55
  */
24
- export declare function DatePickerButton({ id, value, onChange, placeholder, disabled, className, "aria-invalid": ariaInvalid, }: DatePickerButtonProps): React.JSX.Element;
25
- export {};
56
+ export declare function DatePickerButton(props: DatePickerButtonProps): React.JSX.Element;
@@ -1,10 +1,12 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { format, parseISO } from "date-fns";
4
+ import { format, isSameDay, parseISO } from "date-fns";
5
5
  import { CalendarIcon } from "lucide-react";
6
6
  import { Button } from "./button.js";
7
7
  import { Calendar } from "./calendar.js";
8
+ import { formControlTriggerClassName } from "./internal/form-control.js";
9
+ import { MD_MEDIA_QUERY, useMediaQuery } from "./internal/use-media-query.js";
8
10
  import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
9
11
  import { cn } from "./utils.js";
10
12
  /**
@@ -44,14 +46,61 @@ export function formatLocalDate(date) {
44
46
  return format(date, "yyyy-MM-dd");
45
47
  }
46
48
  /**
47
- * Calendar popover trigger. Accepts `Date` or `yyyy-MM-dd` strings; emits
48
- * `Date | undefined`. Square corners follow the shared shape rules.
49
+ * Trigger label for a range: one PPP date while the end is missing or the
50
+ * same day as the start, otherwise `from – to`.
49
51
  */
50
- export function DatePickerButton({ id, value, onChange, placeholder = "Pick a date", disabled, className, "aria-invalid": ariaInvalid, }) {
52
+ function formatRangeTriggerLabel(from, to, placeholder) {
53
+ if (!from)
54
+ return placeholder;
55
+ if (!to || isSameDay(from, to))
56
+ return format(from, "PPP");
57
+ return `${format(from, "PPP")} – ${format(to, "PPP")}`;
58
+ }
59
+ /** Gutter used when the calendar popover collides with the viewport. */
60
+ const DATE_PICKER_COLLISION_PADDING = 16;
61
+ /** Shared trigger + auto-sized popover chrome for single and range modes. */
62
+ function DatePickerShell({ id, disabled, ariaInvalid, className, empty, label, open, onOpenChange, side, children, }) {
63
+ return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { id: id, type: "button", variant: "outline", disabled: disabled, "aria-invalid": ariaInvalid, "data-empty": empty, className: cn(formControlTriggerClassName, "justify-start data-[empty=true]:text-muted-foreground", className), children: [_jsx(CalendarIcon, { "data-icon": "inline-start" }), label] }) }), _jsx(PopoverContent, { align: "start", side: side, collisionPadding: DATE_PICKER_COLLISION_PADDING, className: "w-auto max-h-(--radix-popover-content-available-height) overflow-y-auto rounded-none p-0", children: children })] }));
64
+ }
65
+ function DatePickerSingleButton({ id, value, onChange, placeholder = "Pick a date", disabled, className, "aria-invalid": ariaInvalid, numberOfMonths, side, defaultMonth, }) {
51
66
  const [open, setOpen] = React.useState(false);
52
67
  const date = toDate(value);
53
- return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { id: id, type: "button", variant: "outline", disabled: disabled, "aria-invalid": ariaInvalid, "data-empty": !date, className: cn("h-10 w-full justify-start font-normal tracking-normal normal-case data-[empty=true]:text-muted-foreground", className), children: [_jsx(CalendarIcon, { "data-icon": "inline-start" }), date ? format(date, "PPP") : placeholder] }) }), _jsx(PopoverContent, { align: "start", className: "w-auto rounded-none p-0", children: _jsx(Calendar, { mode: "single", selected: date, defaultMonth: date, onSelect: (next) => {
54
- onChange(next);
68
+ return (_jsx(DatePickerShell, { id: id, disabled: disabled, ariaInvalid: ariaInvalid, className: className, empty: !date, label: date ? format(date, "PPP") : placeholder, open: open, onOpenChange: setOpen, side: side, children: _jsx(Calendar, { mode: "single", selected: date, defaultMonth: defaultMonth ?? date, numberOfMonths: numberOfMonths, onSelect: (next) => {
69
+ onChange(next);
70
+ setOpen(false);
71
+ } }) }));
72
+ }
73
+ function DatePickerRangeButton({ id, value, onChange, placeholder = "Pick a date range", disabled, className, "aria-invalid": ariaInvalid, numberOfMonths, side, defaultMonth, }) {
74
+ const [open, setOpen] = React.useState(false);
75
+ const mdUp = useMediaQuery(MD_MEDIA_QUERY);
76
+ const months = numberOfMonths ?? (mdUp ? 2 : 1);
77
+ const from = toDate(value?.from);
78
+ const to = toDate(value?.to);
79
+ const selected = from || to ? { from, to } : undefined;
80
+ return (_jsxs(DatePickerShell, { id: id, disabled: disabled, ariaInvalid: ariaInvalid, className: className, empty: !from, label: formatRangeTriggerLabel(from, to, placeholder), open: open, onOpenChange: setOpen, side: side, children: [_jsx(Calendar, { mode: "range", selected: selected, defaultMonth: defaultMonth ?? from ?? to, numberOfMonths: months, onSelect: (next) => {
81
+ onChange(next);
82
+ // First click is a same-day range; keep the popover open so the
83
+ // user can set `to`. Close once the ends differ.
84
+ if (next?.from && next.to && !isSameDay(next.from, next.to)) {
85
+ setOpen(false);
86
+ }
87
+ } }), _jsx("div", { className: "flex justify-end border-t p-2", children: _jsx(Button, { type: "button", variant: "ghost", size: "sm", disabled: !from && !to, onClick: () => {
88
+ onChange(undefined);
55
89
  setOpen(false);
56
- } }) })] }));
90
+ }, children: "Clear" }) })] }));
91
+ }
92
+ /**
93
+ * Calendar popover trigger styled as a form control (same chrome as Input
94
+ * and SelectTrigger). `mode="single"` (default) accepts `Date` or
95
+ * `yyyy-MM-dd` and emits `Date | undefined`. `mode="range"` accepts
96
+ * `{ from, to }` and emits `DateRange | undefined`. Range mode shows one
97
+ * month below `md` and two at `md+` unless `numberOfMonths` is set. The
98
+ * popover clamps to the viewport (`side` is overridable). Square corners
99
+ * follow the shared shape rules.
100
+ */
101
+ export function DatePickerButton(props) {
102
+ if (props.mode === "range") {
103
+ return _jsx(DatePickerRangeButton, { ...props });
104
+ }
105
+ return _jsx(DatePickerSingleButton, { ...props });
57
106
  }
@@ -7,7 +7,9 @@ type Props = {
7
7
  meta?: FieldMetaState;
8
8
  placeholder?: string;
9
9
  disabled?: boolean;
10
+ /** Forwarded to the date picker trigger. */
11
+ className?: string;
10
12
  };
11
13
  /** Date picker with built-in field error text and invalid styling. */
12
- export declare function FormDatePickerField({ value, onValueChange, label, meta, placeholder, disabled, }: Props): import("react").JSX.Element;
14
+ export declare function FormDatePickerField({ value, onValueChange, label, meta, placeholder, disabled, className, }: Props): import("react").JSX.Element;
13
15
  export {};
@@ -5,8 +5,8 @@ import { DatePickerButton } from "../date-picker.js";
5
5
  import { Label } from "../label.js";
6
6
  import { FieldError, getFieldErrorMessage, } from "./field-error.js";
7
7
  /** Date picker with built-in field error text and invalid styling. */
8
- export function FormDatePickerField({ value, onValueChange, label, meta, placeholder, disabled, }) {
8
+ export function FormDatePickerField({ value, onValueChange, label, meta, placeholder, disabled, className, }) {
9
9
  const errorText = meta ? getFieldErrorMessage(meta.errors[0]) : undefined;
10
10
  const id = useId();
11
- return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: id, children: label }), _jsx(DatePickerButton, { id: id, value: value, onChange: onValueChange, placeholder: placeholder, disabled: disabled, "aria-invalid": errorText ? true : undefined }), _jsx(FieldError, { text: errorText })] }));
11
+ return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: id, children: label }), _jsx(DatePickerButton, { id: id, value: value, onChange: onValueChange, placeholder: placeholder, disabled: disabled, className: className, "aria-invalid": errorText ? true : undefined }), _jsx(FieldError, { text: errorText })] }));
12
12
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Chrome that makes a Button-based picker trigger match Input / SelectTrigger:
3
+ * 40px height, px-3, body text (16px below md so iOS does not zoom), border-input,
4
+ * background, and shadow. Button outline defaults are text-xs semibold uppercase
5
+ * with px-6 and icon-indented pl-4/pr-4, which reads as a smaller, more indented
6
+ * control next to real form fields. Hover stays Input-like and cancels the
7
+ * outline Button's purple selected wash.
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/30 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,9 @@
1
+ /**
2
+ * Chrome that makes a Button-based picker trigger match Input / SelectTrigger:
3
+ * 40px height, px-3, body text (16px below md so iOS does not zoom), border-input,
4
+ * background, and shadow. Button outline defaults are text-xs semibold uppercase
5
+ * with px-6 and icon-indented pl-4/pr-4, which reads as a smaller, more indented
6
+ * control next to real form fields. Hover stays Input-like and cancels the
7
+ * outline Button's purple selected wash.
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/30 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,7 @@
1
+ /** Tailwind `md` breakpoint. Used for range calendars that stack on small screens. */
2
+ export declare const MD_MEDIA_QUERY = "(min-width: 768px)";
3
+ /**
4
+ * Subscribe to a CSS media query. `defaultMatches` covers SSR and environments
5
+ * without `matchMedia` (jsdom unless the test stubs it).
6
+ */
7
+ export declare function useMediaQuery(query: string, defaultMatches?: boolean): boolean;
@@ -0,0 +1,27 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ /** Tailwind `md` breakpoint. Used for range calendars that stack on small screens. */
4
+ export const MD_MEDIA_QUERY = "(min-width: 768px)";
5
+ /**
6
+ * Subscribe to a CSS media query. `defaultMatches` covers SSR and environments
7
+ * without `matchMedia` (jsdom unless the test stubs it).
8
+ */
9
+ export function useMediaQuery(query, defaultMatches = false) {
10
+ const [matches, setMatches] = React.useState(() => {
11
+ if (typeof window === "undefined" ||
12
+ typeof window.matchMedia !== "function") {
13
+ return defaultMatches;
14
+ }
15
+ return window.matchMedia(query).matches;
16
+ });
17
+ React.useEffect(() => {
18
+ if (typeof window.matchMedia !== "function")
19
+ return;
20
+ const media = window.matchMedia(query);
21
+ const onChange = () => setMatches(media.matches);
22
+ onChange();
23
+ media.addEventListener("change", onChange);
24
+ return () => media.removeEventListener("change", onChange);
25
+ }, [query]);
26
+ return matches;
27
+ }
package/dist/select.js CHANGED
@@ -14,7 +14,7 @@ function SelectValue({ ...props }) {
14
14
  return _jsx(SelectPrimitive.Value, { "data-slot": "select-value", ...props });
15
15
  }
16
16
  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-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none 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 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 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" }) })] }));
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/30 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
18
  }
19
19
  function SelectContent({ className, children, position = "popper", align = "center", sideOffset = 4, collisionPadding = 8, ...props }) {
20
20
  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" &&
package/dist/toggle.js CHANGED
@@ -4,11 +4,11 @@ import * as React from "react";
4
4
  import { cva } from "class-variance-authority";
5
5
  import { Toggle as TogglePrimitive } from "radix-ui";
6
6
  import { cn } from "./utils.js";
7
- 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:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/30 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-selected aria-pressed:text-selected-foreground dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", {
7
+ 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-primary hover:bg-selected hover:text-primary focus-visible:border-primary focus-visible:text-primary focus-visible:ring-[3px] focus-visible:ring-primary/30 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-selected aria-pressed:text-selected-foreground dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", {
8
8
  variants: {
9
9
  variant: {
10
10
  default: "bg-transparent",
11
- outline: "border border-input bg-transparent hover:bg-muted",
11
+ outline: "border border-input bg-transparent",
12
12
  },
13
13
  size: {
14
14
  default: "h-10 min-w-10 px-6 has-data-[icon=inline-end]:pr-4 has-data-[icon=inline-start]:pl-4",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas-react-kit",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Shared React components and Tailwind theme for Atlas",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/theme.css CHANGED
@@ -182,3 +182,24 @@
182
182
  @apply font-sans;
183
183
  }
184
184
  }
185
+
186
+ /*
187
+ * Card padding in this layer so utility `px-0` / `py-0` override `--card-p`
188
+ * without `!`. Size and flush still set the variable on `[data-slot=card]`.
189
+ */
190
+ @layer components {
191
+ [data-slot="card"] {
192
+ padding-block: var(--card-p);
193
+ }
194
+ [data-slot="card-header"],
195
+ [data-slot="card-content"],
196
+ [data-slot="card-footer"] {
197
+ padding-inline: var(--card-p);
198
+ }
199
+ [data-slot="card-header"].border-b {
200
+ padding-bottom: var(--card-p);
201
+ }
202
+ [data-slot="card-footer"].border-t {
203
+ padding-top: var(--card-p);
204
+ }
205
+ }