atlas-react-kit 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/COMPONENTS.md +22 -26
  2. package/DESIGN_RULES.md +14 -40
  3. package/README.md +7 -6
  4. package/dist/accordion.js +2 -2
  5. package/dist/alert.js +6 -5
  6. package/dist/badge.js +5 -2
  7. package/dist/breadcrumb.js +2 -2
  8. package/dist/button.js +7 -4
  9. package/dist/calendar.js +5 -4
  10. package/dist/combobox.js +7 -13
  11. package/dist/date-time-picker.js +2 -1
  12. package/dist/dropdown-menu.js +4 -4
  13. package/dist/field.d.ts +6 -3
  14. package/dist/field.js +3 -2
  15. package/dist/form/checkbox-field.d.ts +1 -1
  16. package/dist/form/checkbox-field.js +8 -11
  17. package/dist/form/combobox-field.js +2 -2
  18. package/dist/form/control-field.js +2 -7
  19. package/dist/form/date-picker-field.js +5 -2
  20. package/dist/form/date-time-picker-field.js +5 -2
  21. package/dist/form/field-error.d.ts +3 -3
  22. package/dist/form/field-shell.d.ts +36 -31
  23. package/dist/form/field-shell.js +41 -34
  24. package/dist/form/markdown-field.js +2 -2
  25. package/dist/form/select-field.js +2 -2
  26. package/dist/form/text-field.js +2 -4
  27. package/dist/form/toggle-group-field.js +2 -2
  28. package/dist/info-tip.d.ts +5 -4
  29. package/dist/info-tip.js +15 -14
  30. package/dist/internal/active-link.d.ts +7 -0
  31. package/dist/internal/active-link.js +9 -0
  32. package/dist/internal/calendar-size.d.ts +5 -0
  33. package/dist/internal/calendar-size.js +5 -0
  34. package/dist/internal/form-control.d.ts +1 -1
  35. package/dist/internal/form-control.js +2 -1
  36. package/dist/internal/listbox-navigation.d.ts +0 -5
  37. package/dist/internal/listbox-navigation.js +0 -7
  38. package/dist/internal/listbox-option.d.ts +16 -3
  39. package/dist/internal/listbox-option.js +22 -5
  40. package/dist/internal/overlay-styles.d.ts +10 -7
  41. package/dist/internal/overlay-styles.js +10 -7
  42. package/dist/internal/search.d.ts +9 -1
  43. package/dist/internal/search.js +14 -5
  44. package/dist/internal/styles.d.ts +43 -0
  45. package/dist/internal/styles.js +43 -0
  46. package/dist/internal/tabs-styles.d.ts +8 -7
  47. package/dist/internal/tabs-styles.js +15 -13
  48. package/dist/internal/use-media-query.d.ts +5 -0
  49. package/dist/internal/use-media-query.js +5 -0
  50. package/dist/kanban.d.ts +1 -1
  51. package/dist/kanban.js +6 -4
  52. package/dist/label.js +2 -1
  53. package/dist/lazy-calendar.js +3 -2
  54. package/dist/nav-link.js +2 -1
  55. package/dist/popover.js +4 -3
  56. package/dist/search-picker.d.ts +1 -1
  57. package/dist/search-picker.js +7 -12
  58. package/dist/select.js +6 -6
  59. package/dist/skip-link.js +2 -1
  60. package/dist/tabs.d.ts +3 -3
  61. package/dist/tabs.js +7 -6
  62. package/dist/toast.js +11 -10
  63. package/dist/toggle.d.ts +2 -0
  64. package/dist/toggle.js +8 -3
  65. package/dist/tooltip.js +2 -2
  66. package/package.json +1 -1
  67. package/skills/atlas-react-kit/SKILL.md +46 -42
@@ -0,0 +1,7 @@
1
+ /** How NavLink and TabsNavLink mark the current page (`selectedState.active`). */
2
+ export declare function activeLinkAttributes(active: boolean): {
3
+ "data-state": string;
4
+ "aria-current": "page" | undefined;
5
+ };
6
+ /** A link marked by `activeLinkAttributes`, or an active Radix tab. */
7
+ export declare const activeLinkSelector = "[aria-current=\"page\"], [data-state=\"active\"]";
@@ -0,0 +1,9 @@
1
+ /** How NavLink and TabsNavLink mark the current page (`selectedState.active`). */
2
+ export function activeLinkAttributes(active) {
3
+ return {
4
+ "data-state": active ? "active" : "inactive",
5
+ "aria-current": active ? "page" : undefined,
6
+ };
7
+ }
8
+ /** A link marked by `activeLinkAttributes`, or an active Radix tab. */
9
+ export const activeLinkSelector = '[aria-current="page"], [data-state="active"]';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Calendar cell size and padding, shared with LazyCalendar's placeholder
3
+ * (here, not in calendar.tsx, so the placeholder skips Day Picker).
4
+ */
5
+ export declare const calendarSize = "[--cell-size:--spacing(8)] [--calendar-padding:--spacing(3)]";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Calendar cell size and padding, shared with LazyCalendar's placeholder
3
+ * (here, not in calendar.tsx, so the placeholder skips Day Picker).
4
+ */
5
+ export const calendarSize = "[--cell-size:--spacing(8)] [--calendar-padding:--spacing(3)]";
@@ -12,7 +12,7 @@ export declare const formControlTriggerClassName = "h-10 w-full border-input bg-
12
12
  * with muted text while empty (`data-empty`). Intrinsic padding makes one
13
13
  * line 40px tall while longer values wrap and consumer height classes work.
14
14
  */
