atlas-react-kit 1.2.0 → 1.2.2

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 (72) hide show
  1. package/COMPONENTS.md +40 -40
  2. package/DESIGN_RULES.md +14 -40
  3. package/README.md +7 -6
  4. package/dist/alert.js +7 -6
  5. package/dist/calendar.js +5 -4
  6. package/dist/card.js +2 -1
  7. package/dist/combobox.d.ts +4 -3
  8. package/dist/combobox.js +8 -17
  9. package/dist/date-time-picker.js +2 -1
  10. package/dist/dialog.js +2 -1
  11. package/dist/dropdown-menu.js +7 -5
  12. package/dist/field.d.ts +6 -3
  13. package/dist/field.js +3 -2
  14. package/dist/form/checkbox-field.d.ts +3 -5
  15. package/dist/form/checkbox-field.js +13 -19
  16. package/dist/form/combobox-field.d.ts +2 -2
  17. package/dist/form/combobox-field.js +5 -6
  18. package/dist/form/control-field.d.ts +5 -5
  19. package/dist/form/control-field.js +6 -9
  20. package/dist/form/date-picker-field.d.ts +1 -1
  21. package/dist/form/date-picker-field.js +3 -7
  22. package/dist/form/date-time-picker-field.d.ts +1 -1
  23. package/dist/form/date-time-picker-field.js +3 -7
  24. package/dist/form/field-error.d.ts +13 -4
  25. package/dist/form/field-shell.d.ts +63 -47
  26. package/dist/form/field-shell.js +67 -35
  27. package/dist/form/focus-first-invalid.d.ts +7 -4
  28. package/dist/form/focus-first-invalid.js +10 -6
  29. package/dist/form/markdown-field.d.ts +1 -1
  30. package/dist/form/markdown-field.js +8 -4
  31. package/dist/form/select-field.d.ts +2 -8
  32. package/dist/form/select-field.js +5 -7
  33. package/dist/form/text-field.d.ts +2 -8
  34. package/dist/form/text-field.js +9 -6
  35. package/dist/form/toggle-group-field.d.ts +2 -2
  36. package/dist/form/toggle-group-field.js +5 -6
  37. package/dist/info-tip.d.ts +7 -4
  38. package/dist/info-tip.js +12 -4
  39. package/dist/internal/active-link.d.ts +7 -0
  40. package/dist/internal/active-link.js +9 -0
  41. package/dist/internal/calendar-size.d.ts +5 -0
  42. package/dist/internal/calendar-size.js +5 -0
  43. package/dist/internal/form-control.d.ts +7 -1
  44. package/dist/internal/form-control.js +8 -1
  45. package/dist/internal/kanban-layout.js +2 -1
  46. package/dist/internal/listbox-navigation.d.ts +8 -8
  47. package/dist/internal/listbox-navigation.js +23 -23
  48. package/dist/internal/listbox-option.d.ts +16 -3
  49. package/dist/internal/listbox-option.js +22 -5
  50. package/dist/internal/overlay-styles.d.ts +15 -5
  51. package/dist/internal/overlay-styles.js +15 -5
  52. package/dist/internal/search.d.ts +4 -0
  53. package/dist/internal/search.js +6 -0
  54. package/dist/internal/styles.d.ts +21 -0
  55. package/dist/internal/styles.js +21 -0
  56. package/dist/kanban.js +3 -2
  57. package/dist/label.js +2 -1
  58. package/dist/lazy-calendar.js +3 -2
  59. package/dist/nav-link.js +2 -1
  60. package/dist/picker-shell.d.ts +3 -1
  61. package/dist/picker-shell.js +3 -3
  62. package/dist/popover.js +6 -5
  63. package/dist/search-picker.js +8 -16
  64. package/dist/select.js +5 -5
  65. package/dist/skip-link.js +2 -1
  66. package/dist/tabs.js +3 -2
  67. package/dist/toast.js +11 -10
  68. package/dist/toggle-group.d.ts +1 -1
  69. package/dist/toggle-group.js +36 -2
  70. package/dist/tooltip.js +2 -2
  71. package/package.json +1 -1
  72. package/skills/atlas-react-kit/SKILL.md +46 -42
@@ -1,23 +1,21 @@
1
+ import { clamp } from "./math.js";
1
2
  /** Page keys move ten rows, a little more than one max-h-60 list viewport. */
