atlas-react-kit 0.3.0 → 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; 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 }`; optional `numberOfMonths` (range default 2) |
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
 
@@ -64,9 +64,17 @@ accept the same error meta shape. `FormDatePickerField` takes a `Date` or
64
64
 
65
65
  `DatePickerButton` is single-day by default. For a from/to range, set
66
66
  `mode="range"`; `value` is `{ from, to }` (each side `Date` or `yyyy-MM-dd`)
67
- and `onChange` receives `DateRange | undefined`. The popover shows two months
68
- unless `numberOfMonths` is set, stays `w-auto` so it does not collapse to the
69
- default popover width, and includes a Clear action.
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").
70
78
 
71
79
  ```tsx
72
80
  import { DatePickerButton, type DateRange } from "atlas-react-kit/date-picker"
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
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.3.0
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.2.1 is 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.3.0.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,6 +17,7 @@ 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.
21
22
  * Keyboard highlight (arrows, Home/End, PageUp/PageDown) scrolls the active
22
23
  * option into view inside the overflow list.
package/dist/combobox.js CHANGED
@@ -4,6 +4,7 @@ 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";
9
10
  /**
@@ -14,6 +15,7 @@ import { cn, onInputChange } from "./utils.js";
14
15
  const LISTBOX_PAGE_SIZE = 10;
15
16
  /**
16
17
  * Searchable single-select built from Button, Popover, and Input.
18
+ * The trigger uses the same form-control chrome as Input and SelectTrigger.
17
19
  * Options are a plain `{ value, label }` list; domain data stays in the app.
18
20
  * Keyboard highlight (arrows, Home/End, PageUp/PageDown) scrolls the active
19
21
  * option into view inside the overflow list.
@@ -100,7 +102,7 @@ export function Combobox({ options, value, onValueChange, placeholder = "Select"
100
102
  setHighlight((index) => Math.max(index - LISTBOX_PAGE_SIZE, 0));
101
103
  }
102
104
  };
103
- 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("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
104
106
  ? `${listId}-option-${activeOption.value}`
105
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) => {
106
108
  const isSelected = option.value === value;
@@ -1,5 +1,6 @@
1
1
  import * as React from "react";
2
2
  import type { DateRange } from "react-day-picker";
3
+ import { PopoverContent } from "./popover.js";
3
4
  export type { DateRange };
4
5
  export type DateLike = Date | string | number | null | undefined;
5
6
  /** From/to value for `DatePickerButton` in range mode; each side is `DateLike`. */
@@ -22,10 +23,13 @@ type DatePickerSharedProps = {
22
23
  className?: string;
23
24
  "aria-invalid"?: boolean;
24
25
  /**
25
- * Visible months in the popover calendar. Defaults to 1 in single mode
26
- * and 2 in range mode.
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.
27
29
  */
28
30
  numberOfMonths?: number;
31
+ /** Popover side. Defaults to `bottom`; Radix still flips on collision. */
32
+ side?: React.ComponentProps<typeof PopoverContent>["side"];
29
33
  /** Month shown when the popover opens; falls back to the selected date. */
30
34
  defaultMonth?: Date;
31
35
  };
@@ -41,9 +45,12 @@ export type DatePickerRangeProps = DatePickerSharedProps & {
41
45
  };
42
46
  export type DatePickerButtonProps = DatePickerSingleProps | DatePickerRangeProps;
43
47
  /**
44
- * Calendar popover trigger. `mode="single"` (default) accepts `Date` or
48
+ * Calendar popover trigger styled as a form control (same chrome as Input
49
+ * and SelectTrigger). `mode="single"` (default) accepts `Date` or
45
50
  * `yyyy-MM-dd` and emits `Date | undefined`. `mode="range"` accepts
46
- * `{ from, to }` and emits `DateRange | undefined`. Square corners follow
47
- * the shared shape rules.
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.
48
55
  */
49
56
  export declare function DatePickerButton(props: DatePickerButtonProps): React.JSX.Element;