15
- export declare const pickerTriggerClassName = "h-10 w-full border-input bg-background px-3 font-normal text-base tracking-normal shadow-xs normal-case md:text-sm hover:border-input hover:bg-background hover:text-foreground focus-visible:text-foreground aria-expanded:border-input aria-expanded:bg-background aria-expanded:text-foreground has-data-[icon=inline-start]:pl-3 has-data-[icon=inline-end]:pr-3 dark:hover:bg-background dark:aria-expanded:bg-background h-auto justify-start py-[calc((--spacing(10)_-_1lh_-_2px)/2)] data-[empty=true]:text-muted-foreground";
15
+ export declare const pickerTriggerClassName = "h-10 w-full border-input bg-background px-3 font-normal text-base tracking-normal shadow-xs normal-case md:text-sm hover:border-input hover:bg-background hover:text-foreground focus-visible:text-foreground aria-expanded:border-input aria-expanded:bg-background aria-expanded:text-foreground has-data-[icon=inline-start]:pl-3 has-data-[icon=inline-end]:pr-3 dark:hover:bg-background dark:aria-expanded:bg-background h-auto justify-start py-[calc((--spacing(10)_-_1lh_-_2px)/2)] data-placeholder:text-muted-foreground data-[empty=true]:text-muted-foreground";
16
16
  /** Calendar popover body: fits the viewport height and scrolls inside it. */
17
17
  export declare const pickerContentClassName = "max-h-(--radix-popover-content-available-height) overflow-y-auto rounded-none p-0";
18
18
  /** Gutter used when a calendar popover collides with the viewport. */
@@ -1,3 +1,4 @@
1
+ import { emptyValueText } from "./styles.js";
1
2
  /**
2
3
  * Chrome that makes a Button-based picker trigger match Input / SelectTrigger:
3
4
  * 40px height, px-3, body text (16px below md so iOS does not zoom), border-input,
@@ -12,7 +13,7 @@ export const formControlTriggerClassName = "h-10 w-full border-input bg-backgrou
12
13
  * with muted text while empty (`data-empty`). Intrinsic padding makes one
13
14
  * line 40px tall while longer values wrap and consumer height classes work.
14
15
  */
15
- export const pickerTriggerClassName = `${formControlTriggerClassName} h-auto justify-start py-[calc((--spacing(10)_-_1lh_-_2px)/2)] data-[empty=true]:text-muted-foreground`;
16
+ export const pickerTriggerClassName = `${formControlTriggerClassName} h-auto justify-start py-[calc((--spacing(10)_-_1lh_-_2px)/2)] ${emptyValueText}`;
16
17
  /** Calendar popover body: fits the viewport height and scrolls inside it. */
17
18
  export const pickerContentClassName = "max-h-(--radix-popover-content-available-height) overflow-y-auto rounded-none p-0";
18
19
  /** Gutter used when a calendar popover collides with the viewport. */
@@ -28,8 +28,3 @@ export declare function handleListboxKey(event: {
28
28
  setHighlight: (index: number) => void;
29
29
  onEnter: (index: number) => void;
30
30
  }): boolean;
31
- /**
32
- * Case-fold and strip combining marks so "nino" matches "Niño". Shared by
33
- * Combobox and SearchPicker (which re-exports it).
34
- */
35
- export declare function foldSearchText(value: string): string;
@@ -66,10 +66,3 @@ export function handleListboxKey(event, { highlight, length, setHighlight, onEnt
66
66
  }
67
67
  return false;
68
68
  }
69
- /**
70
- * Case-fold and strip combining marks so "nino" matches "Niño". Shared by
71
- * Combobox and SearchPicker (which re-exports it).
72
- */
73
- export function foldSearchText(value) {
74
- return value.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
75
- }
@@ -1,4 +1,16 @@
1
1
  import * as React from "react";