2
3
  const PAGE_SIZE = 10;
4
+ /** Rows each key moves the highlight; Home and End jump to the ends. */
5
+ const keySteps = new Map([
6
+ ["ArrowDown", 1],
7
+ ["ArrowUp", -1],
8
+ ["PageDown", PAGE_SIZE],
9
+ ["PageUp", -PAGE_SIZE],
10
+ ["Home", -Infinity],
11
+ ["End", Infinity],
12
+ ]);
3
13
  /** Return the next highlighted index for a listbox navigation key. */
4
14
  function moveListboxHighlight(key, index, length) {
5
- switch (key) {
6
- case "ArrowDown":
7
- return length === 0 ? 0 : Math.min(index + 1, length - 1);
8
- case "ArrowUp":
9
- return Math.max(index - 1, 0);
10
- case "Home":
11
- return 0;
12
- case "End":
13
- return length === 0 ? 0 : length - 1;
14
- case "PageDown":
15
- return length === 0 ? 0 : Math.min(index + PAGE_SIZE, length - 1);
16
- case "PageUp":
17
- return Math.max(index - PAGE_SIZE, 0);
18
- default:
19
- return undefined;
20
- }
15
+ const step = keySteps.get(key);
16
+ if (step === undefined)
17
+ return undefined;
18
+ return clamp(index + step, 0, Math.max(length - 1, 0));
21
19
  }
22
20
  /**
23
21
  * Option id for `aria-activedescendant`. Index-based because option values
@@ -45,15 +43,15 @@ function isComposingKey(event) {
45
43
  }
46
44
  /**
47
45
  * Shared keydown for a search input that drives a listbox: navigation keys
48
- * move the highlight, Enter picks the highlighted index. Returns `true` when
49
- * the key is handled (or an IME is composing, when every key belongs to the
50
- * IME), so the caller can stop; `false` leaves the key to the caller (for
51
- * example SearchPicker's Escape).
46
+ * move the highlight over `items`, Enter picks the highlighted item. Returns
47
+ * `true` when the key is handled (or an IME is composing, when every key
48
+ * belongs to the IME), so the caller can stop; `false` leaves the key to the
49
+ * caller (for example SearchPicker's Escape).
52
50
  */