@@ -5,6 +5,8 @@ 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
  /**
@@ -54,24 +56,28 @@ function formatRangeTriggerLabel(from, to, placeholder) {
54
56
  return format(from, "PPP");
55
57
  return `${format(from, "PPP")} – ${format(to, "PPP")}`;
56
58
  }
59
+ /** Gutter used when the calendar popover collides with the viewport. */
60
+ const DATE_PICKER_COLLISION_PADDING = 16;
57
61
  /** Shared trigger + auto-sized popover chrome for single and range modes. */
58
- function DatePickerShell({ id, disabled, ariaInvalid, className, empty, label, open, onOpenChange, children, }) {
59
- 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("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" }), label] }) }), _jsx(PopoverContent, { align: "start", className: "w-auto rounded-none p-0", children: children })] }));
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 })] }));
60
64
  }
61
- function DatePickerSingleButton({ id, value, onChange, placeholder = "Pick a date", disabled, className, "aria-invalid": ariaInvalid, numberOfMonths, defaultMonth, }) {
65
+ function DatePickerSingleButton({ id, value, onChange, placeholder = "Pick a date", disabled, className, "aria-invalid": ariaInvalid, numberOfMonths, side, defaultMonth, }) {
62
66
  const [open, setOpen] = React.useState(false);
63
67
  const date = toDate(value);
64
- return (_jsx(DatePickerShell, { id: id, disabled: disabled, ariaInvalid: ariaInvalid, className: className, empty: !date, label: date ? format(date, "PPP") : placeholder, open: open, onOpenChange: setOpen, children: _jsx(Calendar, { mode: "single", selected: date, defaultMonth: defaultMonth ?? date, numberOfMonths: numberOfMonths, onSelect: (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) => {
65
69
  onChange(next);
66
70
  setOpen(false);
67
71
  } }) }));
68
72
  }
69
- function DatePickerRangeButton({ id, value, onChange, placeholder = "Pick a date range", disabled, className, "aria-invalid": ariaInvalid, numberOfMonths = 2, defaultMonth, }) {
73
+ function DatePickerRangeButton({ id, value, onChange, placeholder = "Pick a date range", disabled, className, "aria-invalid": ariaInvalid, numberOfMonths, side, defaultMonth, }) {
70
74
  const [open, setOpen] = React.useState(false);
75
+ const mdUp = useMediaQuery(MD_MEDIA_QUERY);
76
+ const months = numberOfMonths ?? (mdUp ? 2 : 1);
71
77
  const from = toDate(value?.from);
72
78
  const to = toDate(value?.to);
73
79
  const selected = from || to ? { from, to } : undefined;
74
- return (_jsxs(DatePickerShell, { id: id, disabled: disabled, ariaInvalid: ariaInvalid, className: className, empty: !from, label: formatRangeTriggerLabel(from, to, placeholder), open: open, onOpenChange: setOpen, children: [_jsx(Calendar, { mode: "range", selected: selected, defaultMonth: defaultMonth ?? from ?? to, numberOfMonths: numberOfMonths, onSelect: (next) => {
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) => {
75
81
  onChange(next);
76
82
  // First click is a same-day range; keep the popover open so the
77
83
  // user can set `to`. Close once the ends differ.
@@ -84,10 +90,13 @@ function DatePickerRangeButton({ id, value, onChange, placeholder = "Pick a date
84
90
  }, children: "Clear" }) })] }));
85
91
  }
86
92
  /**
87
- * Calendar popover trigger. `mode="single"` (default) accepts `Date` or
93
+ * Calendar popover trigger styled as a form control (same chrome as Input
94
+ * and SelectTrigger). `mode="single"` (default) accepts `Date` or
88
95
  * `yyyy-MM-dd` and emits `Date | undefined`. `mode="range"` accepts
89
- * `{ from, to }` and emits `DateRange | undefined`. Square corners follow
90
- * the shared shape rules.
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.
91
100
  */
92
101
  export function DatePickerButton(props) {
93
102
  if (props.mode === "range") {
@@ -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.3.0",
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
+ }