2
+ /**
3
+ * ARIA for the search input of a listbox; the input sets `aria-controls`
4
+ * itself, where the linter can see it.
5
+ */
6
+ export declare function listboxInputAria(listId: string, activeIndex: number | undefined): {
7
+ "aria-autocomplete": "list";
8
+ "aria-activedescendant": string | undefined;
9
+ };
10
+ /** The scrolling listbox; sr-only while `ListboxEmptyStatus` shows. */
11
+ export declare function Listbox({ empty, className, ...props }: Omit<React.ComponentProps<"ul">, "role"> & {
12
+ empty: boolean;
13
+ }): React.JSX.Element;
2
14
  type PickerOptionProps<T> = {
3
15
  item: T;
4
16
  getLabel: (item: T) => string;
@@ -16,9 +28,10 @@ type PickerOptionProps<T> = {
16
28
  declare function PickerOptionInner<T>({ item, getLabel, onSelect, onHighlight, showCheck, ...props }: PickerOptionProps<T>): React.JSX.Element;
17
29
  export declare const PickerOption: typeof PickerOptionInner;
18
30
  /**
19
- * The "No results" status. It sits outside the listbox, which may own only
20
- * options, and stays mounted (sr-only when not empty) so screen readers
21
- * announce the empty state as the query changes.
31
+ * The "No results" status (`children`, default "No results"). It sits
32
+ * outside the listbox, which may own only options, and stays mounted
33
+ * (sr-only when not empty) so screen readers announce the empty state as
34
+ * the query changes.
22
35
  */
23
36
  export declare function ListboxEmptyStatus({ empty, className, children, ...props }: Omit<React.ComponentProps<"div">, "role"> & {
24
37
  empty: boolean;
@@ -4,7 +4,23 @@ import { CheckIcon } from "lucide-react";
4
4
  import { cn } from "../utils.js";
5
5
  import { listboxOptionId, scrollOptionIntoView } from "./listbox-navigation.js";
6
6
  import { optionRow } from "./overlay-styles.js";
7
- import { listboxEmpty, optionState } from "./styles.js";
7
+ import { listboxEmpty, listboxScroll, optionState } from "./styles.js";
8
+ /**
9
+ * ARIA for the search input of a listbox; the input sets `aria-controls`
10
+ * itself, where the linter can see it.
11
+ */
12
+ export function listboxInputAria(listId, activeIndex) {
13
+ return {
14
+ "aria-autocomplete": "list",
15
+ "aria-activedescendant": activeIndex === undefined
16
+ ? undefined
17
+ : listboxOptionId(listId, activeIndex),
18
+ };
19
+ }
20
+ /** The scrolling listbox; sr-only while `ListboxEmptyStatus` shows. */
21
+ export function Listbox({ empty, className, ...props }) {
22
+ return (_jsx("ul", { role: "listbox", className: empty ? "sr-only" : cn(listboxScroll, className), ...props }));
23
+ }
8
24
  /**
9
25
  * One option of a search-driven listbox (Combobox, SearchPicker): a
10
26
  * non-focusable button in a presentational `li`, highlighted by state and
@@ -20,10 +36,11 @@ function PickerOptionInner({ item, getLabel, onSelect, onHighlight, showCheck, .
20
36
  }
21
37
  export const PickerOption = React.memo(PickerOptionInner);
22
38
  /**
23
- * The "No results" status. It sits outside the listbox, which may own only
24
- * options, and stays mounted (sr-only when not empty) so screen readers
25
- * announce the empty state as the query changes.
39
+ * The "No results" status (`children`, default "No results"). It sits
40
+ * outside the listbox, which may own only options, and stays mounted
41
+ * (sr-only when not empty) so screen readers announce the empty state as
42
+ * the query changes.
26
43
  */
27
- export function ListboxEmptyStatus({ empty, className, children, ...props }) {
44
+ export function ListboxEmptyStatus({ empty, className, children = "No results", ...props }) {
28
45
  return (_jsx("div", { role: "status", className: empty ? cn(listboxEmpty, className) : "sr-only", ...props, children: empty ? children : null }));
29
46
  }
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Classes shared by the kit's floating surfaces (Popover, DropdownMenu,
3
- * Select, Dialog, Tooltip) and the option rows inside them. Components add
4
- * these with `cn()` instead of retyping them, so the panels and rows look
5
- * and move the same everywhere. Tailwind only generates class names it
6
- * finds as literals, so every class appears here in full.
2
+ * Classes and placement defaults shared by the kit's floating surfaces
3
+ * (Popover, DropdownMenu, Select, Dialog, Tooltip) and the option rows
4
+ * inside them. Components use these instead of retyping them, so the
5
+ * panels and rows look, sit, and move the same everywhere. Tailwind only
6
+ * generates class names it finds as literals, so every class appears here
7
+ * in full.
7
8
  */
8
9
  /** The panel: square corners, popover colors, shadow, and a hairline ring. */
9
10
  export declare const overlaySurface = "rounded-none bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10";
@@ -22,7 +23,9 @@ export declare const overlayFadeZoom = "duration-100 data-open:animate-in data-o
22
23
  * indicator, plus the highlight from `optionState`.
23
24
  */
24
25
  export declare const optionRow = "relative flex w-full cursor-default items-center gap-2.5 rounded-none py-2 pl-3 text-left text-sm outline-hidden select-none";
25
- /** Icons in a row: no pointer events, no shrinking, 14px unless sized. */
26
- export declare const optionIcon = "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5";
27
26
  /** Group label in a menu or listbox (DropdownMenuLabel, SelectLabel). */
28
27
  export declare const menuLabel = "px-3 py-2 text-xs font-semibold tracking-wider text-muted-foreground uppercase";
28
+ /** Default gap in px between a trigger and its panel (Radix `sideOffset`). */
29
+ export declare const OVERLAY_SIDE_OFFSET = 4;
30
+ /** Default px a panel keeps from the viewport edge (Radix `collisionPadding`). */
31
+ export declare const OVERLAY_COLLISION_PADDING = 8;
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Classes shared by the kit's floating surfaces (Popover, DropdownMenu,
3
- * Select, Dialog, Tooltip) and the option rows inside them. Components add
4
- * these with `cn()` instead of retyping them, so the panels and rows look
5
- * and move the same everywhere. Tailwind only generates class names it
6
- * finds as literals, so every class appears here in full.
2
+ * Classes and placement defaults shared by the kit's floating surfaces
3
+ * (Popover, DropdownMenu, Select, Dialog, Tooltip) and the option rows
4
+ * inside them. Components use these instead of retyping them, so the
5
+ * panels and rows look, sit, and move the same everywhere. Tailwind only
6
+ * generates class names it finds as literals, so every class appears here
7
+ * in full.
7
8
  */
8
9
  /** The panel: square corners, popover colors, shadow, and a hairline ring. */
9
10
  export const overlaySurface = "rounded-none bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10";
@@ -22,7 +23,9 @@ export const overlayFadeZoom = `${overlayFade} data-open:zoom-in-95 data-closed:
22
23
  * indicator, plus the highlight from `optionState`.
23
24
  */
24
25
  export const optionRow = "relative flex w-full cursor-default items-center gap-2.5 rounded-none py-2 pl-3 text-left text-sm outline-hidden select-none";
25
- /** Icons in a row: no pointer events, no shrinking, 14px unless sized. */
26
- export const optionIcon = "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5";
27
26
  /** Group label in a menu or listbox (DropdownMenuLabel, SelectLabel). */
28
27
  export const menuLabel = "px-3 py-2 text-xs font-semibold tracking-wider text-muted-foreground uppercase";
28
+ /** Default gap in px between a trigger and its panel (Radix `sideOffset`). */
29
+ export const OVERLAY_SIDE_OFFSET = 4;
30
+ /** Default px a panel keeps from the viewport edge (Radix `collisionPadding`). */
31
+ export const OVERLAY_COLLISION_PADDING = 8;
@@ -1,8 +1,16 @@
1
+ /**
2
+ * Case-fold and strip combining marks so "nino" matches "Niño". Shared by
3
+ * Combobox and SearchPicker (which re-exports it).
4
+ */
5
+ export declare function foldSearchText(value: string): string;
1
6
  /** Fold the query once; item labels are folded when their catalog changes. */
2
7
  export declare function normalizeSearchQuery(query: string): string;
3
- export declare function matchesSearchLabel(label: string, needle: string): boolean;
4
8
  export declare function createSearchIndex<T>(items: T[], getLabel: (item: T) => string): {
5
9
  item: T;
6
10
  label: string;
7
11
  }[];
12
+ /**
13
+ * The items whose folded label includes `needle` (a `normalizeSearchQuery`
14
+ * result), in order; every item when `needle` is empty.
15
+ */
8
16
  export declare function filterSearchIndex<T>(index: ReturnType<typeof createSearchIndex<T>>, needle: string): T[];
@@ -1,16 +1,25 @@
1
- import { foldSearchText } from "./listbox-navigation.js";
1
+ /**
2
+ * Case-fold and strip combining marks so "nino" matches "Niño". Shared by
3
+ * Combobox and SearchPicker (which re-exports it).
4
+ */
5
+ export function foldSearchText(value) {
6
+ return value.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
7
+ }
2
8
  /** Fold the query once; item labels are folded when their catalog changes. */
3
9
  export function normalizeSearchQuery(query) {
4
10
  return foldSearchText(query.trim());
5
11
  }
6
- export function matchesSearchLabel(label, needle) {
7
- return label.includes(needle);
8
- }
9
12
  export function createSearchIndex(items, getLabel) {
10
13
  return items.map((item) => ({ item, label: foldSearchText(getLabel(item)) }));
11
14
  }
15
+ /**
16
+ * The items whose folded label includes `needle` (a `normalizeSearchQuery`
17
+ * result), in order; every item when `needle` is empty.
18
+ */
12
19
  export function filterSearchIndex(index, needle) {
20
+ if (!needle)
21
+ return index.map(({ item }) => item);
13
22
  return index
14
- .filter(({ label }) => matchesSearchLabel(label, needle))
23
+ .filter(({ label }) => label.includes(needle))
15
24
  .map(({ item }) => item);
16
25
  }
@@ -8,8 +8,12 @@
8
8
  */
9
9
  /** Keyboard focus: a solid 2px ring separated from filled controls (WCAG 1.4.11). */
10
10
  export declare const focusRing = "outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background";
11
+ /** The same ring on a wrapper with keyboard focus inside it. */
12
+ export declare const focusRingWithin = "has-focus-visible:ring-2 has-focus-visible:ring-ring has-focus-visible:ring-offset-2 has-focus-visible:ring-offset-background";
11
13
  /** `aria-invalid`: destructive border and a soft glow; the error text carries the message. */
12
14
  export declare const invalidState = "aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40";
15
+ /** Muted trigger text while empty (`data-placeholder` or `data-empty`). */
16
+ export declare const emptyValueText = "data-placeholder:text-muted-foreground data-[empty=true]:text-muted-foreground";
13
17
  /** Native `disabled`: dimmed, with no hover or click. */
14
18
  export declare const disabledState = "disabled:pointer-events-none disabled:opacity-50";
15
19
  /** Radix `data-disabled` (menu and list items, slider). */
@@ -37,8 +41,23 @@ export declare const selectedState: {
37
41
  /** Quiet buttons and toggles on hover. */
38
42
  hover: string;
39
43
  };
44
+ /**
45
+ * Alert and Toast tones: a border, and `--status-tone` for the title and
46
+ * icon (`text-(--status-tone)`). `neutral` unsets it, so they inherit.
47
+ */
48
+ export declare const statusTone: {
49
+ neutral: string;
50
+ success: string;
51
+ warning: string;
52
+ destructive: string;
53
+ };
40
54
  /** The "No results" row of a listbox. */
41
55
  export declare const listboxEmpty = "px-3 py-4 text-center text-sm text-muted-foreground";
56
+ /**
57
+ * The scrolling frame of a listbox (Combobox, SearchPicker): about ten rows
58
+ * tall, then scrolls. A consumer `listClassName` replaces the height.
59
+ */
60
+ export declare const listboxScroll = "max-h-60 overflow-y-auto";
42
61
  /** Options in menus and listboxes. */
43
62
  export declare const optionState: {
44
63
  /** Highlight as focus moves: grey fill plus a 2px ring bar (the fill alone is 1.1:1). */
@@ -72,6 +91,30 @@ export declare const controlSize: {
72
91
  sm: string;
73
92
  lg: string;
74
93
  };
94
+ /**
95
+ * Label text of a control (Button, Toggle, Badge): small, bold, wide-tracked
96
+ * uppercase on one line. Badge adds its own `text-[0.625rem]` after it;
97
+ * tailwind-merge then drops the `text-xs`.
98
+ */
99
+ export declare const controlText = "text-xs font-semibold tracking-widest whitespace-nowrap uppercase";
100
+ /** Field-label text (Label, FieldLegend, FieldTitle, PopoverTitle). */
101
+ export declare const labelText = "text-xs font-semibold tracking-wide uppercase";
102
+ /**
103
+ * Icons inside a control or row: no pointer events, no shrinking, 14px
104
+ * unless the icon sets its own `size-*`. `sm` lowers that default to 12px
105
+ * for the xs controls; it goes after `default`, and tailwind-merge keeps
106
+ * the later size.
107
+ */
108
+ export declare const inlineIcon: {
109
+ default: string;
110
+ sm: string;
111
+ };
112
+ /**
113
+ * Brand-colored text on hover and keyboard focus for quiet links and
114
+ * triggers (BreadcrumbLink, AccordionTrigger, InfoTip). DESIGN_RULES.md,
115
+ * "Tokens": brand text is `text-selected-foreground`, never `text-primary`.
116
+ */
117
+ export declare const quietTextHover = "hover:text-selected-foreground focus-visible:text-selected-foreground";
75
118
  /**
76
119
  * Uppercase muted label used on tiles, legends, and thin section chrome.
77
120
  * Re-exported publicly from `atlas-react-kit/stat-tile`.
@@ -8,8 +8,12 @@
8
8
  */
9
9
  /** Keyboard focus: a solid 2px ring separated from filled controls (WCAG 1.4.11). */
10
10
  export const focusRing = "outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background";
11
+ /** The same ring on a wrapper with keyboard focus inside it. */
12
+ export const focusRingWithin = "has-focus-visible:ring-2 has-focus-visible:ring-ring has-focus-visible:ring-offset-2 has-focus-visible:ring-offset-background";
11
13
  /** `aria-invalid`: destructive border and a soft glow; the error text carries the message. */
12
14
  export const invalidState = "aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40";
15
+ /** Muted trigger text while empty (`data-placeholder` or `data-empty`). */
16
+ export const emptyValueText = "data-placeholder:text-muted-foreground data-[empty=true]:text-muted-foreground";
13
17
  /** Native `disabled`: dimmed, with no hover or click. */
14
18
  export const disabledState = "disabled:pointer-events-none disabled:opacity-50";
15
19
  /** Radix `data-disabled` (menu and list items, slider). */
@@ -37,8 +41,23 @@ export const selectedState = {
37
41
  /** Quiet buttons and toggles on hover. */
38
42
  hover: "hover:border-selected-foreground hover:bg-selected hover:text-selected-foreground",
39
43
  };
44
+ /**
45
+ * Alert and Toast tones: a border, and `--status-tone` for the title and
46
+ * icon (`text-(--status-tone)`). `neutral` unsets it, so they inherit.
47
+ */
48
+ export const statusTone = {
49
+ neutral: "[--status-tone:initial]",
50
+ success: "border-success/40 [--status-tone:var(--color-success)]",
51
+ warning: "border-warning/40 [--status-tone:var(--color-warning)]",
52
+ destructive: "border-destructive/40 [--status-tone:var(--color-destructive)]",
53
+ };
40
54
  /** The "No results" row of a listbox. */
41
55
  export const listboxEmpty = "px-3 py-4 text-center text-sm text-muted-foreground";
56
+ /**
57
+ * The scrolling frame of a listbox (Combobox, SearchPicker): about ten rows
58
+ * tall, then scrolls. A consumer `listClassName` replaces the height.
59
+ */
60
+ export const listboxScroll = "max-h-60 overflow-y-auto";
42
61
  /** Options in menus and listboxes. */
43
62
  export const optionState = {
44
63
  /** Highlight as focus moves: grey fill plus a 2px ring bar (the fill alone is 1.1:1). */
@@ -72,6 +91,30 @@ export const controlSize = {
72
91
  sm: "h-9 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
73
92
  lg: "h-11 px-8 has-data-[icon=inline-end]:pr-5 has-data-[icon=inline-start]:pl-5",
74
93
  };
94
+ /**
95
+ * Label text of a control (Button, Toggle, Badge): small, bold, wide-tracked
96
+ * uppercase on one line. Badge adds its own `text-[0.625rem]` after it;
97
+ * tailwind-merge then drops the `text-xs`.
98
+ */
99
+ export const controlText = "text-xs font-semibold tracking-widest whitespace-nowrap uppercase";
100
+ /** Field-label text (Label, FieldLegend, FieldTitle, PopoverTitle). */
101
+ export const labelText = "text-xs font-semibold tracking-wide uppercase";
102
+ /**
103
+ * Icons inside a control or row: no pointer events, no shrinking, 14px
104
+ * unless the icon sets its own `size-*`. `sm` lowers that default to 12px
105
+ * for the xs controls; it goes after `default`, and tailwind-merge keeps
106
+ * the later size.
107
+ */
108
+ export const inlineIcon = {
109
+ default: "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
110
+ sm: "[&_svg:not([class*='size-'])]:size-3",
111
+ };
112
+ /**
113
+ * Brand-colored text on hover and keyboard focus for quiet links and
114
+ * triggers (BreadcrumbLink, AccordionTrigger, InfoTip). DESIGN_RULES.md,
115
+ * "Tokens": brand text is `text-selected-foreground`, never `text-primary`.
116
+ */
117
+ export const quietTextHover = "hover:text-selected-foreground focus-visible:text-selected-foreground";
75
118
  /**
76
119
  * Uppercase muted label used on tiles, legends, and thin section chrome.
77
120
  * Re-exported publicly from `atlas-react-kit/stat-tile`.
@@ -10,13 +10,14 @@ declare const tabsListVariants: (props?: ({
10
10
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
11
11
  /**
12
12
  * Horizontal scroller around a tab row. `p-1` leaves room for the active
13
- * border and focus ring; the height is the `h-11` list plus that padding
14
- * (`h-13` = 44px + 8px) so the list fits the content box. Putting padding
15
- * on an overflow box without this height makes `scrollHeight` 4px taller
16
- * than `clientHeight` (48 vs 44) and a vertical scrollbar appears if
17
- * `overflow-y` is not hidden. Line variant adds `pb-2` for the underline
18
- * (`h-14` = 44px + 4px + 8px). A block, so it fills its parent without
19
- * `w-full`. `theme.css` hides the scrollbar and draws the edge fades.
13
+ * border and focus ring; the height is the `h-7` list plus that padding
14
+ * (`h-9` = 28px + 8px) so the list fits the content box and the outer
15
+ * row matches `Button` `size="sm"`. Putting padding on an overflow box
16
+ * without this height makes `scrollHeight` taller than `clientHeight`
17
+ * and a vertical scrollbar appears if `overflow-y` is not hidden. Line
18
+ * variant adds `pb-2` for the underline (`h-10` = 28px + 4px + 8px). A
19
+ * block, so it fills its parent without `w-full`. `theme.css` hides the
20
+ * scrollbar and draws the edge fades.
20
21
  */
21
22
  declare const tabsScrollerVariants: (props?: ({
22
23
  variant?: "default" | "line" | null | undefined;
@@ -1,5 +1,5 @@
1
1
  import { cva } from "class-variance-authority";
2
- import { disabledState, focusRing, selectedState } from "./styles.js";
2
+ import { disabledState, focusRing, inlineIcon, selectedState, } from "./styles.js";
3
3
  /**
4
4
  * Tab row. `TabsList` and `TabsNav` pass `orientation`, which picks plain
5
5
  * size classes so a consumer `className` (`h-9`) replaces them. Called
@@ -13,8 +13,8 @@ const tabsListVariants = cva("group/tabs-list inline-flex w-max min-w-full items
13
13
  line: "gap-1 bg-transparent",
14
14
  },
15
15
  orientation: {
16
- auto: 'group-data-[orientation="horizontal"]/tabs:h-11 group-data-[orientation="vertical"]/tabs:h-fit group-data-[orientation="vertical"]/tabs:flex-col',
17
- horizontal: "h-11",
16
+ auto: 'group-data-[orientation="horizontal"]/tabs:h-7 group-data-[orientation="vertical"]/tabs:h-fit group-data-[orientation="vertical"]/tabs:flex-col',
17
+ horizontal: "h-7",
18
18
  vertical: "h-fit flex-col",
19
19
  },
20
20
  },
@@ -25,13 +25,14 @@ const tabsListVariants = cva("group/tabs-list inline-flex w-max min-w-full items
25
25
  });
26
26
  /**
27
27
  * Horizontal scroller around a tab row. `p-1` leaves room for the active
28
- * border and focus ring; the height is the `h-11` list plus that padding
29
- * (`h-13` = 44px + 8px) so the list fits the content box. Putting padding
30
- * on an overflow box without this height makes `scrollHeight` 4px taller
31
- * than `clientHeight` (48 vs 44) and a vertical scrollbar appears if
32
- * `overflow-y` is not hidden. Line variant adds `pb-2` for the underline
33
- * (`h-14` = 44px + 4px + 8px). A block, so it fills its parent without
34
- * `w-full`. `theme.css` hides the scrollbar and draws the edge fades.
28
+ * border and focus ring; the height is the `h-7` list plus that padding
29
+ * (`h-9` = 28px + 8px) so the list fits the content box and the outer
30
+ * row matches `Button` `size="sm"`. Putting padding on an overflow box
31
+ * without this height makes `scrollHeight` taller than `clientHeight`
32
+ * and a vertical scrollbar appears if `overflow-y` is not hidden. Line
33
+ * variant adds `pb-2` for the underline (`h-10` = 28px + 4px + 8px). A
34
+ * block, so it fills its parent without `w-full`. `theme.css` hides the
35
+ * scrollbar and draws the edge fades.
35
36
  */
36
37
  const tabsScrollerVariants = cva("relative box-border min-w-0 max-w-full overflow-x-auto overflow-y-hidden overscroll-x-contain p-1", {
37
38
  variants: {
@@ -45,8 +46,8 @@ const tabsScrollerVariants = cva("relative box-border min-w-0 max-w-full overflo
45
46
  },
46
47
  },
47
48
  compoundVariants: [
48
- { variant: "default", orientation: "horizontal", className: "h-13" },
49
- { variant: "line", orientation: "horizontal", className: "h-14" },
49
+ { variant: "default", orientation: "horizontal", className: "h-9" },
50
+ { variant: "line", orientation: "horizontal", className: "h-10" },
50
51
  ],
51
52
  defaultVariants: {
52
53
  variant: "default",
@@ -59,7 +60,8 @@ const tabsTriggerVariants = cva([
59
60
  // flex-none + px-4: every trigger and TabsNav link keeps the same
60
61
  // horizontal padding. flex-1 used to grow short labels and make the
61
62
  // longest (e.g. Apple Reminders) look tighter. No last-child padding.
62
- 'relative inline-flex h-full min-w-max flex-none items-center justify-center gap-2 border border-transparent px-4 py-1.5 text-xs font-semibold tracking-wider whitespace-nowrap text-foreground/60 uppercase transition-all group-data-[orientation="vertical"]/tabs:w-full group-data-[orientation="vertical"]/tabs:justify-start group-data-[orientation="vertical"]/tabs:px-4 group-data-[orientation="vertical"]/tabs:py-2 hover:text-foreground has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-3.5',
63
+ 'relative inline-flex h-full min-w-max flex-none items-center justify-center gap-2 border border-transparent px-4 py-1.5 text-xs font-semibold tracking-wider whitespace-nowrap text-foreground/60 uppercase transition-all group-data-[orientation="vertical"]/tabs:w-full group-data-[orientation="vertical"]/tabs:justify-start group-data-[orientation="vertical"]/tabs:px-4 group-data-[orientation="vertical"]/tabs:py-2 hover:text-foreground has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5 dark:text-muted-foreground dark:hover:text-foreground',
64
+ inlineIcon.default,
63
65
  'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state="active"]:border-transparent group-data-[variant=line]/tabs-list:data-[state="active"]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state="active"]:bg-transparent',
64
66
  selectedState.active,
65
67
  'after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation="horizontal"]/tabs:after:inset-x-0 group-data-[orientation="horizontal"]/tabs:after:bottom-[-5px] group-data-[orientation="horizontal"]/tabs:after:h-0.5 group-data-[orientation="vertical"]/tabs:after:inset-y-0 group-data-[orientation="vertical"]/tabs:after:-right-1 group-data-[orientation="vertical"]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state="active"]:after:opacity-100',
@@ -1,5 +1,10 @@
1
1
  /** Tailwind `md` breakpoint. Used for range calendars that stack on small screens. */
2
2
  export declare const MD_MEDIA_QUERY = "(min-width: 768px)";
3
+ /**
4
+ * A pointer that can hover (a mouse or trackpad). Touch and coarse pointers
5
+ * must use click; a hover-open popover would trap them after the first tap.
6
+ */
7
+ export declare const HOVER_MEDIA_QUERY = "(hover: hover) and (pointer: fine)";
3
8
  /**
4
9
  * Subscribe to a CSS media query with `useSyncExternalStore`. The server
5
10
  * render and hydration use `defaultMatches`, so the markup always agrees;
@@ -2,6 +2,11 @@
2
2
  import * as React from "react";
3
3
  /** Tailwind `md` breakpoint. Used for range calendars that stack on small screens. */
4
4
  export const MD_MEDIA_QUERY = "(min-width: 768px)";
5
+ /**
6
+ * A pointer that can hover (a mouse or trackpad). Touch and coarse pointers
7
+ * must use click; a hover-open popover would trap them after the first tap.
8
+ */
9
+ export const HOVER_MEDIA_QUERY = "(hover: hover) and (pointer: fine)";
5
10
  function hasMatchMedia() {
6
11
  return (typeof window !== "undefined" && typeof window.matchMedia === "function");
7
12
  }
package/dist/kanban.d.ts CHANGED
@@ -59,7 +59,7 @@ export type KanbanBoardProps<T extends {
59
59
  deleteLabel?: string;
60
60
  /** Classes for the horizontal scroller that holds the columns. */
61
61
  scrollerClassName?: string;
62
- /** Classes for every column, e.g. its width. */
62
+ /** Classes for every column, e.g. to pin a width instead of growing. */
63
63
  columnClassName?: string;
64
64
  /** Classes for every card, including the one under the pointer while dragging. */
65
65
  cardClassName?: string;
package/dist/kanban.js CHANGED
@@ -7,13 +7,15 @@ import { CheckCircle2, ChevronLeft, ChevronRight, GripVertical, Trash2, } from "
7
7
  import { Button } from "./button.js";
8
8
  import { KANBAN_COMPLETE_ID, KANBAN_DELETE_ID, buildKanbanGroups, kanbanOverflowEdges, kanbanScrollBy, resolveKanbanDrop, } from "./internal/kanban-layout.js";
9
9
  import { configureCardSortablePlugins, configureKanbanPlugins, } from "./internal/kanban-plugins.js";
10
+ import { focusRingWithin } from "./internal/styles.js";
10
11
  import { Skeleton } from "./skeleton.js";
11
12
  import { cn } from "./utils.js";
12
13
  /** Column droppables lose to cards, so a drop lands between cards. */
13
14
  const COLUMN_DROP_PRIORITY = 1;
14
15
  /** Rail zones beat columns when both are under the pointer. */
15
16
  const ACTION_ZONE_PRIORITY = 10;
16
- const columnSize = "w-72 min-w-72 min-h-[min(70vh,32rem)]";
17
+ /** Grow to fill the board; 18rem floor so a long row still scrolls. */
18
+ const columnSize = "flex-1 basis-72 min-w-72 min-h-[min(70vh,32rem)]";
17
19
  /**
18
20
  * Drag-and-drop board built on dnd-kit. The consumer owns `items` and maps
19
21
  * each to a column; the board derives its layout from them, so an optimistic
@@ -123,7 +125,7 @@ function KanbanColumnView({ column, count, emptyLabel, loading, className, child
123
125
  collisionPriority: COLUMN_DROP_PRIORITY,
124
126
  });
125
127
  const empty = count === 0;
126
- return (_jsxs("div", { ref: ref, "data-slot": "kanban-column", "data-drop-target": isDropTarget || undefined, className: cn("flex shrink-0 snap-start flex-col gap-3 p-3", "bg-muted/40 ring-1 ring-foreground/5", "data-drop-target:bg-muted/60 data-drop-target:ring-2 data-drop-target:ring-ring/60", columnSize, className), children: [_jsxs("header", { "data-slot": "kanban-column-header", className: "flex items-start justify-between gap-2", children: [_jsxs("span", { className: "flex items-center gap-2 text-xs font-semibold tracking-widest uppercase", children: [column.color ? (_jsx("span", { "aria-hidden": true, className: "size-2 shrink-0 rounded-full", style: { backgroundColor: column.color } })) : null, _jsx("span", { className: "leading-tight", children: column.label })] }), _jsxs("span", { className: "flex shrink-0 flex-col items-end gap-0.5 text-muted-foreground tabular-nums", children: [_jsx("span", { className: "text-xs", children: count }), column.badge ? (_jsx("span", { className: "text-[10px] font-medium", children: column.badge })) : null] })] }), _jsx("div", { className: "flex min-h-32 flex-1 flex-col gap-2 overflow-y-auto pr-1", children: loading ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { className: "h-20" }), _jsx(Skeleton, { className: "h-16" })] })) : (_jsxs(_Fragment, { children: [children, _jsx("p", { "data-slot": "kanban-column-empty", "aria-hidden": !empty, className: cn("border border-dashed px-3 py-6 text-center text-xs text-muted-foreground", !empty && "hidden"), children: emptyLabel })] })) })] }));
128
+ return (_jsxs("div", { ref: ref, "data-slot": "kanban-column", "data-drop-target": isDropTarget || undefined, className: cn("flex snap-start flex-col gap-3 p-3", "bg-muted/40 ring-1 ring-foreground/5", "data-drop-target:bg-muted/60 data-drop-target:ring-2 data-drop-target:ring-ring/60", columnSize, className), children: [_jsxs("header", { "data-slot": "kanban-column-header", className: "flex items-start justify-between gap-2", children: [_jsxs("span", { className: "flex items-center gap-2 text-xs font-semibold tracking-widest uppercase", children: [column.color ? (_jsx("span", { "aria-hidden": true, className: "size-2 shrink-0 rounded-full", style: { backgroundColor: column.color } })) : null, _jsx("span", { className: "leading-tight", children: column.label })] }), _jsxs("span", { className: "flex shrink-0 flex-col items-end gap-0.5 text-muted-foreground tabular-nums", children: [_jsx("span", { className: "text-xs", children: count }), column.badge ? (_jsx("span", { className: "text-[10px] font-medium", children: column.badge })) : null] })] }), _jsx("div", { className: "flex min-h-32 flex-1 flex-col gap-2 overflow-y-auto pr-1", children: loading ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { className: "h-20" }), _jsx(Skeleton, { className: "h-16" })] })) : (_jsxs(_Fragment, { children: [children, _jsx("p", { "data-slot": "kanban-column-empty", "aria-hidden": !empty, className: cn("border border-dashed px-3 py-6 text-center text-xs text-muted-foreground", !empty && "hidden"), children: emptyLabel })] })) })] }));
127
129
  }
128
130
  const cardChrome = cn("flex border border-border bg-card text-card-foreground");
129
131
  const handleChrome = cn("flex shrink-0 items-center self-stretch border-r border-border", "bg-muted/30 px-1.5 text-muted-foreground");
@@ -145,7 +147,7 @@ function KanbanCardViewImpl({ item, index, column, onCardClick, className, rende
145
147
  // The overlay moves; the source keeps its slot.
146
148
  "data-dragging:pointer-events-none data-dragging:invisible",
147
149
  // One ring on the card for the grip and the body alike.
148
- "has-focus-visible:ring-2 has-focus-visible:ring-ring has-focus-visible:ring-offset-2 has-focus-visible:ring-offset-background", onCardClick && "hover:border-foreground/15", className), children: [_jsx("button", { type: "button", ref: handleRef, "data-slot": "kanban-card-handle", "aria-label": "Drag to move", onClick: stopClick, className: cn(handleChrome, "cursor-grab outline-none hover:bg-muted/50 active:cursor-grabbing"), children: _jsx(GripVertical, { className: "size-4", "aria-hidden": true }) }), onCardClick ? (_jsx("button", { type: "button", "data-slot": "kanban-card-body", onClick: () => onCardClick(item), className: "min-w-0 flex-1 cursor-pointer p-3 text-left outline-none hover:bg-muted/20", children: children })) : (_jsx("div", { "data-slot": "kanban-card-body", className: "min-w-0 flex-1 p-3", children: children }))] }));
150
+ focusRingWithin, onCardClick && "hover:border-foreground/15", className), children: [_jsx("button", { type: "button", ref: handleRef, "data-slot": "kanban-card-handle", "aria-label": "Drag to move", onClick: stopClick, className: cn(handleChrome, "cursor-grab outline-none hover:bg-muted/50 active:cursor-grabbing"), children: _jsx(GripVertical, { className: "size-4", "aria-hidden": true }) }), onCardClick ? (_jsx("button", { type: "button", "data-slot": "kanban-card-body", onClick: () => onCardClick(item), className: "min-w-0 flex-1 cursor-pointer p-3 text-left outline-none hover:bg-muted/20", children: children })) : (_jsx("div", { "data-slot": "kanban-card-body", className: "min-w-0 flex-1 p-3", children: children }))] }));
149
151
  }
150
152
  /**
151
153
  * Memoized so a board update re-renders only the cards whose item, index or
@@ -192,6 +194,6 @@ function OverflowFade({ side, visible, }) {
192
194
  }
193
195
  /** Board-shaped placeholder for a route `loading` file. */
194
196
  function KanbanSkeleton({ columnCount = 4, className, columnClassName, ...props }) {
195
- return (_jsx("div", { "data-slot": "kanban-skeleton", className: cn("flex gap-4 overflow-hidden", className), ...props, children: Array.from({ length: columnCount }, (_, index) => (_jsx(Skeleton, { className: cn("shrink-0", columnSize, columnClassName) }, index))) }));
197
+ return (_jsx("div", { "data-slot": "kanban-skeleton", className: cn("flex gap-4 overflow-hidden", className), ...props, children: Array.from({ length: columnCount }, (_, index) => (_jsx(Skeleton, { className: cn(columnSize, columnClassName) }, index))) }));
196
198
  }
197
199
  export { KanbanBoard, KanbanSkeleton };
package/dist/label.js CHANGED
@@ -2,12 +2,13 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Label as LabelPrimitive } from "radix-ui";
5
+ import { labelText } from "./internal/styles.js";
5
6
  import { cn } from "./utils.js";
6
7
  /**
7
8
  * Form label. `required` adds a visible marker; style it with
8
9
  * `requiredClassName` or the `label-required` slot.
9
10
  */
10
11
  function Label({ className, required, requiredClassName, children, ...props }) {
11
- return (_jsxs(LabelPrimitive.Root, { "data-slot": "label", className: cn("flex items-center gap-2 text-xs font-semibold tracking-wide uppercase select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 peer-data-[slot=checkbox]:text-sm peer-data-[slot=checkbox]:font-normal peer-data-[slot=checkbox]:tracking-normal peer-data-[slot=checkbox]:normal-case peer-data-[slot=radio-group-item]:text-sm peer-data-[slot=radio-group-item]:font-normal peer-data-[slot=radio-group-item]:tracking-normal peer-data-[slot=radio-group-item]:normal-case peer-data-[slot=switch]:text-sm peer-data-[slot=switch]:font-normal peer-data-[slot=switch]:tracking-normal peer-data-[slot=switch]:normal-case", className), ...props, children: [children, required ? (_jsx("span", { "data-slot": "label-required", "aria-hidden": "true", className: cn("-ml-1.5 text-destructive", requiredClassName), children: "*" })) : null] }));
12
+ return (_jsxs(LabelPrimitive.Root, { "data-slot": "label", className: cn(labelText, "flex items-center gap-2 select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 peer-data-[slot=checkbox]:text-sm peer-data-[slot=checkbox]:font-normal peer-data-[slot=checkbox]:tracking-normal peer-data-[slot=checkbox]:normal-case peer-data-[slot=radio-group-item]:text-sm peer-data-[slot=radio-group-item]:font-normal peer-data-[slot=radio-group-item]:tracking-normal peer-data-[slot=radio-group-item]:normal-case peer-data-[slot=switch]:text-sm peer-data-[slot=switch]:font-normal peer-data-[slot=switch]:tracking-normal peer-data-[slot=switch]:normal-case", className), ...props, children: [children, required ? (_jsx("span", { "data-slot": "label-required", "aria-hidden": "true", className: cn("-ml-1.5 text-destructive", requiredClassName), children: "*" })) : null] }));
12
13
  }
13
14
  export { Label };
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
+ import { calendarSize } from "./internal/calendar-size.js";
4
5
  import { Spinner } from "./spinner.js";
5
6
  import { cn } from "./utils.js";
6
7
  /**
@@ -16,11 +17,11 @@ export function preloadCalendar() {
16
17
  const LazyCalendarInner = React.lazy(() => loadCalendar().then((module) => ({ default: module.Calendar })));
17
18
  /**
18
19
  * While loading, a placeholder the size of a one-month calendar, measured in
19
- * the calendar's own `--cell-size` (7 columns plus `p-3`; about 9 cells
20
+ * the calendar's own `calendarSize` (7 columns plus padding; about 9 cells
20
21
  * tall), so the popover does not jump. It takes the calendar's `className`,
21
22
  * so a `--cell-size` override sizes both.
22
23
  */
23
- const fallbackClassName = "flex h-[calc(var(--cell-size)*9)] w-[calc(var(--cell-size)*7+--spacing(6))] items-center justify-center [--cell-size:--spacing(8)]";
24
+ const fallbackClassName = cn("flex h-[calc(var(--cell-size)*9)] w-[calc(var(--cell-size)*7+var(--calendar-padding)*2)] items-center justify-center", calendarSize);
24
25
  export function LazyCalendar(props) {
25
26
  return (_jsx(React.Suspense, { fallback: _jsx("div", { className: cn(fallbackClassName, props.className), children: _jsx(Spinner, { "aria-label": "Loading calendar" }) }), children: _jsx(LazyCalendarInner, { ...props }) }));
26
27
  }