53
- export function handleListboxKey(event, { highlight, length, setHighlight, onEnter, }) {
51
+ export function handleListboxKey(event, { items, highlight, setHighlight, onPick, }) {
54
52
  if (isComposingKey(event))
55
53
  return true;
56
- const next = moveListboxHighlight(event.key, highlight, length);
54
+ const next = moveListboxHighlight(event.key, highlight, items.length);
57
55
  if (next !== undefined) {
58
56
  event.preventDefault();
59
57
  setHighlight(next);
@@ -61,7 +59,9 @@ export function handleListboxKey(event, { highlight, length, setHighlight, onEnt
61
59
  }
62
60
  if (event.key === "Enter") {
63
61
  event.preventDefault();
64
- onEnter(highlight);
62
+ const item = items[highlight];
63
+ if (item !== undefined)
64
+ onPick(item);
65
65
  return true;
66
66
  }
67
67
  return false;
@@ -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";
@@ -16,6 +17,11 @@ export declare const overlaySideSlide = "data-[side=bottom]:slide-in-from-top-2
16
17
  export declare const overlayFade = "duration-100 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0";
17
18
  /** Fade plus a slight zoom, for panels (the backdrop only fades). */
18
19
  export declare const overlayFadeZoom = "duration-100 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0 data-open:zoom-in-95 data-closed:zoom-out-95";
20
+ /**
21
+ * A panel opened from a trigger (Popover, Select, DropdownMenu): the
22
+ * surface, sliding in from the trigger's side with a fade and zoom.
23
+ */
24
+ export declare const anchoredPanel = "rounded-none bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 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 duration-100 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0 data-open:zoom-in-95 data-closed:zoom-out-95";
19
25
  /**
20
26
  * Base of a menu or listbox row (SelectItem, DropdownMenuItem, Combobox and
21
27
  * SearchPicker options). Each component adds its own right padding and
@@ -24,3 +30,7 @@ export declare const overlayFadeZoom = "duration-100 data-open:animate-in data-o
24
30
  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
31
  /** Group label in a menu or listbox (DropdownMenuLabel, SelectLabel). */
26
32
  export declare const menuLabel = "px-3 py-2 text-xs font-semibold tracking-wider text-muted-foreground uppercase";
33
+ /** Default gap in px between a trigger and its panel (Radix `sideOffset`). */
34
+ export declare const OVERLAY_SIDE_OFFSET = 4;
35
+ /** Default px a panel keeps from the viewport edge (Radix `collisionPadding`). */
36
+ 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";
@@ -16,6 +17,11 @@ export const overlaySideSlide = "data-[side=bottom]:slide-in-from-top-2 data-[si
16
17
  export const overlayFade = "duration-100 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0";
17
18
  /** Fade plus a slight zoom, for panels (the backdrop only fades). */
18
19
  export const overlayFadeZoom = `${overlayFade} data-open:zoom-in-95 data-closed:zoom-out-95`;
20
+ /**
21
+ * A panel opened from a trigger (Popover, Select, DropdownMenu): the
22
+ * surface, sliding in from the trigger's side with a fade and zoom.
23
+ */
24
+ export const anchoredPanel = `${overlaySurface} ${overlaySideSlide} ${overlayFadeZoom}`;
19
25
  /**
20
26
  * Base of a menu or listbox row (SelectItem, DropdownMenuItem, Combobox and
21
27
  * SearchPicker options). Each component adds its own right padding and
@@ -24,3 +30,7 @@ export const overlayFadeZoom = `${overlayFade} data-open:zoom-in-95 data-closed:
24
30
  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
31
  /** Group label in a menu or listbox (DropdownMenuLabel, SelectLabel). */
26
32
  export const menuLabel = "px-3 py-2 text-xs font-semibold tracking-wider text-muted-foreground uppercase";
33
+ /** Default gap in px between a trigger and its panel (Radix `sideOffset`). */
34
+ export const OVERLAY_SIDE_OFFSET = 4;
35
+ /** Default px a panel keeps from the viewport edge (Radix `collisionPadding`). */
36
+ export const OVERLAY_COLLISION_PADDING = 8;
@@ -9,4 +9,8 @@ export declare function createSearchIndex<T>(items: T[], getLabel: (item: T) =>
9
9
  item: T;
10
10
  label: string;
11
11
  }[];
12
+ /**
13
+ * The items whose folded label includes `needle` (a `normalizeSearchQuery`
14
+ * result), in order; every item when `needle` is empty.
15
+ */
12
16
  export declare function filterSearchIndex<T>(index: ReturnType<typeof createSearchIndex<T>>, needle: string): T[];
@@ -12,7 +12,13 @@ export function normalizeSearchQuery(query) {
12
12
  export function createSearchIndex(items, getLabel) {
13
13
  return items.map((item) => ({ item, label: foldSearchText(getLabel(item)) }));
14
14
  }
15
+ /**
16
+ * The items whose folded label includes `needle` (a `normalizeSearchQuery`
17
+ * result), in order; every item when `needle` is empty.
18
+ */
15
19
  export function filterSearchIndex(index, needle) {
20
+ if (!needle)
21
+ return index.map(({ item }) => item);
16
22
  return index
17
23
  .filter(({ label }) => label.includes(needle))
18
24
  .map(({ item }) => item);
@@ -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,6 +41,16 @@ 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";
42
56
  /**
@@ -83,6 +97,13 @@ export declare const controlSize: {
83
97
  * tailwind-merge then drops the `text-xs`.
84
98
  */
85
99
  export declare const controlText = "text-xs font-semibold tracking-widest whitespace-nowrap uppercase";
100
+ /**
101
+ * Secondary text under a title (CardDescription, PopoverDescription,
102
+ * DialogDescription, AlertDescription).
103
+ */
104
+ export declare const descriptionText = "text-sm leading-relaxed text-muted-foreground";
105
+ /** Field-label text (Label, FieldLegend, FieldTitle, PopoverTitle). */
106
+ export declare const labelText = "text-xs font-semibold tracking-wide uppercase";
86
107
  /**
87
108
  * Icons inside a control or row: no pointer events, no shrinking, 14px
88
109
  * unless the icon sets its own `size-*`. `sm` lowers that default to 12px
@@ -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,6 +41,16 @@ 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";
42
56
  /**
@@ -83,6 +97,13 @@ export const controlSize = {
83
97
  * tailwind-merge then drops the `text-xs`.
84
98
  */
85
99
  export const controlText = "text-xs font-semibold tracking-widest whitespace-nowrap uppercase";
100
+ /**
101
+ * Secondary text under a title (CardDescription, PopoverDescription,
102
+ * DialogDescription, AlertDescription).
103
+ */
104
+ export const descriptionText = "text-sm leading-relaxed text-muted-foreground";
105
+ /** Field-label text (Label, FieldLegend, FieldTitle, PopoverTitle). */
106
+ export const labelText = "text-xs font-semibold tracking-wide uppercase";
86
107
  /**
87
108
  * Icons inside a control or row: no pointer events, no shrinking, 14px
88
109
  * unless the icon sets its own `size-*`. `sm` lowers that default to 12px
package/dist/kanban.js CHANGED
@@ -7,6 +7,7 @@ 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. */
@@ -124,7 +125,7 @@ function KanbanColumnView({ column, count, emptyLabel, loading, className, child
124
125
  collisionPriority: COLUMN_DROP_PRIORITY,
125
126
  });
126
127
  const empty = count === 0;
127
- 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 })] })) })] }));
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 flex-wrap items-start justify-between gap-x-2 gap-y-0.5", children: [_jsxs("span", { className: "flex max-w-full min-w-0 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: "truncate leading-tight", title: column.label, children: column.label })] }), _jsxs("span", { className: "ml-auto flex max-w-full min-w-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: "max-w-full truncate text-[10px] font-medium", children: column.badge })) : null] })] }), _jsx("div", { className: "-m-1 mr-0 flex min-h-32 flex-1 flex-col gap-2 overflow-y-auto p-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
129
  }
129
130
  const cardChrome = cn("flex border border-border bg-card text-card-foreground");
130
131
  const handleChrome = cn("flex shrink-0 items-center self-stretch border-r border-border", "bg-muted/30 px-1.5 text-muted-foreground");
@@ -146,7 +147,7 @@ function KanbanCardViewImpl({ item, index, column, onCardClick, className, rende
146
147
  // The overlay moves; the source keeps its slot.
147
148
  "data-dragging:pointer-events-none data-dragging:invisible",
148
149
  // One ring on the card for the grip and the body alike.
149
- "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 }))] }));
150
151
  }
151
152
  /**
152
153
  * Memoized so a board update re-renders only the cards whose item, index or
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
  }
package/dist/nav-link.js CHANGED
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { cva } from "class-variance-authority";
4
4
  import { Slot } from "radix-ui";
5
+ import { activeLinkAttributes } from "./internal/active-link.js";
5
6
  import { focusRing, selectedState } from "./internal/styles.js";
6
7
  import { cn } from "./utils.js";
7
8
  /**
@@ -36,6 +37,6 @@ function isNavActive(pathname, href) {
36
37
  */
37
38
  function NavLink({ asChild, active, className, ...props }) {
38
39
  const Comp = asChild ? Slot.Root : "a";
39
- return (_jsx(Comp, { "data-slot": "nav-link", "data-state": active ? "active" : "inactive", "aria-current": active ? "page" : undefined, className: cn(navLinkVariants(), className), ...props }));
40
+ return (_jsx(Comp, { "data-slot": "nav-link", ...activeLinkAttributes(active), className: cn(navLinkVariants(), className), ...props }));
40
41
  }
41
42
  export { NavLink, isNavActive, navLinkVariants };
@@ -64,6 +64,8 @@ export type PickerTriggerProps = {
64
64
  "aria-labelledby"?: string;
65
65
  /** Forwarded to the trigger, e.g. the id of a hint or error text. */
66
66
  "aria-describedby"?: string;
67
+ /** Forwarded to the trigger; a form field sets it for `focusFirstInvalid`. */
68
+ "data-field-name"?: string;
67
69
  /** Popover side. Defaults to `bottom`; Radix still flips on collision. */
68
70
  side?: React.ComponentProps<typeof PopoverContent>["side"];
69
71
  /** Month shown when the popover opens; falls back to the selected date. */
@@ -112,5 +114,5 @@ type PickerShellProps = Omit<PickerTriggerProps, "placeholder"> & {
112
114
  * Form-control trigger plus the viewport-clamped popover holding the
113
115
  * calendar, any extra rows, and an optional Clear row.
114
116
  */
115
- export declare function PickerShell({ id, disabled, className, contentClassName, "aria-invalid": ariaInvalid, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, side, defaultMonth, minDate, maxDate, calendarProps, empty, label, dialogLabel, open, onOpenChange, calendar, onClear, clearDisabled, children, }: PickerShellProps): React.JSX.Element;
117
+ export declare function PickerShell({ id, disabled, className, contentClassName, "aria-invalid": ariaInvalid, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "data-field-name": fieldName, side, defaultMonth, minDate, maxDate, calendarProps, empty, label, dialogLabel, open, onOpenChange, calendar, onClear, clearDisabled, children, }: PickerShellProps): React.JSX.Element;
116
118
  export {};
@@ -4,7 +4,7 @@ import * as React from "react";
4
4
  import { CalendarIcon } from "lucide-react";
5
5
  import { Button } from "./button.js";
6
6
  import { toDate, } from "./internal/date-like.js";
7
- import { PICKER_COLLISION_PADDING, pickerContentClassName, pickerTriggerClassName, pickerValueId, } from "./internal/form-control.js";
7
+ import { fieldNameProps, PICKER_COLLISION_PADDING, pickerContentClassName, pickerTriggerClassName, pickerValueId, } from "./internal/form-control.js";
8
8
  import { LazyCalendar, preloadCalendar } from "./lazy-calendar.js";
9
9
  import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
10
10
  import { cn } from "./utils.js";
@@ -39,8 +39,8 @@ function limitCalendarProps(calendarProps, minDate, maxDate) {
39
39
  * Form-control trigger plus the viewport-clamped popover holding the
40
40
  * calendar, any extra rows, and an optional Clear row.
41
41
  */
42
- export function PickerShell({ id, disabled, className, contentClassName, "aria-invalid": ariaInvalid, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, side, defaultMonth, minDate, maxDate, calendarProps, empty, label, dialogLabel, open, onOpenChange, calendar, onClear, clearDisabled = false, children, }) {
43
- 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, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "data-empty": empty, className: cn(pickerTriggerClassName, className), onPointerEnter: preloadCalendar, onFocus: preloadCalendar, children: [_jsx(CalendarIcon, { "data-icon": "inline-start" }), _jsx("span", { "data-slot": "date-picker-value", className: "min-w-0 text-left whitespace-normal", id: id ? pickerValueId(id) : undefined, children: label })] }) }), _jsxs(PopoverContent, { align: "start", side: side, collisionPadding: PICKER_COLLISION_PADDING, "aria-label": dialogLabel,
42
+ export function PickerShell({ id, disabled, className, contentClassName, "aria-invalid": ariaInvalid, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "data-field-name": fieldName, side, defaultMonth, minDate, maxDate, calendarProps, empty, label, dialogLabel, open, onOpenChange, calendar, onClear, clearDisabled = false, children, }) {
43
+ 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, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...fieldNameProps(fieldName), "data-empty": empty, className: cn(pickerTriggerClassName, className), onPointerEnter: preloadCalendar, onFocus: preloadCalendar, children: [_jsx(CalendarIcon, { "data-icon": "inline-start" }), _jsx("span", { "data-slot": "date-picker-value", className: "min-w-0 text-left whitespace-normal", id: id ? pickerValueId(id) : undefined, children: label })] }) }), _jsxs(PopoverContent, { align: "start", side: side, collisionPadding: PICKER_COLLISION_PADDING, "aria-label": dialogLabel,
44
44
  // The calendar's `autoFocus` puts focus on the selected day (or
45
45
  // today) once it loads, as the APG date picker dialog does; Radix
46
46
  // focusing the popover first would leave it on the container.
package/dist/popover.js CHANGED
@@ -2,7 +2,8 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Popover as PopoverPrimitive } from "radix-ui";
5
- import { overlayFadeZoom, overlaySideSlide, overlaySurface, } from "./internal/overlay-styles.js";
5
+ import { anchoredPanel, OVERLAY_COLLISION_PADDING, OVERLAY_SIDE_OFFSET, } from "./internal/overlay-styles.js";
6
+ import { descriptionText, labelText } from "./internal/styles.js";
6
7
  import { cn } from "./utils.js";
7
8
  function Popover({ ...props }) {
8
9
  return _jsx(PopoverPrimitive.Root, { "data-slot": "popover", ...props });
@@ -10,8 +11,8 @@ function Popover({ ...props }) {
10
11
  function PopoverTrigger({ ...props }) {
11
12
  return _jsx(PopoverPrimitive.Trigger, { "data-slot": "popover-trigger", ...props });
12
13
  }
13
- function PopoverContent({ className, align = "center", sideOffset = 4, collisionPadding = 8, ...props }) {
14
- return (_jsx(PopoverPrimitive.Portal, { children: _jsx(PopoverPrimitive.Content, { "data-slot": "popover-content", align: align, sideOffset: sideOffset, collisionPadding: collisionPadding, className: cn(overlaySurface, overlaySideSlide, overlayFadeZoom, "z-50 flex max-w-(--radix-popover-content-available-width) origin-(--radix-popover-content-transform-origin) flex-col gap-4 p-4 text-sm outline-hidden", className), ...props }) }));
14
+ function PopoverContent({ className, align = "center", sideOffset = OVERLAY_SIDE_OFFSET, collisionPadding = OVERLAY_COLLISION_PADDING, ...props }) {
15
+ return (_jsx(PopoverPrimitive.Portal, { children: _jsx(PopoverPrimitive.Content, { "data-slot": "popover-content", align: align, sideOffset: sideOffset, collisionPadding: collisionPadding, className: cn(anchoredPanel, "z-50 flex max-w-(--radix-popover-content-available-width) origin-(--radix-popover-content-transform-origin) flex-col gap-4 p-4 text-sm outline-hidden", className), ...props }) }));
15
16
  }
16
17
  function PopoverAnchor({ ...props }) {
17
18
  return _jsx(PopoverPrimitive.Anchor, { "data-slot": "popover-anchor", ...props });
@@ -20,9 +21,9 @@ function PopoverHeader({ className, ...props }) {
20
21
  return (_jsx("div", { "data-slot": "popover-header", className: cn("flex flex-col gap-1 text-sm", className), ...props }));
21
22
  }
22
23
  function PopoverTitle({ className, ...props }) {
23
- return (_jsx("h2", { "data-slot": "popover-title", className: cn("text-xs font-semibold uppercase", className), ...props }));
24
+ return (_jsx("h2", { "data-slot": "popover-title", className: cn(labelText, className), ...props }));
24
25
  }
25
26
  function PopoverDescription({ className, ...props }) {
26
- return (_jsx("p", { "data-slot": "popover-description", className: cn("mt-0.5 text-sm leading-relaxed text-muted-foreground", className), ...props }));
27
+ return (_jsx("p", { "data-slot": "popover-description", className: cn("mt-0.5", descriptionText, className), ...props }));
27
28
  }
28
29
  export { Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, };
@@ -3,11 +3,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Input } from "./input.js";
5
5
  import { Label } from "./label.js";
6
- import { handleListboxKey, listboxOptionId, } from "./internal/listbox-navigation.js";
7
- import { ListboxEmptyStatus, PickerOption } from "./internal/listbox-option.js";
6
+ import { handleListboxKey } from "./internal/listbox-navigation.js";
7
+ import { Listbox, ListboxEmptyStatus, listboxInputAria, PickerOption, } from "./internal/listbox-option.js";
8
8
  import { createSearchIndex, filterSearchIndex, foldSearchText, normalizeSearchQuery, } from "./internal/search.js";
9
- import { listboxScroll } from "./internal/styles.js";
10
- import { cn } from "./utils.js";
9
+ import { cn, onInputChange } from "./utils.js";
11
10
  export { foldSearchText };
12
11
  /**
13
12
  * Default SearchPicker filter: the folded label includes the folded query.
@@ -25,7 +24,7 @@ const listFrame = "border border-input bg-background";
25
24
  * the list is not in a popover. Keyboard: arrows, Home/End, PageUp/PageDown,
26
25
  * Enter, Escape.
27
26
  */
28
- function SearchPicker({ items, getValue, getLabel, filter = defaultSearchFilter, value, onSelect, label, placeholder = "Search", emptyText = "No results", listLabel, disabled, className, listClassName, ...props }) {
27
+ function SearchPicker({ items, getValue, getLabel, filter = defaultSearchFilter, value, onSelect, label, placeholder = "Search", emptyText, listLabel, disabled, className, listClassName, ...props }) {
29
28
  const [query, setQuery] = React.useState("");
30
29
  const listId = React.useId();
31
30
  const inputId = React.useId();
@@ -34,11 +33,8 @@ function SearchPicker({ items, getValue, getLabel, filter = defaultSearchFilter,
34
33
  : null, [filter, getLabel, items]);
35
34
  const foldedQuery = filter === defaultSearchFilter ? normalizeSearchQuery(query) : "";
36
35
  const filtered = React.useMemo(() => {
37
- if (normalizedItems) {
38
- if (!foldedQuery)
39
- return items;
36
+ if (normalizedItems)
40
37
  return filterSearchIndex(normalizedItems, foldedQuery);
41
- }
42
38
  return items.filter((item) => filter(item, query, getLabel));
43
39
  }, [items, filter, foldedQuery, getLabel, normalizedItems, query]);
44
40
  const selectedIndex = React.useMemo(() => {
@@ -67,14 +63,10 @@ function SearchPicker({ items, getValue, getLabel, filter = defaultSearchFilter,
67
63
  }, [onSelect]);
68
64
  const onKeyDown = (event) => {
69
65
  const handled = handleListboxKey(event, {
66
+ items: filtered,
70
67
  highlight,
71
- length: filtered.length,
72
68
  setHighlight,
73
- onEnter: (index) => {
74
- const item = filtered[index];
75
- if (item)
76
- selectItem(item);
77
- },
69
+ onPick: selectItem,
78
70
  });
79
71
  if (handled || event.key !== "Escape")
80
72
  return;
@@ -85,6 +77,6 @@ function SearchPicker({ items, getValue, getLabel, filter = defaultSearchFilter,
85
77
  }
86
78
  event.currentTarget.blur();
87
79
  };
88
- return (_jsxs("div", { "data-slot": "search-picker", className: cn("flex flex-col gap-1.5", className), ...props, children: [_jsx(Label, { htmlFor: inputId, children: label }), _jsx(Input, { id: inputId, role: "combobox", "aria-expanded": true, "aria-controls": listId, "aria-autocomplete": "list", "aria-activedescendant": active ? listboxOptionId(listId, highlight) : undefined, value: query, disabled: disabled, placeholder: placeholder, onChange: (event) => setQuery(event.target.value), onKeyDown: onKeyDown }), _jsx("ul", { id: listId, role: "listbox", "aria-label": listLabel ?? label, className: empty ? "sr-only" : cn(listboxScroll, listFrame, listClassName), children: rows.map(({ item, value: itemValue }, index) => (_jsx(PickerOption, { "data-slot": "search-picker-option", listId: listId, index: index, active: index === highlight, selected: itemValue === value, disabled: disabled, item: item, getLabel: getLabel, onHighlight: setHighlight, onSelect: selectItem }, itemValue))) }), _jsx(ListboxEmptyStatus, { "data-slot": "search-picker-empty", empty: empty, className: listFrame, children: emptyText })] }));
80
+ return (_jsxs("div", { "data-slot": "search-picker", className: cn("flex flex-col gap-1.5", className), ...props, children: [_jsx(Label, { htmlFor: inputId, children: label }), _jsx(Input, { id: inputId, role: "combobox", "aria-expanded": true, "aria-controls": listId, ...listboxInputAria(listId, active ? highlight : undefined), value: query, disabled: disabled, placeholder: placeholder, onChange: onInputChange(setQuery), onKeyDown: onKeyDown }), _jsx(Listbox, { id: listId, empty: empty, "aria-label": listLabel ?? label, className: cn(listFrame, listClassName), children: rows.map(({ item, value: itemValue }, index) => (_jsx(PickerOption, { "data-slot": "search-picker-option", listId: listId, index: index, active: index === highlight, selected: itemValue === value, disabled: disabled, item: item, getLabel: getLabel, onHighlight: setHighlight, onSelect: selectItem }, itemValue))) }), _jsx(ListboxEmptyStatus, { "data-slot": "search-picker-empty", empty: empty, className: listFrame, children: emptyText })] }));
89
81
  }
90
82
  export { SearchPicker };
package/dist/select.js CHANGED
@@ -2,8 +2,8 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Select as SelectPrimitive } from "radix-ui";
5
- import { dataDisabledState, disabledState, fieldSurface, focusRing, inlineIcon, invalidState, optionState, } from "./internal/styles.js";
6
- import { menuLabel, optionRow, overlayFadeZoom, overlaySideSlide, overlaySurface, } from "./internal/overlay-styles.js";
5
+ import { dataDisabledState, disabledState, emptyValueText, fieldSurface, focusRing, inlineIcon, invalidState, optionState, } from "./internal/styles.js";
6
+ import { anchoredPanel, menuLabel, optionRow, OVERLAY_COLLISION_PADDING, OVERLAY_SIDE_OFFSET, } from "./internal/overlay-styles.js";
7
7
  import { cn } from "./utils.js";
8
8
  import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react";
9
9
  /**
@@ -22,10 +22,10 @@ function SelectValue({ ...props }) {
22
22
  return _jsx(SelectPrimitive.Value, { "data-slot": "select-value", ...props });
23
23
  }
24
24
  function SelectTrigger({ className, size = "default", children, ...props }) {
25
- return (_jsxs(SelectPrimitive.Trigger, { "data-slot": "select-trigger", "data-size": size, className: cn(fieldSurface, focusRing, invalidState, disabledState, inlineIcon.default, triggerHeight[size], "flex items-center justify-between gap-1.5 whitespace-nowrap data-placeholder:text-muted-foreground *: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", className), ...props, children: [children, _jsx(SelectPrimitive.Icon, { "data-slot": "select-icon", asChild: true, children: _jsx(ChevronDownIcon, { className: "pointer-events-none size-3.5 text-muted-foreground" }) })] }));
25
+ return (_jsxs(SelectPrimitive.Trigger, { "data-slot": "select-trigger", "data-size": size, className: cn(fieldSurface, focusRing, invalidState, disabledState, emptyValueText, inlineIcon.default, triggerHeight[size], "flex items-center justify-between gap-1.5 whitespace-nowrap *: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", className), ...props, children: [children, _jsx(SelectPrimitive.Icon, { "data-slot": "select-icon", asChild: true, children: _jsx(ChevronDownIcon, { className: "pointer-events-none size-3.5 text-muted-foreground" }) })] }));
26
26
  }
27
- function SelectContent({ className, children, position = "popper", align = "center", sideOffset = 4, collisionPadding = 8, ...props }) {
28
- return (_jsx(SelectPrimitive.Portal, { children: _jsxs(SelectPrimitive.Content, { "data-slot": "select-content", "data-align-trigger": position === "item-aligned", className: cn(overlaySurface, overlaySideSlide, overlayFadeZoom, "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 data-[align-trigger=true]:animate-none", position === "popper" &&
27
+ function SelectContent({ className, children, position = "popper", align = "center", sideOffset = OVERLAY_SIDE_OFFSET, collisionPadding = OVERLAY_COLLISION_PADDING, ...props }) {
28
+ return (_jsx(SelectPrimitive.Portal, { children: _jsxs(SelectPrimitive.Content, { "data-slot": "select-content", "data-align-trigger": position === "item-aligned", className: cn(anchoredPanel, "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 data-[align-trigger=true]:animate-none", position === "popper" &&
29
29
  "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className), position: position, align: align, sideOffset: sideOffset, collisionPadding: collisionPadding, ...props, children: [_jsx(SelectScrollUpButton, {}), _jsx(SelectPrimitive.Viewport, { "data-slot": "select-viewport", "data-position": position, className: "data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)", children: children }), _jsx(SelectScrollDownButton, {})] }) }));
30
30
  }
31
31
  function SelectLabel({ className, ...props }) {
package/dist/skip-link.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { focusRing } from "./internal/styles.js";
2
3
  import { cn } from "./utils.js";
3
4
  /**
4
5
  * First focusable link on the page: hidden until a keyboard user tabs to
5
6
  * it, then it jumps past the navigation to `href` (the main landmark's id).
6
7
  */
7
8
  function SkipLink({ href = "#main-content", className, children = "Skip to content", ...props }) {
8
- return (_jsx("a", { "data-slot": "skip-link", href: href, className: cn("sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:bg-background focus:px-3 focus:py-2 focus:text-sm focus:ring-2 focus:ring-ring", className), ...props, children: children }));
9
+ return (_jsx("a", { "data-slot": "skip-link", href: href, className: cn("sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:bg-background focus:px-3 focus:py-2 focus:text-sm", focusRing, className), ...props, children: children }));
9
10
  }
10
11
  export { SkipLink };