@seekalfred/primitives 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,7 +24,7 @@ import { ComponentPropsWithoutRef, ReactNode } from "react";
24
24
  * problems rather than porting them.
25
25
  */
26
26
  export declare const alertVariants: (props?: ({
27
- variant?: "default" | "destructive" | null | undefined;
27
+ variant?: "destructive" | "default" | null | undefined;
28
28
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
29
29
  /**
30
30
  * An inline status message.
@@ -36,7 +36,7 @@ import { ComponentPropsWithoutRef } from "react";
36
36
  * across every shadcn control.
37
37
  */
38
38
  export declare const badgeVariants: (props?: ({
39
- tone?: "neutral" | "success" | "warning" | "danger" | "info" | "primary" | null | undefined;
39
+ tone?: "primary" | "neutral" | "success" | "warning" | "danger" | "info" | null | undefined;
40
40
  appearance?: "outline" | "solid" | "soft" | null | undefined;
41
41
  size?: "sm" | "md" | "lg" | null | undefined;
42
42
  shape?: "rounded" | "pill" | null | undefined;
@@ -21,7 +21,7 @@ import { ReactNode } from "react";
21
21
  * touch and keyboard uniformly. The original had no pressed state at all.
22
22
  */
23
23
  export declare const buttonVariants: (props?: ({
24
- variant?: "destructive" | "primary" | "secondary" | "outline" | "outlineNeutral" | "ghost" | "link" | null | undefined;
24
+ variant?: "primary" | "secondary" | "outline" | "outlineNeutral" | "ghost" | "destructive" | "link" | null | undefined;
25
25
  size?: "sm" | "md" | "lg" | "icon" | null | undefined;
26
26
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
27
27
  /**
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ import { Card, CardContent, CardContentProps, CardDescription, CardDescriptionPr
11
11
  import { Checkbox, CheckboxProps, checkboxBoxVariants, checkboxVariants } from "./checkbox/checkbox.js";
12
12
  import { Input, InputProps, inputVariants } from "./input/input.js";
13
13
  import { Label, LabelProps, labelVariants } from "./label/label.js";
14
- import { cn } from "./lib/cn.js";
14
+ import { cn, cnAll } from "./lib/cn.js";
15
15
  import { Menu, MenuItem, MenuItemProps, MenuProps, MenuSeparator, MenuTrigger, menuItemVariants, menuPopoverVariants, menuSeparatorVariants, menuVariants } from "./menu/menu.js";
16
16
  import { Popover, PopoverProps, PopoverTrigger, popoverVariants } from "./popover/popover.js";
17
17
  import { Progress, ProgressProps, progressFillVariants, progressTrackVariants, progressVariants } from "./progress/progress.js";
@@ -29,4 +29,4 @@ import { Tab, TabList, TabListProps, TabPanel, TabPanelProps, TabProps, Tabs, Ta
29
29
  import { Textarea, TextareaProps, textareaVariants } from "./textarea/textarea.js";
30
30
  import { ToastContent, ToastRegion, ToastRegionProps, toast, toastCloseVariants, toastQueue, toastRegionVariants, toastVariants } from "./toast/toast.js";
31
31
  import { Tooltip, TooltipProps, TooltipTrigger, TooltipTriggerProps, tooltipVariants } from "./tooltip/tooltip.js";
32
- export { Accordion, AccordionItem, type AccordionItemProps, AccordionPanel, type AccordionPanelProps, type AccordionProps, AccordionTrigger, type AccordionTriggerProps, Alert, AlertDialog, type AlertDialogProps, type AlertProps, Avatar, type AvatarProps, Badge, type BadgeProps, BreadcrumbItem, type BreadcrumbItemProps, Breadcrumbs, type BreadcrumbsProps, Button, type ButtonProps, Calendar, type CalendarProps, CalendarRange, type CalendarRangeProps, Card, CardContent, type CardContentProps, CardDescription, type CardDescriptionProps, CardFooter, type CardFooterProps, CardHeader, type CardHeaderProps, type CardProps, CardTitle, type CardTitleProps, Checkbox, type CheckboxProps, type Column, Dialog, DialogFooter, type DialogFooterProps, type DialogProps, DialogTrigger, Input, type InputProps, Label, type LabelProps, Menu, MenuItem, type MenuItemProps, type MenuProps, MenuSeparator, MenuTrigger, Popover, type PopoverProps, PopoverTrigger, Progress, type ProgressProps, Radio, RadioGroup, type RadioGroupProps, type RadioProps, type RowKey, ScrollArea, type ScrollAreaProps, Select, SelectItem, type SelectItemProps, type SelectProps, Separator, type SeparatorProps, Sheet, type SheetProps, Skeleton, type SkeletonProps, Slider, type SliderProps, Spinner, type SpinnerProps, Switch, type SwitchProps, Tab, TabList, type TabListProps, TabPanel, type TabPanelProps, type TabProps, Table, type TableProps, Tabs, type TabsProps, Textarea, type TextareaProps, type ToastContent, ToastRegion, type ToastRegionProps, Tooltip, type TooltipProps, TooltipTrigger, type TooltipTriggerProps, alertVariants, avatarVariants, badgeVariants, breadcrumbItemVariants, breadcrumbLinkVariants, breadcrumbsVariants, buttonVariants, calendarCellVariants, calendarClearVariants, calendarGridVariants, calendarHeaderCellVariants, calendarHeaderVariants, calendarHeadingVariants, calendarLabelVariants, calendarMonthsVariants, calendarNavVariants, calendarVariants, cardContentVariants, cardDescriptionVariants, cardFooterVariants, cardHeaderVariants, cardTitleVariants, cardVariants, checkboxBoxVariants, checkboxVariants, cn, createColumns, dialogDescriptionVariants, dialogFooterVariants, dialogOverlayVariants, dialogPanelVariants, dialogTitleVariants, inputVariants, labelVariants, menuItemVariants, menuPopoverVariants, menuSeparatorVariants, menuVariants, popoverVariants, progressFillVariants, progressTrackVariants, progressVariants, radioGroupVariants, radioVariants, scrollAreaVariants, selectItemVariants, selectPopoverVariants, selectTriggerVariants, selectVariants, separatorVariants, sheetOverlayVariants, sheetPanelVariants, skeletonVariants, sliderBarVariants, sliderFillVariants, sliderThumbVariants, sliderTrackVariants, sliderVariants, spinnerVariants, switchThumbVariants, switchTrackVariants, tabListVariants, tabPanelVariants, tabVariants, tableCardVariants, tableCellVariants, tableColumnVariants, tableFooterVariants, tableHeaderVariants, tablePaginationVariants, tableRowVariants, tableScrollVariants, tableVariants, tabsVariants, textareaVariants, toast, toastCloseVariants, toastQueue, toastRegionVariants, toastVariants, tooltipVariants };
32
+ export { Accordion, AccordionItem, type AccordionItemProps, AccordionPanel, type AccordionPanelProps, type AccordionProps, AccordionTrigger, type AccordionTriggerProps, Alert, AlertDialog, type AlertDialogProps, type AlertProps, Avatar, type AvatarProps, Badge, type BadgeProps, BreadcrumbItem, type BreadcrumbItemProps, Breadcrumbs, type BreadcrumbsProps, Button, type ButtonProps, Calendar, type CalendarProps, CalendarRange, type CalendarRangeProps, Card, CardContent, type CardContentProps, CardDescription, type CardDescriptionProps, CardFooter, type CardFooterProps, CardHeader, type CardHeaderProps, type CardProps, CardTitle, type CardTitleProps, Checkbox, type CheckboxProps, type Column, Dialog, DialogFooter, type DialogFooterProps, type DialogProps, DialogTrigger, Input, type InputProps, Label, type LabelProps, Menu, MenuItem, type MenuItemProps, type MenuProps, MenuSeparator, MenuTrigger, Popover, type PopoverProps, PopoverTrigger, Progress, type ProgressProps, Radio, RadioGroup, type RadioGroupProps, type RadioProps, type RowKey, ScrollArea, type ScrollAreaProps, Select, SelectItem, type SelectItemProps, type SelectProps, Separator, type SeparatorProps, Sheet, type SheetProps, Skeleton, type SkeletonProps, Slider, type SliderProps, Spinner, type SpinnerProps, Switch, type SwitchProps, Tab, TabList, type TabListProps, TabPanel, type TabPanelProps, type TabProps, Table, type TableProps, Tabs, type TabsProps, Textarea, type TextareaProps, type ToastContent, ToastRegion, type ToastRegionProps, Tooltip, type TooltipProps, TooltipTrigger, type TooltipTriggerProps, alertVariants, avatarVariants, badgeVariants, breadcrumbItemVariants, breadcrumbLinkVariants, breadcrumbsVariants, buttonVariants, calendarCellVariants, calendarClearVariants, calendarGridVariants, calendarHeaderCellVariants, calendarHeaderVariants, calendarHeadingVariants, calendarLabelVariants, calendarMonthsVariants, calendarNavVariants, calendarVariants, cardContentVariants, cardDescriptionVariants, cardFooterVariants, cardHeaderVariants, cardTitleVariants, cardVariants, checkboxBoxVariants, checkboxVariants, cn, cnAll, createColumns, dialogDescriptionVariants, dialogFooterVariants, dialogOverlayVariants, dialogPanelVariants, dialogTitleVariants, inputVariants, labelVariants, menuItemVariants, menuPopoverVariants, menuSeparatorVariants, menuVariants, popoverVariants, progressFillVariants, progressTrackVariants, progressVariants, radioGroupVariants, radioVariants, scrollAreaVariants, selectItemVariants, selectPopoverVariants, selectTriggerVariants, selectVariants, separatorVariants, sheetOverlayVariants, sheetPanelVariants, skeletonVariants, sliderBarVariants, sliderFillVariants, sliderThumbVariants, sliderTrackVariants, sliderVariants, spinnerVariants, switchThumbVariants, switchTrackVariants, tabListVariants, tabPanelVariants, tabVariants, tableCardVariants, tableCellVariants, tableColumnVariants, tableFooterVariants, tableHeaderVariants, tablePaginationVariants, tableRowVariants, tableScrollVariants, tableVariants, tabsVariants, textareaVariants, toast, toastCloseVariants, toastQueue, toastRegionVariants, toastVariants, tooltipVariants };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { cn } from "./lib/cn.js";
1
+ import { cn, cnAll } from "./lib/cn.js";
2
2
  import { Accordion, AccordionItem, AccordionPanel, AccordionTrigger } from "./accordion/accordion.js";
3
3
  import { Dialog, DialogFooter, DialogTrigger, dialogDescriptionVariants, dialogFooterVariants, dialogOverlayVariants, dialogPanelVariants, dialogTitleVariants } from "./dialog/dialog.js";
4
4
  import { AlertDialog } from "./alert-dialog/alert-dialog.js";
@@ -29,4 +29,4 @@ import { Tab, TabList, TabPanel, Tabs, tabListVariants, tabPanelVariants, tabVar
29
29
  import { Textarea, textareaVariants } from "./textarea/textarea.js";
30
30
  import { ToastRegion, toast, toastCloseVariants, toastQueue, toastRegionVariants, toastVariants } from "./toast/toast.js";
31
31
  import { Tooltip, TooltipTrigger, tooltipVariants } from "./tooltip/tooltip.js";
32
- export { Accordion, AccordionItem, AccordionPanel, AccordionTrigger, Alert, AlertDialog, Avatar, Badge, BreadcrumbItem, Breadcrumbs, Button, Calendar, CalendarRange, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, Dialog, DialogFooter, DialogTrigger, Input, Label, Menu, MenuItem, MenuSeparator, MenuTrigger, Popover, PopoverTrigger, Progress, Radio, RadioGroup, ScrollArea, Select, SelectItem, Separator, Sheet, Skeleton, Slider, Spinner, Switch, Tab, TabList, TabPanel, Table, Tabs, Textarea, ToastRegion, Tooltip, TooltipTrigger, alertVariants, avatarVariants, badgeVariants, breadcrumbItemVariants, breadcrumbLinkVariants, breadcrumbsVariants, buttonVariants, calendarCellVariants, calendarClearVariants, calendarGridVariants, calendarHeaderCellVariants, calendarHeaderVariants, calendarHeadingVariants, calendarLabelVariants, calendarMonthsVariants, calendarNavVariants, calendarVariants, cardContentVariants, cardDescriptionVariants, cardFooterVariants, cardHeaderVariants, cardTitleVariants, cardVariants, checkboxBoxVariants, checkboxVariants, cn, createColumns, dialogDescriptionVariants, dialogFooterVariants, dialogOverlayVariants, dialogPanelVariants, dialogTitleVariants, inputVariants, labelVariants, menuItemVariants, menuPopoverVariants, menuSeparatorVariants, menuVariants, popoverVariants, progressFillVariants, progressTrackVariants, progressVariants, radioGroupVariants, radioVariants, scrollAreaVariants, selectItemVariants, selectPopoverVariants, selectTriggerVariants, selectVariants, separatorVariants, sheetOverlayVariants, sheetPanelVariants, skeletonVariants, sliderBarVariants, sliderFillVariants, sliderThumbVariants, sliderTrackVariants, sliderVariants, spinnerVariants, switchThumbVariants, switchTrackVariants, tabListVariants, tabPanelVariants, tabVariants, tableCardVariants, tableCellVariants, tableColumnVariants, tableFooterVariants, tableHeaderVariants, tablePaginationVariants, tableRowVariants, tableScrollVariants, tableVariants, tabsVariants, textareaVariants, toast, toastCloseVariants, toastQueue, toastRegionVariants, toastVariants, tooltipVariants };
32
+ export { Accordion, AccordionItem, AccordionPanel, AccordionTrigger, Alert, AlertDialog, Avatar, Badge, BreadcrumbItem, Breadcrumbs, Button, Calendar, CalendarRange, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, Dialog, DialogFooter, DialogTrigger, Input, Label, Menu, MenuItem, MenuSeparator, MenuTrigger, Popover, PopoverTrigger, Progress, Radio, RadioGroup, ScrollArea, Select, SelectItem, Separator, Sheet, Skeleton, Slider, Spinner, Switch, Tab, TabList, TabPanel, Table, Tabs, Textarea, ToastRegion, Tooltip, TooltipTrigger, alertVariants, avatarVariants, badgeVariants, breadcrumbItemVariants, breadcrumbLinkVariants, breadcrumbsVariants, buttonVariants, calendarCellVariants, calendarClearVariants, calendarGridVariants, calendarHeaderCellVariants, calendarHeaderVariants, calendarHeadingVariants, calendarLabelVariants, calendarMonthsVariants, calendarNavVariants, calendarVariants, cardContentVariants, cardDescriptionVariants, cardFooterVariants, cardHeaderVariants, cardTitleVariants, cardVariants, checkboxBoxVariants, checkboxVariants, cn, cnAll, createColumns, dialogDescriptionVariants, dialogFooterVariants, dialogOverlayVariants, dialogPanelVariants, dialogTitleVariants, inputVariants, labelVariants, menuItemVariants, menuPopoverVariants, menuSeparatorVariants, menuVariants, popoverVariants, progressFillVariants, progressTrackVariants, progressVariants, radioGroupVariants, radioVariants, scrollAreaVariants, selectItemVariants, selectPopoverVariants, selectTriggerVariants, selectVariants, separatorVariants, sheetOverlayVariants, sheetPanelVariants, skeletonVariants, sliderBarVariants, sliderFillVariants, sliderThumbVariants, sliderTrackVariants, sliderVariants, spinnerVariants, switchThumbVariants, switchTrackVariants, tabListVariants, tabPanelVariants, tabVariants, tableCardVariants, tableCellVariants, tableColumnVariants, tableFooterVariants, tableHeaderVariants, tablePaginationVariants, tableRowVariants, tableScrollVariants, tableVariants, tabsVariants, textareaVariants, toast, toastCloseVariants, toastQueue, toastRegionVariants, toastVariants, tooltipVariants };
@@ -74,7 +74,7 @@ interface InputProps$1 extends Omit<InputProps, "className" | "style" | "size">
74
74
  /** Decorative glyph at the trailing edge. Same rules as {@link InputProps.startIcon}. */
75
75
  endIcon?: ReactNode;
76
76
  }
77
- declare function Input$1({ className, size, startIcon, endIcon, ...props }: InputProps$1): import("react").JSX.Element;
77
+ declare const Input$1: import("react").ForwardRefExoticComponent<InputProps$1 & import("react").RefAttributes<HTMLInputElement>>;
78
78
  //#endregion
79
79
  export { Input$1 as Input, InputProps$1 as InputProps };
80
80
  //# sourceMappingURL=input.d.ts.map
@@ -4,6 +4,7 @@ import { fieldSurfaceVariants } from "../lib/field-surface.js";
4
4
  import { cva } from "class-variance-authority";
5
5
  import { Input } from "react-aria-components";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { forwardRef } from "react";
7
8
  //#region src/input/input.tsx
8
9
  /**
9
10
  * Input, ported from the legacy shadcn component onto React Aria's Input.
@@ -154,8 +155,9 @@ const affixVariants = cva([
154
155
  ],
155
156
  defaultVariants: { size: "md" }
156
157
  });
157
- function Input$1({ className, size = "md", startIcon, endIcon, ...props }) {
158
+ const Input$1 = forwardRef(function Input$2({ className, size = "md", startIcon, endIcon, ...props }, ref) {
158
159
  const input = /* @__PURE__ */ jsx(Input, {
160
+ ref,
159
161
  ...props,
160
162
  "data-slot": "input",
161
163
  className: cn(inputVariants({
@@ -188,7 +190,7 @@ function Input$1({ className, size = "md", startIcon, endIcon, ...props }) {
188
190
  })
189
191
  ]
190
192
  });
191
- }
193
+ });
192
194
  //#endregion
193
195
  export { Input$1 as Input, inputVariants };
194
196
 
@@ -1 +1 @@
1
- {"version":3,"file":"input.js","names":["Input","AriaInput"],"sources":["../../src/input/input.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\nimport { Input as AriaInput, type InputProps as AriaInputProps } from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { fieldSurfaceVariants } from \"../lib/field-surface.js\";\n\n/**\n * Input, ported from the legacy shadcn component onto React Aria's Input.\n *\n * Census: 25 files, 51 uses -- the busiest control in the app after Button.\n * Props are `onChange` 36, `value` 35, `placeholder` 33, `id` 23, `type` 21.\n *\n * EVERY ONE OF THOSE 51 CALL SITES MIGRATES UNCHANGED. React Aria's `Input`\n * takes NATIVE props -- `disabled`, `aria-invalid`, `onChange` with a real\n * event -- not the `isDisabled`/`onChange(value)` shape the rest of React\n * Aria uses. It reads `InputContext` when one is present, so the same\n * component is a bare styled input today and composes inside a `TextField`\n * the moment one is wrapped around it. There is no rename to schedule.\n *\n * That is also why this is not `TextField`. Wrapping 51 call sites in a\n * field they do not have, to get a `onChange(value: string)` they did not\n * ask for, would be the migration doing work on the app's behalf.\n *\n * `border-input` becomes `border-border`: in the legacy theme `--input` is\n * character-for-character `--border` on light (`oklch(0.922 0 0)`), and the\n * dark values differ only in alpha. A fourth token role that turns out to\n * be an existing one, after `popover`, `accent` and the scrim.\n */\nexport const inputVariants = cva(\n [\n fieldSurfaceVariants(),\n\n // text-base always, not the legacy `text-base md:text-sm`. 16px is the\n // threshold below which iOS Safari zooms the viewport on focus, and the\n // legacy dropped to 14px at exactly the widths where that stops being a\n // phone -- which reads as a deliberate fix but leaves the field a\n // different size on either side of a breakpoint for no visual reason.\n \"text-base\",\n\n // type=\"file\" has one call site, which is not zero. Enough to keep it\n // from rendering as an unstyled user-agent button.\n \"file:inline-flex file:border-0 file:bg-transparent file:text-sm file:font-medium\",\n \"file:text-foreground\",\n ],\n {\n variants: {\n /**\n * Height and inline padding from the control scale, so `data-density`\n * moves a whole form at once. Same three names and the same tokens as\n * Button, because they sit in the same row: Button's `sm` is the app's\n * second most common at 167 call sites, and while the field was locked\n * to `md` the pair was 10px out of alignment with no supported fix.\n *\n * Type does NOT scale, which is the one place this axis deliberately\n * departs from Button's. iOS Safari zooms the viewport when a focused\n * field's text is under 16px, so `text-base` stays pinned in the base\n * at every rung -- see the note there. Shrinking the box is a layout\n * choice; shrinking the type is a bug on a phone.\n */\n size: {\n sm: \"h-(--density-control-h-sm) px-(--density-control-pad-sm)\",\n md: \"h-(--density-control-h-md) px-(--density-control-pad-md)\",\n lg: \"h-(--density-control-h-lg) px-(--density-control-pad-lg)\",\n },\n // Declared empty and filled by the compound variants below: the affix\n // inset depends on BOTH whether a slot is used and which rung it is on.\n hasStartIcon: { true: \"\" },\n hasEndIcon: { true: \"\" },\n },\n compoundVariants: [\n // The affix slots float ABOVE the field rather than sitting beside it\n // in a flex row, so the only thing keeping the value from running\n // underneath a glyph is this padding. It is the same three tokens that\n // place the glyph -- inline padding, icon box, control gap -- so the\n // text and the glyph move together across density AND size. A\n // hand-tuned `pl-9` at the call site can do neither, and the product\n // app has eighteen of those.\n {\n size: \"sm\",\n hasStartIcon: true,\n class:\n \"ps-[calc(var(--density-control-pad-sm)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"md\",\n hasStartIcon: true,\n class:\n \"ps-[calc(var(--density-control-pad-md)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"lg\",\n hasStartIcon: true,\n class:\n \"ps-[calc(var(--density-control-pad-lg)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"sm\",\n hasEndIcon: true,\n class:\n \"pe-[calc(var(--density-control-pad-sm)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"md\",\n hasEndIcon: true,\n class:\n \"pe-[calc(var(--density-control-pad-md)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"lg\",\n hasEndIcon: true,\n class:\n \"pe-[calc(var(--density-control-pad-lg)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n ],\n defaultVariants: { size: \"md\" },\n },\n);\n\n/**\n * An affix slot.\n *\n * `pointer-events-none` is load-bearing, not cosmetic: the slot is painted on\n * top of the field, so without it the glyph swallows the click that was meant\n * to focus the input. Which also fixes the slot's scope -- these are\n * DECORATIVE marks. An interactive trailing control (a password reveal, a\n * clear button) needs its own pointer events and its own accessible name, and\n * the census found no call site for one, so it is not built here.\n *\n * Logical properties throughout. The product app's eighteen hand-rolled\n * adornments are all `absolute left-*`, with `absolute start-*` at zero\n * occurrences, so every one of them puts the glyph on the wrong side under\n * RTL and lets the text run over it.\n */\nconst affixVariants = cva(\n [\n \"pointer-events-none absolute inset-y-0 flex items-center\",\n \"text-muted-foreground\",\n // Sized like Button's icon slots, and skipped when the caller sized the\n // glyph themselves. An svg with neither a width attribute nor a resolved\n // class renders at 0x0 and every position assertion still passes.\n \"[&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n ],\n {\n variants: {\n side: { start: \"\", end: \"\" },\n size: { sm: \"\", md: \"\", lg: \"\" },\n },\n // The glyph's inset is the field's own inline padding, so it lines up with\n // where the value would have started. That makes it a pair of axes, not\n // one: a small field at the medium inset puts the glyph under the text.\n compoundVariants: [\n { side: \"start\", size: \"sm\", class: \"start-(--density-control-pad-sm)\" },\n { side: \"start\", size: \"md\", class: \"start-(--density-control-pad-md)\" },\n { side: \"start\", size: \"lg\", class: \"start-(--density-control-pad-lg)\" },\n { side: \"end\", size: \"sm\", class: \"end-(--density-control-pad-sm)\" },\n { side: \"end\", size: \"md\", class: \"end-(--density-control-pad-md)\" },\n { side: \"end\", size: \"lg\", class: \"end-(--density-control-pad-lg)\" },\n ],\n defaultVariants: { size: \"md\" },\n },\n);\n\n/**\n * A single-line text field.\n *\n * @example\n * <Input type=\"email\" placeholder=\"you@example.com\" value={v} onChange={e => set(e.target.value)} />\n * <Input aria-label=\"Search\" startIcon={<SearchIcon aria-hidden />} />\n */\nexport interface InputProps extends Omit<AriaInputProps, \"className\" | \"style\" | \"size\"> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n /**\n * Control scale. Height and inline padding come from the same\n * `--density-control-*` tokens as Button, so a field and a button on one\n * rung are the same height.\n *\n * Type does not scale with it: the field stays at 16px everywhere, because\n * iOS Safari zooms the viewport when a focused input's text is smaller.\n *\n * This REPLACES the native `size` attribute, which React Aria inherits from\n * `InputHTMLAttributes` as a character count. No call site in the census\n * used it; set a width with `className` instead.\n *\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /**\n * Decorative glyph at the leading edge of the field.\n *\n * Mark it `aria-hidden` -- the field's label carries the accessible name,\n * and a second name source only makes it announce twice. It is sized at\n * `--icon-sm` unless it carries a `size-*` class of its own, positioned\n * from `--density-control-pad-md`, and the field's leading padding grows to\n * match so the value cannot run underneath it.\n *\n * The slot is `pointer-events: none`, so a click on the glyph focuses the\n * field. That makes it decorative by construction: an interactive\n * adornment -- a password reveal, a clear button -- is NOT this prop.\n *\n * Two named slots rather than one `icon` plus a position prop, matching\n * Button: a search field can legitimately carry a leading glyph AND a\n * trailing unit, which one slot cannot express.\n */\n startIcon?: ReactNode;\n /** Decorative glyph at the trailing edge. Same rules as {@link InputProps.startIcon}. */\n endIcon?: ReactNode;\n}\n\nexport function Input({ className, size = \"md\", startIcon, endIcon, ...props }: InputProps) {\n const input = (\n <AriaInput\n {...props}\n data-slot=\"input\"\n className={cn(\n inputVariants({\n size,\n hasStartIcon: startIcon !== undefined,\n hasEndIcon: endIcon !== undefined,\n }),\n className,\n )}\n />\n );\n\n // With neither slot used there is no wrapper AT ALL, so the DOM is\n // byte-identical to what shipped before this prop existed. That is what\n // keeps this additive for the 97 call sites that pass no glyph, and\n // NoIconsLeaveDomUnchanged is the assertion holding it true.\n if (startIcon === undefined && endIcon === undefined) return input;\n\n // An overlay, not a flex row. Moving the border onto this wrapper would\n // take the painted surface off [data-slot=\"input\"] -- the selector the\n // escape hatch is built on -- and would force the focus ring onto\n // :focus-within and the invalid border onto :has(), when React Aria\n // already puts data-focus-visible and data-invalid on the input itself.\n return (\n <span data-slot=\"input-wrapper\" className=\"relative block w-full\">\n {input}\n {startIcon === undefined ? null : (\n <span data-slot=\"input-start-icon\" className={affixVariants({ side: \"start\", size })}>\n {startIcon}\n </span>\n )}\n {endIcon === undefined ? null : (\n <span data-slot=\"input-end-icon\" className={affixVariants({ side: \"end\", size })}>\n {endIcon}\n </span>\n )}\n </span>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAa,gBAAgB,IAC3B;CACE,qBAAqB;CAOrB;CAIA;CACA;AACF,GACA;CACE,UAAU;;;;;;;;;;;;;;EAcR,MAAM;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EAGA,cAAc,EAAE,MAAM,GAAG;EACzB,YAAY,EAAE,MAAM,GAAG;CACzB;CACA,kBAAkB;EAQhB;GACE,MAAM;GACN,cAAc;GACd,OACE;EACJ;EACA;GACE,MAAM;GACN,cAAc;GACd,OACE;EACJ;EACA;GACE,MAAM;GACN,cAAc;GACd,OACE;EACJ;EACA;GACE,MAAM;GACN,YAAY;GACZ,OACE;EACJ;EACA;GACE,MAAM;GACN,YAAY;GACZ,OACE;EACJ;EACA;GACE,MAAM;GACN,YAAY;GACZ,OACE;EACJ;CACF;CACA,iBAAiB,EAAE,MAAM,KAAK;AAChC,CACF;;;;;;;;;;;;;;;;AAiBA,MAAM,gBAAgB,IACpB;CACE;CACA;CAIA;AACF,GACA;CACE,UAAU;EACR,MAAM;GAAE,OAAO;GAAI,KAAK;EAAG;EAC3B,MAAM;GAAE,IAAI;GAAI,IAAI;GAAI,IAAI;EAAG;CACjC;CAIA,kBAAkB;EAChB;GAAE,MAAM;GAAS,MAAM;GAAM,OAAO;EAAmC;EACvE;GAAE,MAAM;GAAS,MAAM;GAAM,OAAO;EAAmC;EACvE;GAAE,MAAM;GAAS,MAAM;GAAM,OAAO;EAAmC;EACvE;GAAE,MAAM;GAAO,MAAM;GAAM,OAAO;EAAiC;EACnE;GAAE,MAAM;GAAO,MAAM;GAAM,OAAO;EAAiC;EACnE;GAAE,MAAM;GAAO,MAAM;GAAM,OAAO;EAAiC;CACrE;CACA,iBAAiB,EAAE,MAAM,KAAK;AAChC,CACF;AAiDA,SAAgBA,QAAM,EAAE,WAAW,OAAO,MAAM,WAAW,SAAS,GAAG,SAAqB;CAC1F,MAAM,QACJ,oBAACC,OAAD;EACE,GAAI;EACJ,aAAU;EACV,WAAW,GACT,cAAc;GACZ;GACA,cAAc,cAAc,KAAA;GAC5B,YAAY,YAAY,KAAA;EAC1B,CAAC,GACD,SACF;CACD,CAAA;CAOH,IAAI,cAAc,KAAA,KAAa,YAAY,KAAA,GAAW,OAAO;CAO7D,OACE,qBAAC,QAAD;EAAM,aAAU;EAAgB,WAAU;EAA1C,UAAA;GACG;GACA,cAAc,KAAA,IAAY,OACzB,oBAAC,QAAD;IAAM,aAAU;IAAmB,WAAW,cAAc;KAAE,MAAM;KAAS;IAAK,CAAC;IAChF,UAAA;GACG,CAAA;GAEP,YAAY,KAAA,IAAY,OACvB,oBAAC,QAAD;IAAM,aAAU;IAAiB,WAAW,cAAc;KAAE,MAAM;KAAO;IAAK,CAAC;IAC5E,UAAA;GACG,CAAA;EAEJ;;AAEV"}
1
+ {"version":3,"file":"input.js","names":["Input","AriaInput"],"sources":["../../src/input/input.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport { Input as AriaInput, type InputProps as AriaInputProps } from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { fieldSurfaceVariants } from \"../lib/field-surface.js\";\n\n/**\n * Input, ported from the legacy shadcn component onto React Aria's Input.\n *\n * Census: 25 files, 51 uses -- the busiest control in the app after Button.\n * Props are `onChange` 36, `value` 35, `placeholder` 33, `id` 23, `type` 21.\n *\n * EVERY ONE OF THOSE 51 CALL SITES MIGRATES UNCHANGED. React Aria's `Input`\n * takes NATIVE props -- `disabled`, `aria-invalid`, `onChange` with a real\n * event -- not the `isDisabled`/`onChange(value)` shape the rest of React\n * Aria uses. It reads `InputContext` when one is present, so the same\n * component is a bare styled input today and composes inside a `TextField`\n * the moment one is wrapped around it. There is no rename to schedule.\n *\n * That is also why this is not `TextField`. Wrapping 51 call sites in a\n * field they do not have, to get a `onChange(value: string)` they did not\n * ask for, would be the migration doing work on the app's behalf.\n *\n * `border-input` becomes `border-border`: in the legacy theme `--input` is\n * character-for-character `--border` on light (`oklch(0.922 0 0)`), and the\n * dark values differ only in alpha. A fourth token role that turns out to\n * be an existing one, after `popover`, `accent` and the scrim.\n */\nexport const inputVariants = cva(\n [\n fieldSurfaceVariants(),\n\n // text-base always, not the legacy `text-base md:text-sm`. 16px is the\n // threshold below which iOS Safari zooms the viewport on focus, and the\n // legacy dropped to 14px at exactly the widths where that stops being a\n // phone -- which reads as a deliberate fix but leaves the field a\n // different size on either side of a breakpoint for no visual reason.\n \"text-base\",\n\n // type=\"file\" has one call site, which is not zero. Enough to keep it\n // from rendering as an unstyled user-agent button.\n \"file:inline-flex file:border-0 file:bg-transparent file:text-sm file:font-medium\",\n \"file:text-foreground\",\n ],\n {\n variants: {\n /**\n * Height and inline padding from the control scale, so `data-density`\n * moves a whole form at once. Same three names and the same tokens as\n * Button, because they sit in the same row: Button's `sm` is the app's\n * second most common at 167 call sites, and while the field was locked\n * to `md` the pair was 10px out of alignment with no supported fix.\n *\n * Type does NOT scale, which is the one place this axis deliberately\n * departs from Button's. iOS Safari zooms the viewport when a focused\n * field's text is under 16px, so `text-base` stays pinned in the base\n * at every rung -- see the note there. Shrinking the box is a layout\n * choice; shrinking the type is a bug on a phone.\n */\n size: {\n sm: \"h-(--density-control-h-sm) px-(--density-control-pad-sm)\",\n md: \"h-(--density-control-h-md) px-(--density-control-pad-md)\",\n lg: \"h-(--density-control-h-lg) px-(--density-control-pad-lg)\",\n },\n // Declared empty and filled by the compound variants below: the affix\n // inset depends on BOTH whether a slot is used and which rung it is on.\n hasStartIcon: { true: \"\" },\n hasEndIcon: { true: \"\" },\n },\n compoundVariants: [\n // The affix slots float ABOVE the field rather than sitting beside it\n // in a flex row, so the only thing keeping the value from running\n // underneath a glyph is this padding. It is the same three tokens that\n // place the glyph -- inline padding, icon box, control gap -- so the\n // text and the glyph move together across density AND size. A\n // hand-tuned `pl-9` at the call site can do neither, and the product\n // app has eighteen of those.\n {\n size: \"sm\",\n hasStartIcon: true,\n class:\n \"ps-[calc(var(--density-control-pad-sm)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"md\",\n hasStartIcon: true,\n class:\n \"ps-[calc(var(--density-control-pad-md)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"lg\",\n hasStartIcon: true,\n class:\n \"ps-[calc(var(--density-control-pad-lg)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"sm\",\n hasEndIcon: true,\n class:\n \"pe-[calc(var(--density-control-pad-sm)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"md\",\n hasEndIcon: true,\n class:\n \"pe-[calc(var(--density-control-pad-md)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n {\n size: \"lg\",\n hasEndIcon: true,\n class:\n \"pe-[calc(var(--density-control-pad-lg)_+_var(--icon-sm)_+_var(--density-control-gap))]\",\n },\n ],\n defaultVariants: { size: \"md\" },\n },\n);\n\n/**\n * An affix slot.\n *\n * `pointer-events-none` is load-bearing, not cosmetic: the slot is painted on\n * top of the field, so without it the glyph swallows the click that was meant\n * to focus the input. Which also fixes the slot's scope -- these are\n * DECORATIVE marks. An interactive trailing control (a password reveal, a\n * clear button) needs its own pointer events and its own accessible name, and\n * the census found no call site for one, so it is not built here.\n *\n * Logical properties throughout. The product app's eighteen hand-rolled\n * adornments are all `absolute left-*`, with `absolute start-*` at zero\n * occurrences, so every one of them puts the glyph on the wrong side under\n * RTL and lets the text run over it.\n */\nconst affixVariants = cva(\n [\n \"pointer-events-none absolute inset-y-0 flex items-center\",\n \"text-muted-foreground\",\n // Sized like Button's icon slots, and skipped when the caller sized the\n // glyph themselves. An svg with neither a width attribute nor a resolved\n // class renders at 0x0 and every position assertion still passes.\n \"[&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n ],\n {\n variants: {\n side: { start: \"\", end: \"\" },\n size: { sm: \"\", md: \"\", lg: \"\" },\n },\n // The glyph's inset is the field's own inline padding, so it lines up with\n // where the value would have started. That makes it a pair of axes, not\n // one: a small field at the medium inset puts the glyph under the text.\n compoundVariants: [\n { side: \"start\", size: \"sm\", class: \"start-(--density-control-pad-sm)\" },\n { side: \"start\", size: \"md\", class: \"start-(--density-control-pad-md)\" },\n { side: \"start\", size: \"lg\", class: \"start-(--density-control-pad-lg)\" },\n { side: \"end\", size: \"sm\", class: \"end-(--density-control-pad-sm)\" },\n { side: \"end\", size: \"md\", class: \"end-(--density-control-pad-md)\" },\n { side: \"end\", size: \"lg\", class: \"end-(--density-control-pad-lg)\" },\n ],\n defaultVariants: { size: \"md\" },\n },\n);\n\n/**\n * A single-line text field.\n *\n * @example\n * <Input type=\"email\" placeholder=\"you@example.com\" value={v} onChange={e => set(e.target.value)} />\n * <Input aria-label=\"Search\" startIcon={<SearchIcon aria-hidden />} />\n */\nexport interface InputProps extends Omit<AriaInputProps, \"className\" | \"style\" | \"size\"> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n /**\n * Control scale. Height and inline padding come from the same\n * `--density-control-*` tokens as Button, so a field and a button on one\n * rung are the same height.\n *\n * Type does not scale with it: the field stays at 16px everywhere, because\n * iOS Safari zooms the viewport when a focused input's text is smaller.\n *\n * This REPLACES the native `size` attribute, which React Aria inherits from\n * `InputHTMLAttributes` as a character count. No call site in the census\n * used it; set a width with `className` instead.\n *\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /**\n * Decorative glyph at the leading edge of the field.\n *\n * Mark it `aria-hidden` -- the field's label carries the accessible name,\n * and a second name source only makes it announce twice. It is sized at\n * `--icon-sm` unless it carries a `size-*` class of its own, positioned\n * from `--density-control-pad-md`, and the field's leading padding grows to\n * match so the value cannot run underneath it.\n *\n * The slot is `pointer-events: none`, so a click on the glyph focuses the\n * field. That makes it decorative by construction: an interactive\n * adornment -- a password reveal, a clear button -- is NOT this prop.\n *\n * Two named slots rather than one `icon` plus a position prop, matching\n * Button: a search field can legitimately carry a leading glyph AND a\n * trailing unit, which one slot cannot express.\n */\n startIcon?: ReactNode;\n /** Decorative glyph at the trailing edge. Same rules as {@link InputProps.startIcon}. */\n endIcon?: ReactNode;\n}\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n { className, size = \"md\", startIcon, endIcon, ...props },\n ref,\n) {\n const input = (\n <AriaInput\n // The ref goes on the INPUT, never on the wrapper span below. A ref\n // pointing at the wrapper is still non-null, so it satisfies the obvious\n // assertion while breaking `.focus()`, `.select()` and every\n // react-hook-form site -- but only at the call sites that pass an icon.\n // RefLandsOnTheInputNotTheWrapper is the guard.\n ref={ref}\n {...props}\n data-slot=\"input\"\n className={cn(\n inputVariants({\n size,\n hasStartIcon: startIcon !== undefined,\n hasEndIcon: endIcon !== undefined,\n }),\n className,\n )}\n />\n );\n\n // With neither slot used there is no wrapper AT ALL, so the DOM is\n // byte-identical to what shipped before this prop existed. That is what\n // keeps this additive for the 97 call sites that pass no glyph, and\n // NoIconsLeaveDomUnchanged is the assertion holding it true.\n if (startIcon === undefined && endIcon === undefined) return input;\n\n // An overlay, not a flex row. Moving the border onto this wrapper would\n // take the painted surface off [data-slot=\"input\"] -- the selector the\n // escape hatch is built on -- and would force the focus ring onto\n // :focus-within and the invalid border onto :has(), when React Aria\n // already puts data-focus-visible and data-invalid on the input itself.\n return (\n <span data-slot=\"input-wrapper\" className=\"relative block w-full\">\n {input}\n {startIcon === undefined ? null : (\n <span data-slot=\"input-start-icon\" className={affixVariants({ side: \"start\", size })}>\n {startIcon}\n </span>\n )}\n {endIcon === undefined ? null : (\n <span data-slot=\"input-end-icon\" className={affixVariants({ side: \"end\", size })}>\n {endIcon}\n </span>\n )}\n </span>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAa,gBAAgB,IAC3B;CACE,qBAAqB;CAOrB;CAIA;CACA;AACF,GACA;CACE,UAAU;;;;;;;;;;;;;;EAcR,MAAM;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EAGA,cAAc,EAAE,MAAM,GAAG;EACzB,YAAY,EAAE,MAAM,GAAG;CACzB;CACA,kBAAkB;EAQhB;GACE,MAAM;GACN,cAAc;GACd,OACE;EACJ;EACA;GACE,MAAM;GACN,cAAc;GACd,OACE;EACJ;EACA;GACE,MAAM;GACN,cAAc;GACd,OACE;EACJ;EACA;GACE,MAAM;GACN,YAAY;GACZ,OACE;EACJ;EACA;GACE,MAAM;GACN,YAAY;GACZ,OACE;EACJ;EACA;GACE,MAAM;GACN,YAAY;GACZ,OACE;EACJ;CACF;CACA,iBAAiB,EAAE,MAAM,KAAK;AAChC,CACF;;;;;;;;;;;;;;;;AAiBA,MAAM,gBAAgB,IACpB;CACE;CACA;CAIA;AACF,GACA;CACE,UAAU;EACR,MAAM;GAAE,OAAO;GAAI,KAAK;EAAG;EAC3B,MAAM;GAAE,IAAI;GAAI,IAAI;GAAI,IAAI;EAAG;CACjC;CAIA,kBAAkB;EAChB;GAAE,MAAM;GAAS,MAAM;GAAM,OAAO;EAAmC;EACvE;GAAE,MAAM;GAAS,MAAM;GAAM,OAAO;EAAmC;EACvE;GAAE,MAAM;GAAS,MAAM;GAAM,OAAO;EAAmC;EACvE;GAAE,MAAM;GAAO,MAAM;GAAM,OAAO;EAAiC;EACnE;GAAE,MAAM;GAAO,MAAM;GAAM,OAAO;EAAiC;EACnE;GAAE,MAAM;GAAO,MAAM;GAAM,OAAO;EAAiC;CACrE;CACA,iBAAiB,EAAE,MAAM,KAAK;AAChC,CACF;AAiDA,MAAaA,UAAQ,WAAyC,SAASA,QACrE,EAAE,WAAW,OAAO,MAAM,WAAW,SAAS,GAAG,SACjD,KACA;CACA,MAAM,QACJ,oBAACC,OAAD;EAMO;EACL,GAAI;EACJ,aAAU;EACV,WAAW,GACT,cAAc;GACZ;GACA,cAAc,cAAc,KAAA;GAC5B,YAAY,YAAY,KAAA;EAC1B,CAAC,GACD,SACF;CACD,CAAA;CAOH,IAAI,cAAc,KAAA,KAAa,YAAY,KAAA,GAAW,OAAO;CAO7D,OACE,qBAAC,QAAD;EAAM,aAAU;EAAgB,WAAU;EAA1C,UAAA;GACG;GACA,cAAc,KAAA,IAAY,OACzB,oBAAC,QAAD;IAAM,aAAU;IAAmB,WAAW,cAAc;KAAE,MAAM;KAAS;IAAK,CAAC;IAChF,UAAA;GACG,CAAA;GAEP,YAAY,KAAA,IAAY,OACvB,oBAAC,QAAD;IAAM,aAAU;IAAiB,WAAW,cAAc;KAAE,MAAM;KAAO;IAAK,CAAC;IAC5E,UAAA;GACG,CAAA;EAEJ;;AAEV,CAAC"}
package/dist/lib/cn.d.ts CHANGED
@@ -1,12 +1,62 @@
1
1
  import { ClassValue } from "clsx";
2
2
  //#region src/lib/cn.d.ts
3
3
  /**
4
- * Merge class names, with the caller's classes winning conflicts.
4
+ * Merge OUR classes, then append the consumer's UNTOUCHED.
5
5
  *
6
- * Every component runs its own classes through this with the consumer's
7
- * className last, so a consumer class replaces ours inside the class
8
- * attribute rather than fighting it on specificity.
6
+ * The consumer's `className` deliberately does not go through tailwind-merge.
7
+ *
8
+ * WHY. tailwind-merge resolves conflicts by class GROUP, and several of its
9
+ * groups are matched by validators that accept any token -- `text-color` takes
10
+ * anything shaped `text-<word>-<number>`, which is also the shape of a
11
+ * consuming app's own utilities. It cannot tell `text-red-500` from a custom
12
+ * `text-semibold-14`, so it treats them as conflicting and deletes one. There
13
+ * is no error, no warning and no type error; the element simply renders without
14
+ * the class the author wrote.
15
+ *
16
+ * Measured in the consuming app before this changed: 38 call sites passed a
17
+ * custom type-scale class and 14 lost it. Two of those flipped at runtime
18
+ * depending on a data value, which is why it went unnoticed for so long. A
19
+ * second, unrelated utility (`bg-sidebar-bg`) was being eaten the same way, so
20
+ * the defect is structural rather than specific to typography.
21
+ *
22
+ * The loss happens INSIDE the consumer's own string, not between theirs and
23
+ * ours -- a lone consumer class survives, because it comes last and
24
+ * tailwind-merge keeps the last of a group. It is a pair like
25
+ * `"text-semibold-14 text-white"` that loses its first half. So no amount of
26
+ * ordering fixes it: their string has to stay out of the merge entirely.
27
+ *
28
+ * WHY THAT IS SAFE. "The consumer always wins" is delivered twice over. This
29
+ * package ships entirely inside `@layer alfred`, and unlayered CSS beats every
30
+ * layered rule regardless of specificity (ARCHITECTURE.md section 5;
31
+ * PHASE-0.md). `apps/smoke` asserts exactly that on every build. tailwind-merge
32
+ * was belt-and-braces for something the cascade already handles -- and the
33
+ * braces were cutting.
34
+ *
35
+ * WHAT THIS IS NOT. It is not "stop merging". A consumer class that
36
+ * tailwind-merge genuinely understands still wins outright -- `max-w-96` over
37
+ * our `max-w-128` collapses to one class, exactly as before. Only the tokens
38
+ * the merge DISCARDED from their string come back, which is precisely the set
39
+ * it had no business discarding.
40
+ *
41
+ * The one behaviour change to know about: if a consumer passes two conflicting
42
+ * classes of their own (`className="p-2 p-4"`), both now reach the element and
43
+ * CSS picks by stylesheet order rather than by token order. Their string, their
44
+ * conflict -- and the alternative is deleting custom utilities, which is worse.
45
+ *
46
+ * @param ours classes this library authored -- merged against each other
47
+ * @param theirs the consumer's `className` -- merged, then anything the merge
48
+ * discarded from it is restored
49
+ */
50
+ export declare function cn(ours: ClassValue, theirs?: ClassValue): string;
51
+ /**
52
+ * Merge classes that are ALL ours.
53
+ *
54
+ * The same tailwind-merge behaviour `cn` used to have, for the call sites that
55
+ * combine several of this library's own class strings and never touch a
56
+ * consumer's. Keeping them on a separate function is what lets `cn` treat its
57
+ * second argument as foreign without guessing from argument position -- several
58
+ * internal call sites legitimately end with a library-owned conditional.
9
59
  */
10
- export declare function cn(...inputs: ClassValue[]): string;
60
+ export declare function cnAll(...inputs: ClassValue[]): string;
11
61
  //#endregion
12
62
  //# sourceMappingURL=cn.d.ts.map
package/dist/lib/cn.js CHANGED
@@ -2,16 +2,74 @@ import { clsx } from "clsx";
2
2
  import { twMerge } from "tailwind-merge";
3
3
  //#region src/lib/cn.ts
4
4
  /**
5
- * Merge class names, with the caller's classes winning conflicts.
5
+ * Merge OUR classes, then append the consumer's UNTOUCHED.
6
6
  *
7
- * Every component runs its own classes through this with the consumer's
8
- * className last, so a consumer class replaces ours inside the class
9
- * attribute rather than fighting it on specificity.
7
+ * The consumer's `className` deliberately does not go through tailwind-merge.
8
+ *
9
+ * WHY. tailwind-merge resolves conflicts by class GROUP, and several of its
10
+ * groups are matched by validators that accept any token -- `text-color` takes
11
+ * anything shaped `text-<word>-<number>`, which is also the shape of a
12
+ * consuming app's own utilities. It cannot tell `text-red-500` from a custom
13
+ * `text-semibold-14`, so it treats them as conflicting and deletes one. There
14
+ * is no error, no warning and no type error; the element simply renders without
15
+ * the class the author wrote.
16
+ *
17
+ * Measured in the consuming app before this changed: 38 call sites passed a
18
+ * custom type-scale class and 14 lost it. Two of those flipped at runtime
19
+ * depending on a data value, which is why it went unnoticed for so long. A
20
+ * second, unrelated utility (`bg-sidebar-bg`) was being eaten the same way, so
21
+ * the defect is structural rather than specific to typography.
22
+ *
23
+ * The loss happens INSIDE the consumer's own string, not between theirs and
24
+ * ours -- a lone consumer class survives, because it comes last and
25
+ * tailwind-merge keeps the last of a group. It is a pair like
26
+ * `"text-semibold-14 text-white"` that loses its first half. So no amount of
27
+ * ordering fixes it: their string has to stay out of the merge entirely.
28
+ *
29
+ * WHY THAT IS SAFE. "The consumer always wins" is delivered twice over. This
30
+ * package ships entirely inside `@layer alfred`, and unlayered CSS beats every
31
+ * layered rule regardless of specificity (ARCHITECTURE.md section 5;
32
+ * PHASE-0.md). `apps/smoke` asserts exactly that on every build. tailwind-merge
33
+ * was belt-and-braces for something the cascade already handles -- and the
34
+ * braces were cutting.
35
+ *
36
+ * WHAT THIS IS NOT. It is not "stop merging". A consumer class that
37
+ * tailwind-merge genuinely understands still wins outright -- `max-w-96` over
38
+ * our `max-w-128` collapses to one class, exactly as before. Only the tokens
39
+ * the merge DISCARDED from their string come back, which is precisely the set
40
+ * it had no business discarding.
41
+ *
42
+ * The one behaviour change to know about: if a consumer passes two conflicting
43
+ * classes of their own (`className="p-2 p-4"`), both now reach the element and
44
+ * CSS picks by stylesheet order rather than by token order. Their string, their
45
+ * conflict -- and the alternative is deleting custom utilities, which is worse.
46
+ *
47
+ * @param ours classes this library authored -- merged against each other
48
+ * @param theirs the consumer's `className` -- merged, then anything the merge
49
+ * discarded from it is restored
50
+ */
51
+ function cn(ours, theirs) {
52
+ const mine = clsx(ours);
53
+ const yours = clsx(theirs);
54
+ if (!yours) return twMerge(mine);
55
+ const merged = twMerge(`${mine} ${yours}`);
56
+ const kept = new Set(merged.split(" "));
57
+ const rescued = yours.split(" ").filter((token) => token && !kept.has(token));
58
+ return rescued.length === 0 ? merged : `${merged} ${rescued.join(" ")}`;
59
+ }
60
+ /**
61
+ * Merge classes that are ALL ours.
62
+ *
63
+ * The same tailwind-merge behaviour `cn` used to have, for the call sites that
64
+ * combine several of this library's own class strings and never touch a
65
+ * consumer's. Keeping them on a separate function is what lets `cn` treat its
66
+ * second argument as foreign without guessing from argument position -- several
67
+ * internal call sites legitimately end with a library-owned conditional.
10
68
  */
11
- function cn(...inputs) {
69
+ function cnAll(...inputs) {
12
70
  return twMerge(clsx(inputs));
13
71
  }
14
72
  //#endregion
15
- export { cn };
73
+ export { cn, cnAll };
16
74
 
17
75
  //# sourceMappingURL=cn.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cn.js","names":[],"sources":["../../src/lib/cn.ts"],"sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names, with the caller's classes winning conflicts.\n *\n * Every component runs its own classes through this with the consumer's\n * className last, so a consumer class replaces ours inside the class\n * attribute rather than fighting it on specificity.\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n"],"mappings":";;;;;;;;;;AAUA,SAAgB,GAAG,GAAG,QAA8B;CAClD,OAAO,QAAQ,KAAK,MAAM,CAAC;AAC7B"}
1
+ {"version":3,"file":"cn.js","names":[],"sources":["../../src/lib/cn.ts"],"sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge OUR classes, then append the consumer's UNTOUCHED.\n *\n * The consumer's `className` deliberately does not go through tailwind-merge.\n *\n * WHY. tailwind-merge resolves conflicts by class GROUP, and several of its\n * groups are matched by validators that accept any token -- `text-color` takes\n * anything shaped `text-<word>-<number>`, which is also the shape of a\n * consuming app's own utilities. It cannot tell `text-red-500` from a custom\n * `text-semibold-14`, so it treats them as conflicting and deletes one. There\n * is no error, no warning and no type error; the element simply renders without\n * the class the author wrote.\n *\n * Measured in the consuming app before this changed: 38 call sites passed a\n * custom type-scale class and 14 lost it. Two of those flipped at runtime\n * depending on a data value, which is why it went unnoticed for so long. A\n * second, unrelated utility (`bg-sidebar-bg`) was being eaten the same way, so\n * the defect is structural rather than specific to typography.\n *\n * The loss happens INSIDE the consumer's own string, not between theirs and\n * ours -- a lone consumer class survives, because it comes last and\n * tailwind-merge keeps the last of a group. It is a pair like\n * `\"text-semibold-14 text-white\"` that loses its first half. So no amount of\n * ordering fixes it: their string has to stay out of the merge entirely.\n *\n * WHY THAT IS SAFE. \"The consumer always wins\" is delivered twice over. This\n * package ships entirely inside `@layer alfred`, and unlayered CSS beats every\n * layered rule regardless of specificity (ARCHITECTURE.md section 5;\n * PHASE-0.md). `apps/smoke` asserts exactly that on every build. tailwind-merge\n * was belt-and-braces for something the cascade already handles -- and the\n * braces were cutting.\n *\n * WHAT THIS IS NOT. It is not \"stop merging\". A consumer class that\n * tailwind-merge genuinely understands still wins outright -- `max-w-96` over\n * our `max-w-128` collapses to one class, exactly as before. Only the tokens\n * the merge DISCARDED from their string come back, which is precisely the set\n * it had no business discarding.\n *\n * The one behaviour change to know about: if a consumer passes two conflicting\n * classes of their own (`className=\"p-2 p-4\"`), both now reach the element and\n * CSS picks by stylesheet order rather than by token order. Their string, their\n * conflict -- and the alternative is deleting custom utilities, which is worse.\n *\n * @param ours classes this library authored -- merged against each other\n * @param theirs the consumer's `className` -- merged, then anything the merge\n * discarded from it is restored\n */\nexport function cn(ours: ClassValue, theirs?: ClassValue): string {\n const mine = clsx(ours);\n const yours = clsx(theirs);\n if (!yours) return twMerge(mine);\n\n // Merge normally first, so a consumer class that tailwind-merge DOES\n // understand still beats ours -- `max-w-96` over our `max-w-128` resolves to\n // one class, as it always did.\n const merged = twMerge(`${mine} ${yours}`);\n\n // Then put back anything of theirs the merge ate. Only their tokens are\n // restored, never ours, so this cannot resurrect a class the consumer\n // deliberately overrode.\n const kept = new Set(merged.split(\" \"));\n const rescued = yours.split(\" \").filter((token) => token && !kept.has(token));\n\n return rescued.length === 0 ? merged : `${merged} ${rescued.join(\" \")}`;\n}\n\n/**\n * Merge classes that are ALL ours.\n *\n * The same tailwind-merge behaviour `cn` used to have, for the call sites that\n * combine several of this library's own class strings and never touch a\n * consumer's. Keeping them on a separate function is what lets `cn` treat its\n * second argument as foreign without guessing from argument position -- several\n * internal call sites legitimately end with a library-owned conditional.\n */\nexport function cnAll(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,SAAgB,GAAG,MAAkB,QAA6B;CAChE,MAAM,OAAO,KAAK,IAAI;CACtB,MAAM,QAAQ,KAAK,MAAM;CACzB,IAAI,CAAC,OAAO,OAAO,QAAQ,IAAI;CAK/B,MAAM,SAAS,QAAQ,GAAG,KAAK,GAAG,OAAO;CAKzC,MAAM,OAAO,IAAI,IAAI,OAAO,MAAM,GAAG,CAAC;CACtC,MAAM,UAAU,MAAM,MAAM,GAAG,CAAC,CAAC,QAAQ,UAAU,SAAS,CAAC,KAAK,IAAI,KAAK,CAAC;CAE5E,OAAO,QAAQ,WAAW,IAAI,SAAS,GAAG,OAAO,GAAG,QAAQ,KAAK,GAAG;AACtE;;;;;;;;;;AAWA,SAAgB,MAAM,GAAG,QAA8B;CACrD,OAAO,QAAQ,KAAK,MAAM,CAAC;AAC7B"}
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { cn } from "../lib/cn.js";
2
+ import { cn, cnAll } from "../lib/cn.js";
3
3
  import { cva } from "class-variance-authority";
4
4
  import { Label, ProgressBar } from "react-aria-components";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -53,10 +53,10 @@ function Progress({ label, labelHidden, showValue, tone, trackClassName, fillCla
53
53
  className: cn(progressVariants(), className),
54
54
  children: ({ percentage, isIndeterminate, valueText }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
55
55
  "data-slot": "progress-header",
56
- className: cn(showValue && "flex items-baseline justify-between gap-2"),
56
+ className: cnAll(showValue && "flex items-baseline justify-between gap-2"),
57
57
  children: [/* @__PURE__ */ jsx(Label, {
58
58
  "data-slot": "progress-label",
59
- className: cn("text-sm leading-(--leading-normal) text-foreground", labelHidden && "sr-only"),
59
+ className: cnAll("text-sm leading-(--leading-normal) text-foreground", labelHidden && "sr-only"),
60
60
  children: label
61
61
  }), showValue ? /* @__PURE__ */ jsx("span", {
62
62
  "data-slot": "progress-value",
@@ -1 +1 @@
1
- {"version":3,"file":"progress.js","names":["AriaProgressBar","AriaLabel"],"sources":["../../src/progress/progress.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\nimport {\n Label as AriaLabel,\n ProgressBar as AriaProgressBar,\n type ProgressBarProps as AriaProgressBarProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\n\n/**\n * Progress, ported from the legacy shadcn/Radix component.\n *\n * Census: seven files import it and only TWO render anything --\n * `ResponseOption.tsx` and `LiveQA.tsx`. Both pass `value` and\n * `className` and nothing else.\n *\n * `label` IS REQUIRED, and that is the point. Neither live call site\n * passes `aria-label`, `aria-labelledby` or an `id` a label targets, and\n * `role=\"progressbar\"` does not take its name from content -- so both\n * rendered progress bars are announced as an unnamed progressbar. React\n * Aria supplies the plumbing for three naming routes but no name of its\n * own: `<ProgressBar value={60}/>` is just as nameless there. Making the\n * prop required is what turns it into something you cannot forget.\n *\n * The migration plan said this component \"adds the missing\n * aria-valuenow\". That is wrong -- Radix already supplies `aria-valuenow`,\n * `aria-valuemin`, `aria-valuemax` and `aria-valuetext`. The missing thing\n * was always the NAME.\n *\n * THE COLOURS ARE NOT THE LEGACY'S. Its track is `bg-primary/20`, an alpha\n * fill that composites against unknown page content and therefore has no\n * fixed contrast pair at all -- over white it is roughly 1.05:1, an\n * invisible boundary. Its indicator is `bg-primary`, 2.44:1 on the page.\n * Here the track is `muted` with a `border` boundary and the fill is\n * `primary-strong`, which puts the fill against the track at 4.61:1 and\n * the track against the page at 5.25:1.\n *\n * Both are in the build-failing contrast contract.\n */\nexport const progressVariants = cva([\"flex w-full flex-col gap-1.5 box-border font-sans\"]);\n\nexport const progressTrackVariants = cva([\n \"relative h-2 w-full overflow-hidden rounded-pill box-border\",\n // muted, NOT `border` as the Switch uses. A switch has no fill; a\n // progress bar does, and primary-strong on a `border` track is 1.01:1 --\n // the two halves are the same grey and the value disappears.\n \"bg-muted border-(length:--border-width) border-border\",\n]);\n\nexport const progressFillVariants = cva(\n [\n \"h-full rounded-pill\",\n \"motion-safe:transition-[width] motion-safe:duration-(--duration-base) motion-safe:ease-standard\",\n ],\n {\n variants: {\n /*\n * What the value MEANS, which for a progress bar is usually a score\n * rather than a completion. The product's bar carried exactly this axis\n * (`default`/`success`/`warning`/`error`/`info`) and reached for it to\n * colour performance metrics.\n *\n * `-strong` throughout: the fill sits on the `muted` track and has to\n * clear the 3:1 non-text floor of WCAG 1.4.11 against it, which is the\n * same reason the track is muted rather than `border` -- primary-strong\n * on a `border` track measures 1.01:1 and the value disappears.\n */\n tone: {\n neutral: \"bg-primary-strong\",\n success: \"bg-success-strong\",\n warning: \"bg-warning-strong\",\n danger: \"bg-destructive-strong\",\n info: \"bg-info-strong\",\n },\n },\n defaultVariants: { tone: \"neutral\" },\n },\n);\n\n/**\n * A determinate or indeterminate progress bar.\n *\n * @example\n * <Progress label=\"Upload\" value={62} />\n * <Progress label=\"Loading results\" />\n */\nexport interface ProgressProps\n extends Omit<AriaProgressBarProps, \"className\" | \"style\" | \"children\">,\n VariantProps<typeof progressFillVariants> {\n /**\n * What is progressing, and the bar's accessible name.\n *\n * **Required.** Both legacy call sites render an unnamed progressbar.\n * Pass `labelHidden` when the surrounding UI already says it.\n */\n label: ReactNode;\n /**\n * Render the label for screen readers only.\n *\n * Both legacy call sites sit beside visible text that was never\n * associated with the bar -- this is how that text becomes the name\n * without printing it twice.\n */\n labelHidden?: boolean;\n /**\n * Print the value beside the label.\n *\n * The text itself comes from React Aria, which already formats it from\n * `value`/`minValue`/`maxValue` and honours `formatOptions` and\n * `valueLabel` -- so a percentage, a currency or \"3 of 7\" are all reachable\n * without a second prop here. It was never RENDERED before: the bar\n * announced its value to assistive tech and showed a sighted reader nothing.\n */\n showValue?: boolean;\n /** Extra classes for the track. */\n trackClassName?: string;\n /** Extra classes for the fill -- a per-call-site colour the tone axis cannot express. */\n fillClassName?: string;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport function Progress({\n label,\n labelHidden,\n showValue,\n tone,\n trackClassName,\n fillClassName,\n className,\n ...props\n}: ProgressProps) {\n return (\n <AriaProgressBar\n {...props}\n data-slot=\"progress\"\n className={cn(progressVariants(), className)}\n >\n {({ percentage, isIndeterminate, valueText }) => (\n <>\n {/*\n The row exists only when the value is printed. Wrapping the label\n in a flex row unconditionally would change the label's box for\n every existing consumer to no purpose.\n */}\n <div\n data-slot=\"progress-header\"\n className={cn(showValue && \"flex items-baseline justify-between gap-2\")}\n >\n <AriaLabel\n data-slot=\"progress-label\"\n className={cn(\n \"text-sm leading-(--leading-normal) text-foreground\",\n labelHidden && \"sr-only\",\n )}\n >\n {label}\n </AriaLabel>\n {showValue ? (\n <span\n data-slot=\"progress-value\"\n // aria-hidden: React Aria already announces the value on the\n // progressbar itself. Without this a screen reader reads it\n // twice, once as the widget's value and once as loose text.\n aria-hidden=\"true\"\n className=\"text-sm leading-(--leading-normal) text-muted-foreground tabular-nums\"\n >\n {valueText}\n </span>\n ) : null}\n </div>\n <div\n data-slot=\"progress-track\"\n className={cn(progressTrackVariants(), trackClassName)}\n >\n {/*\n width, not the legacy translateX(-(100 - value)%). That\n overflowed for value > 100 -- a positive translate slid the\n indicator off to the right, while the announced value stayed\n whatever was passed. React Aria clamps the value, so the two\n can no longer disagree.\n\n An indeterminate bar renders no fill rather than the\n legacy's silent 0%: `value={undefined}` became `(value || 0)`\n there, so a sighted user saw \"0% done\" while a screen reader\n said \"indeterminate\".\n */}\n <div\n data-slot=\"progress-fill\"\n data-indeterminate={isIndeterminate || undefined}\n className={cn(progressFillVariants({ tone }), fillClassName)}\n style={{ width: isIndeterminate ? \"0%\" : `${percentage ?? 0}%` }}\n />\n </div>\n </>\n )}\n </AriaProgressBar>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAa,mBAAmB,IAAI,CAAC,mDAAmD,CAAC;AAEzF,MAAa,wBAAwB,IAAI,CACvC,+DAIA,uDACF,CAAC;AAED,MAAa,uBAAuB,IAClC,CACE,uBACA,iGACF,GACA;CACE,UAAU,EAYR,MAAM;EACJ,SAAS;EACT,SAAS;EACT,SAAS;EACT,QAAQ;EACR,MAAM;CACR,EACF;CACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CACF;AA6CA,SAAgB,SAAS,EACvB,OACA,aACA,WACA,MACA,gBACA,eACA,WACA,GAAG,SACa;CAChB,OACE,oBAACA,aAAD;EACE,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,iBAAiB,GAAG,SAAS;EAEzC,WAAA,EAAE,YAAY,iBAAiB,gBAC/B,qBAAA,UAAA,EAAA,UAAA,CAME,qBAAC,OAAD;GACE,aAAU;GACV,WAAW,GAAG,aAAa,2CAA2C;GAFxE,UAAA,CAIE,oBAACC,OAAD;IACE,aAAU;IACV,WAAW,GACT,sDACA,eAAe,SACjB;IAEC,UAAA;GACQ,CAAA,GACV,YACC,oBAAC,QAAD;IACE,aAAU;IAIV,eAAY;IACZ,WAAU;IAET,UAAA;GACG,CAAA,IACJ,IACD;EACL,CAAA,GAAA,oBAAC,OAAD;GACE,aAAU;GACV,WAAW,GAAG,sBAAsB,GAAG,cAAc;GAcrD,UAAA,oBAAC,OAAD;IACE,aAAU;IACV,sBAAoB,mBAAmB,KAAA;IACvC,WAAW,GAAG,qBAAqB,EAAE,KAAK,CAAC,GAAG,aAAa;IAC3D,OAAO,EAAE,OAAO,kBAAkB,OAAO,GAAG,cAAc,EAAE,GAAG;GAChE,CAAA;EACE,CAAA,CACL,EAAA,CAAA;CAEW,CAAA;AAErB"}
1
+ {"version":3,"file":"progress.js","names":["AriaProgressBar","AriaLabel"],"sources":["../../src/progress/progress.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\nimport {\n Label as AriaLabel,\n ProgressBar as AriaProgressBar,\n type ProgressBarProps as AriaProgressBarProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\n\n/**\n * Progress, ported from the legacy shadcn/Radix component.\n *\n * Census: seven files import it and only TWO render anything --\n * `ResponseOption.tsx` and `LiveQA.tsx`. Both pass `value` and\n * `className` and nothing else.\n *\n * `label` IS REQUIRED, and that is the point. Neither live call site\n * passes `aria-label`, `aria-labelledby` or an `id` a label targets, and\n * `role=\"progressbar\"` does not take its name from content -- so both\n * rendered progress bars are announced as an unnamed progressbar. React\n * Aria supplies the plumbing for three naming routes but no name of its\n * own: `<ProgressBar value={60}/>` is just as nameless there. Making the\n * prop required is what turns it into something you cannot forget.\n *\n * The migration plan said this component \"adds the missing\n * aria-valuenow\". That is wrong -- Radix already supplies `aria-valuenow`,\n * `aria-valuemin`, `aria-valuemax` and `aria-valuetext`. The missing thing\n * was always the NAME.\n *\n * THE COLOURS ARE NOT THE LEGACY'S. Its track is `bg-primary/20`, an alpha\n * fill that composites against unknown page content and therefore has no\n * fixed contrast pair at all -- over white it is roughly 1.05:1, an\n * invisible boundary. Its indicator is `bg-primary`, 2.44:1 on the page.\n * Here the track is `muted` with a `border` boundary and the fill is\n * `primary-strong`, which puts the fill against the track at 4.61:1 and\n * the track against the page at 5.25:1.\n *\n * Both are in the build-failing contrast contract.\n */\nexport const progressVariants = cva([\"flex w-full flex-col gap-1.5 box-border font-sans\"]);\n\nexport const progressTrackVariants = cva([\n \"relative h-2 w-full overflow-hidden rounded-pill box-border\",\n // muted, NOT `border` as the Switch uses. A switch has no fill; a\n // progress bar does, and primary-strong on a `border` track is 1.01:1 --\n // the two halves are the same grey and the value disappears.\n \"bg-muted border-(length:--border-width) border-border\",\n]);\n\nexport const progressFillVariants = cva(\n [\n \"h-full rounded-pill\",\n \"motion-safe:transition-[width] motion-safe:duration-(--duration-base) motion-safe:ease-standard\",\n ],\n {\n variants: {\n /*\n * What the value MEANS, which for a progress bar is usually a score\n * rather than a completion. The product's bar carried exactly this axis\n * (`default`/`success`/`warning`/`error`/`info`) and reached for it to\n * colour performance metrics.\n *\n * `-strong` throughout: the fill sits on the `muted` track and has to\n * clear the 3:1 non-text floor of WCAG 1.4.11 against it, which is the\n * same reason the track is muted rather than `border` -- primary-strong\n * on a `border` track measures 1.01:1 and the value disappears.\n */\n tone: {\n neutral: \"bg-primary-strong\",\n success: \"bg-success-strong\",\n warning: \"bg-warning-strong\",\n danger: \"bg-destructive-strong\",\n info: \"bg-info-strong\",\n },\n },\n defaultVariants: { tone: \"neutral\" },\n },\n);\n\n/**\n * A determinate or indeterminate progress bar.\n *\n * @example\n * <Progress label=\"Upload\" value={62} />\n * <Progress label=\"Loading results\" />\n */\nexport interface ProgressProps\n extends Omit<AriaProgressBarProps, \"className\" | \"style\" | \"children\">,\n VariantProps<typeof progressFillVariants> {\n /**\n * What is progressing, and the bar's accessible name.\n *\n * **Required.** Both legacy call sites render an unnamed progressbar.\n * Pass `labelHidden` when the surrounding UI already says it.\n */\n label: ReactNode;\n /**\n * Render the label for screen readers only.\n *\n * Both legacy call sites sit beside visible text that was never\n * associated with the bar -- this is how that text becomes the name\n * without printing it twice.\n */\n labelHidden?: boolean;\n /**\n * Print the value beside the label.\n *\n * The text itself comes from React Aria, which already formats it from\n * `value`/`minValue`/`maxValue` and honours `formatOptions` and\n * `valueLabel` -- so a percentage, a currency or \"3 of 7\" are all reachable\n * without a second prop here. It was never RENDERED before: the bar\n * announced its value to assistive tech and showed a sighted reader nothing.\n */\n showValue?: boolean;\n /** Extra classes for the track. */\n trackClassName?: string;\n /** Extra classes for the fill -- a per-call-site colour the tone axis cannot express. */\n fillClassName?: string;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport function Progress({\n label,\n labelHidden,\n showValue,\n tone,\n trackClassName,\n fillClassName,\n className,\n ...props\n}: ProgressProps) {\n return (\n <AriaProgressBar\n {...props}\n data-slot=\"progress\"\n className={cn(progressVariants(), className)}\n >\n {({ percentage, isIndeterminate, valueText }) => (\n <>\n {/*\n The row exists only when the value is printed. Wrapping the label\n in a flex row unconditionally would change the label's box for\n every existing consumer to no purpose.\n */}\n <div\n data-slot=\"progress-header\"\n className={cnAll(showValue && \"flex items-baseline justify-between gap-2\")}\n >\n <AriaLabel\n data-slot=\"progress-label\"\n className={cnAll(\n \"text-sm leading-(--leading-normal) text-foreground\",\n labelHidden && \"sr-only\",\n )}\n >\n {label}\n </AriaLabel>\n {showValue ? (\n <span\n data-slot=\"progress-value\"\n // aria-hidden: React Aria already announces the value on the\n // progressbar itself. Without this a screen reader reads it\n // twice, once as the widget's value and once as loose text.\n aria-hidden=\"true\"\n className=\"text-sm leading-(--leading-normal) text-muted-foreground tabular-nums\"\n >\n {valueText}\n </span>\n ) : null}\n </div>\n <div\n data-slot=\"progress-track\"\n className={cn(progressTrackVariants(), trackClassName)}\n >\n {/*\n width, not the legacy translateX(-(100 - value)%). That\n overflowed for value > 100 -- a positive translate slid the\n indicator off to the right, while the announced value stayed\n whatever was passed. React Aria clamps the value, so the two\n can no longer disagree.\n\n An indeterminate bar renders no fill rather than the\n legacy's silent 0%: `value={undefined}` became `(value || 0)`\n there, so a sighted user saw \"0% done\" while a screen reader\n said \"indeterminate\".\n */}\n <div\n data-slot=\"progress-fill\"\n data-indeterminate={isIndeterminate || undefined}\n className={cn(progressFillVariants({ tone }), fillClassName)}\n style={{ width: isIndeterminate ? \"0%\" : `${percentage ?? 0}%` }}\n />\n </div>\n </>\n )}\n </AriaProgressBar>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAa,mBAAmB,IAAI,CAAC,mDAAmD,CAAC;AAEzF,MAAa,wBAAwB,IAAI,CACvC,+DAIA,uDACF,CAAC;AAED,MAAa,uBAAuB,IAClC,CACE,uBACA,iGACF,GACA;CACE,UAAU,EAYR,MAAM;EACJ,SAAS;EACT,SAAS;EACT,SAAS;EACT,QAAQ;EACR,MAAM;CACR,EACF;CACA,iBAAiB,EAAE,MAAM,UAAU;AACrC,CACF;AA6CA,SAAgB,SAAS,EACvB,OACA,aACA,WACA,MACA,gBACA,eACA,WACA,GAAG,SACa;CAChB,OACE,oBAACA,aAAD;EACE,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,iBAAiB,GAAG,SAAS;EAEzC,WAAA,EAAE,YAAY,iBAAiB,gBAC/B,qBAAA,UAAA,EAAA,UAAA,CAME,qBAAC,OAAD;GACE,aAAU;GACV,WAAW,MAAM,aAAa,2CAA2C;GAF3E,UAAA,CAIE,oBAACC,OAAD;IACE,aAAU;IACV,WAAW,MACT,sDACA,eAAe,SACjB;IAEC,UAAA;GACQ,CAAA,GACV,YACC,oBAAC,QAAD;IACE,aAAU;IAIV,eAAY;IACZ,WAAU;IAET,UAAA;GACG,CAAA,IACJ,IACD;EACL,CAAA,GAAA,oBAAC,OAAD;GACE,aAAU;GACV,WAAW,GAAG,sBAAsB,GAAG,cAAc;GAcrD,UAAA,oBAAC,OAAD;IACE,aAAU;IACV,sBAAoB,mBAAmB,KAAA;IACvC,WAAW,GAAG,qBAAqB,EAAE,KAAK,CAAC,GAAG,aAAa;IAC3D,OAAO,EAAE,OAAO,kBAAkB,OAAO,GAAG,cAAc,EAAE,GAAG;GAChE,CAAA;EACE,CAAA,CACL,EAAA,CAAA;CAEW,CAAA;AAErB"}
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { cn } from "../lib/cn.js";
2
+ import { cn, cnAll } from "../lib/cn.js";
3
3
  import { selectionLabelVariants, selectionRingVariants } from "../lib/selection-control.js";
4
4
  import { cva } from "class-variance-authority";
5
5
  import { Radio, RadioGroup } from "react-aria-components";
@@ -44,7 +44,7 @@ function Radio$1({ className, children, ...props }) {
44
44
  return /* @__PURE__ */ jsx(Radio, {
45
45
  ...props,
46
46
  "data-slot": "radio",
47
- className: cn(selectionLabelVariants(), "justify-start", className),
47
+ className: cn(cnAll(selectionLabelVariants(), "justify-start"), className),
48
48
  children: ({ isSelected }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
49
49
  "aria-hidden": "true",
50
50
  "data-slot": "radio-dot",
@@ -1 +1 @@
1
- {"version":3,"file":"radio-group.js","names":["RadioGroup","AriaRadioGroup","Radio","AriaRadio"],"sources":["../../src/radio-group/radio-group.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\nimport {\n Radio as AriaRadio,\n RadioGroup as AriaRadioGroup,\n type RadioGroupProps as AriaRadioGroupProps,\n type RadioProps as AriaRadioProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { selectionLabelVariants, selectionRingVariants } from \"../lib/selection-control.js\";\n\n/**\n * RadioGroup, ported from the legacy shadcn/Radix component onto React Aria.\n *\n * Census: 1 file, 3 uses -- `RadioGroup` 1, `RadioGroupItem` 2. The\n * smallest real surface in the migration, so the port is deliberately plain.\n *\n * THE LABEL MOVES INSIDE THE ITEM. Radix's `RadioGroupItem` is a bare\n * button and the caller pairs it with a separate `<Label htmlFor>`; React\n * Aria's `Radio` takes its label as `children` and wires the association\n * itself. That is the one rename a call site has to make, and it removes\n * the id bookkeeping.\n *\n * `border-primary` becomes `border-border` for the same reason it did on\n * Checkbox: brand orange is 2.44:1 on white and cannot legally be the only\n * thing describing a control's edge (WCAG 1.4.11). Selected reuses\n * Checkbox's exact pair -- `bg-primary` with a `primary-strong` boundary --\n * so a radio and a checkbox in the same form agree.\n */\nexport const radioGroupVariants = cva([\n \"flex flex-col gap-2\",\n \"box-border font-sans\",\n]);\n\nexport const radioVariants = cva([\n \"inline-flex size-4 shrink-0 items-center justify-center\",\n \"rounded-full border-(length:--border-width) box-border\",\n \"bg-background border-border\",\n \"motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard\",\n \"group-data-[selected]:bg-primary group-data-[selected]:border-primary-strong\",\n \"group-data-[selected]:text-primary-foreground\",\n selectionRingVariants(),\n]);\n\n/**\n * A set of mutually exclusive choices.\n *\n * @example\n * <RadioGroup value={plan} onChange={setPlan} aria-label=\"Plan\">\n * <Radio value=\"free\">Free</Radio>\n * <Radio value=\"pro\">Pro</Radio>\n * </RadioGroup>\n */\nexport interface RadioGroupProps extends Omit<AriaRadioGroupProps, \"className\" | \"style\"> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport function RadioGroup({ className, ...props }: RadioGroupProps) {\n return (\n <AriaRadioGroup\n {...props}\n data-slot=\"radio-group\"\n className={cn(radioGroupVariants(), className)}\n />\n );\n}\n\n/**\n * One choice.\n *\n * @example\n * <Radio value=\"pro\">Pro</Radio>\n */\nexport interface RadioProps extends Omit<AriaRadioProps, \"className\" | \"style\" | \"children\"> {\n /** The label, and this option's accessible name. */\n children?: ReactNode;\n /** Extra classes for the label. Merged last, so they win conflicts. */\n className?: string;\n}\n\nexport function Radio({ className, children, ...props }: RadioProps) {\n return (\n <AriaRadio\n {...props}\n data-slot=\"radio\"\n // justify-start, overriding the shared label's centring: a radio sits\n // in a vertical list where every label must start on the same line,\n // not centre itself in a 24px box.\n className={cn(selectionLabelVariants(), \"justify-start\", className)}\n >\n {({ isSelected }) => (\n <>\n <span aria-hidden=\"true\" data-slot=\"radio-dot\" className={radioVariants()}>\n {isSelected ? (\n // Rendered in JS rather than toggled with CSS, so the\n // precedence is explicit and a test can read it from the DOM.\n <span data-slot=\"radio-indicator\" className=\"size-1.5 rounded-full bg-current\" />\n ) : null}\n </span>\n {children == null ? null : <span data-slot=\"radio-label\">{children}</span>}\n </>\n )}\n </AriaRadio>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAa,qBAAqB,IAAI,CACpC,uBACA,sBACF,CAAC;AAED,MAAa,gBAAgB,IAAI;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA,sBAAsB;AACxB,CAAC;AAgBD,SAAgBA,aAAW,EAAE,WAAW,GAAG,SAA0B;CACnE,OACE,oBAACC,YAAD;EACE,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,mBAAmB,GAAG,SAAS;CAC9C,CAAA;AAEL;AAeA,SAAgBC,QAAM,EAAE,WAAW,UAAU,GAAG,SAAqB;CACnE,OACE,oBAACC,OAAD;EACE,GAAI;EACJ,aAAU;EAIV,WAAW,GAAG,uBAAuB,GAAG,iBAAiB,SAAS;EAEhE,WAAA,EAAE,iBACF,qBAAA,UAAA,EAAA,UAAA,CACE,oBAAC,QAAD;GAAM,eAAY;GAAO,aAAU;GAAY,WAAW,cAAc;GACrE,UAAA,aAGC,oBAAC,QAAD;IAAM,aAAU;IAAkB,WAAU;GAAoC,CAAA,IAC9E;EACA,CAAA,GACL,YAAY,OAAO,OAAO,oBAAC,QAAD;GAAM,aAAU;GAAe;EAAe,CAAA,CACzE,EAAA,CAAA;CAEK,CAAA;AAEf"}
1
+ {"version":3,"file":"radio-group.js","names":["RadioGroup","AriaRadioGroup","Radio","AriaRadio"],"sources":["../../src/radio-group/radio-group.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\nimport {\n Radio as AriaRadio,\n RadioGroup as AriaRadioGroup,\n type RadioGroupProps as AriaRadioGroupProps,\n type RadioProps as AriaRadioProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { selectionLabelVariants, selectionRingVariants } from \"../lib/selection-control.js\";\n\n/**\n * RadioGroup, ported from the legacy shadcn/Radix component onto React Aria.\n *\n * Census: 1 file, 3 uses -- `RadioGroup` 1, `RadioGroupItem` 2. The\n * smallest real surface in the migration, so the port is deliberately plain.\n *\n * THE LABEL MOVES INSIDE THE ITEM. Radix's `RadioGroupItem` is a bare\n * button and the caller pairs it with a separate `<Label htmlFor>`; React\n * Aria's `Radio` takes its label as `children` and wires the association\n * itself. That is the one rename a call site has to make, and it removes\n * the id bookkeeping.\n *\n * `border-primary` becomes `border-border` for the same reason it did on\n * Checkbox: brand orange is 2.44:1 on white and cannot legally be the only\n * thing describing a control's edge (WCAG 1.4.11). Selected reuses\n * Checkbox's exact pair -- `bg-primary` with a `primary-strong` boundary --\n * so a radio and a checkbox in the same form agree.\n */\nexport const radioGroupVariants = cva([\n \"flex flex-col gap-2\",\n \"box-border font-sans\",\n]);\n\nexport const radioVariants = cva([\n \"inline-flex size-4 shrink-0 items-center justify-center\",\n \"rounded-full border-(length:--border-width) box-border\",\n \"bg-background border-border\",\n \"motion-safe:transition-colors motion-safe:duration-(--duration-fast) motion-safe:ease-standard\",\n \"group-data-[selected]:bg-primary group-data-[selected]:border-primary-strong\",\n \"group-data-[selected]:text-primary-foreground\",\n selectionRingVariants(),\n]);\n\n/**\n * A set of mutually exclusive choices.\n *\n * @example\n * <RadioGroup value={plan} onChange={setPlan} aria-label=\"Plan\">\n * <Radio value=\"free\">Free</Radio>\n * <Radio value=\"pro\">Pro</Radio>\n * </RadioGroup>\n */\nexport interface RadioGroupProps extends Omit<AriaRadioGroupProps, \"className\" | \"style\"> {\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport function RadioGroup({ className, ...props }: RadioGroupProps) {\n return (\n <AriaRadioGroup\n {...props}\n data-slot=\"radio-group\"\n className={cn(radioGroupVariants(), className)}\n />\n );\n}\n\n/**\n * One choice.\n *\n * @example\n * <Radio value=\"pro\">Pro</Radio>\n */\nexport interface RadioProps extends Omit<AriaRadioProps, \"className\" | \"style\" | \"children\"> {\n /** The label, and this option's accessible name. */\n children?: ReactNode;\n /** Extra classes for the label. Merged last, so they win conflicts. */\n className?: string;\n}\n\nexport function Radio({ className, children, ...props }: RadioProps) {\n return (\n <AriaRadio\n {...props}\n data-slot=\"radio\"\n // justify-start, overriding the shared label's centring: a radio sits\n // in a vertical list where every label must start on the same line,\n // not centre itself in a 24px box.\n className={cn(cnAll(selectionLabelVariants(), \"justify-start\"), className)}\n >\n {({ isSelected }) => (\n <>\n <span aria-hidden=\"true\" data-slot=\"radio-dot\" className={radioVariants()}>\n {isSelected ? (\n // Rendered in JS rather than toggled with CSS, so the\n // precedence is explicit and a test can read it from the DOM.\n <span data-slot=\"radio-indicator\" className=\"size-1.5 rounded-full bg-current\" />\n ) : null}\n </span>\n {children == null ? null : <span data-slot=\"radio-label\">{children}</span>}\n </>\n )}\n </AriaRadio>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAa,qBAAqB,IAAI,CACpC,uBACA,sBACF,CAAC;AAED,MAAa,gBAAgB,IAAI;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA,sBAAsB;AACxB,CAAC;AAgBD,SAAgBA,aAAW,EAAE,WAAW,GAAG,SAA0B;CACnE,OACE,oBAACC,YAAD;EACE,GAAI;EACJ,aAAU;EACV,WAAW,GAAG,mBAAmB,GAAG,SAAS;CAC9C,CAAA;AAEL;AAeA,SAAgBC,QAAM,EAAE,WAAW,UAAU,GAAG,SAAqB;CACnE,OACE,oBAACC,OAAD;EACE,GAAI;EACJ,aAAU;EAIV,WAAW,GAAG,MAAM,uBAAuB,GAAG,eAAe,GAAG,SAAS;EAEvE,WAAA,EAAE,iBACF,qBAAA,UAAA,EAAA,UAAA,CACE,oBAAC,QAAD;GAAM,eAAY;GAAO,aAAU;GAAY,WAAW,cAAc;GACrE,UAAA,aAGC,oBAAC,QAAD;IAAM,aAAU;IAAkB,WAAU;GAAoC,CAAA,IAC9E;EACA,CAAA,GACL,YAAY,OAAO,OAAO,oBAAC,QAAD;GAAM,aAAU;GAAe;EAAe,CAAA,CACzE,EAAA,CAAA;CAEK,CAAA;AAEf"}
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { cn } from "../lib/cn.js";
2
+ import { cn, cnAll } from "../lib/cn.js";
3
3
  import { fieldSurfaceVariants } from "../lib/field-surface.js";
4
4
  import { cva } from "class-variance-authority";
5
5
  import { Button, FieldError, Label, ListBox, ListBoxItem, Popover, Select, SelectValue } from "react-aria-components";
@@ -81,7 +81,7 @@ function Select$1({ children, label, labelHidden, placeholder, errorMessage, siz
81
81
  children: [
82
82
  /* @__PURE__ */ jsx(Label, {
83
83
  "data-slot": "select-label",
84
- className: cn("text-sm leading-(--leading-normal) text-foreground", labelHidden && "sr-only"),
84
+ className: cnAll("text-sm leading-(--leading-normal) text-foreground", labelHidden && "sr-only"),
85
85
  children: label
86
86
  }),
87
87
  /* @__PURE__ */ jsxs(Button, {
@@ -1 +1 @@
1
- {"version":3,"file":"select.js","names":["Select","AriaSelect","AriaLabel","AriaButton","AriaSelectValue","AriaFieldError","AriaPopover","AriaListBox","AriaListBoxItem"],"sources":["../../src/select/select.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n FieldError as AriaFieldError,\n Label as AriaLabel,\n ListBox as AriaListBox,\n ListBoxItem as AriaListBoxItem,\n Popover as AriaPopover,\n Select as AriaSelect,\n SelectValue as AriaSelectValue,\n type ListBoxItemProps as AriaListBoxItemProps,\n type SelectProps as AriaSelectProps,\n} from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { fieldSurfaceVariants } from \"../lib/field-surface.js\";\n\n/**\n * Select, ported from the legacy shadcn/Radix component.\n *\n * FOUR PARTS COLLAPSE INTO ONE. The census reads `SelectItem` 35 and\n * `Select`, `SelectTrigger`, `SelectValue`, `SelectContent` 15 each --\n * those four always appear together in the same shape, exactly like\n * Dialog's header trio, so they are one component and a prop.\n * `SelectGroup`, `SelectLabel`, `SelectSeparator`, `SelectScrollUpButton`\n * and `SelectScrollDownButton` have ZERO call sites and are not ported.\n *\n * `label` IS REQUIRED. The legacy exports a `SelectLabel` that nobody\n * renders and passes no `aria-label` anywhere, so all 15 selects are\n * unnamed comboboxes -- the same finding as Progress, Slider and\n * ScrollArea. Pass `labelHidden` when the surrounding UI already says it.\n *\n * The trigger reuses `fieldSurfaceVariants`, shared with Input and\n * Textarea. A select and a text field sit in the same form row and must\n * be the same box; the legacy styled them separately and drifted --\n * `h-10` here against Input's `h-9`, for no stated reason.\n *\n * The popover is `card`, not a `popover` role: `--popover` is\n * byte-identical to `--card` in the legacy theme. Item hover is `muted`,\n * not `accent`, for the same reason.\n */\n/*\n * `group` is load-bearing, not decoration. React Aria puts `data-invalid` on\n * THIS element, never on the trigger -- so the trigger's own\n * `data-[invalid]:` rule, inherited from the shared field surface, can never\n * match and `isInvalid` painted nothing at all. The trigger reads the state\n * from here instead.\n */\nexport const selectVariants = cva([\"group flex w-full flex-col gap-1.5 box-border font-sans\"]);\n\nexport const selectTriggerVariants = cva(\n [\n // See selectVariants: the invalid state lives on the ROOT, so this reads it\n // from the group rather than from itself.\n \"group-data-[invalid]:border-destructive-strong\",\n fieldSurfaceVariants(),\n \"flex items-center justify-between gap-2 text-start\",\n \"text-base leading-(--leading-normal) cursor-pointer\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n ],\n {\n variants: {\n /**\n * The same rungs and the same tokens as Input and Button. 14 of the\n * product's 32 `SelectTrigger` call sites override the height by hand,\n * and a Select sits in the same form row as a field -- giving Input the\n * axis without giving it here would be exactly the drift\n * `fieldSurfaceVariants` exists to prevent.\n *\n * Type stays `text-base` at every rung, matching Input. A row whose two\n * controls disagree about type size reads as a mistake.\n */\n size: {\n sm: \"h-(--density-control-h-sm) px-(--density-control-pad-sm)\",\n md: \"h-(--density-control-h-md) px-(--density-control-pad-md)\",\n lg: \"h-(--density-control-h-lg) px-(--density-control-pad-lg)\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\nexport const selectPopoverVariants = cva([\n // --trigger-width is set inline by React Aria, so the list matches the\n // control it came from rather than guessing a width.\n \"w-(--trigger-width) min-w-32 overflow-y-auto\",\n \"rounded-md border-(length:--border-width) border-border p-1\",\n \"bg-card text-card-foreground shadow-floating\",\n \"box-border font-sans text-sm leading-(--leading-normal)\",\n]);\n\nexport const selectItemVariants = cva([\n \"relative flex cursor-pointer select-none items-center gap-2\",\n \"rounded-2xs py-1.5 pe-2 ps-8\",\n \"box-border text-sm leading-(--leading-normal) text-card-foreground\",\n // data-focused, not :hover. React Aria drives one highlight from both\n // pointer and keyboard, so the row under the mouse and the row the\n // arrows reached are the same row.\n \"outline-none data-[focused]:bg-muted\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n]);\n\n/**\n * A single-choice picker.\n *\n * @example\n * <Select label=\"Country\" placeholder=\"Pick one\"\n * selectedKey={code} onSelectionChange={setCode}>\n * <SelectItem id=\"gb\">United Kingdom</SelectItem>\n * <SelectItem id=\"us\">United States</SelectItem>\n * </Select>\n */\nexport interface SelectProps\n extends Omit<AriaSelectProps<object>, \"className\" | \"style\" | \"children\" | \"items\"> {\n /** `SelectItem` children. */\n children?: ReactNode;\n /**\n * What is being chosen, and the control's accessible name.\n *\n * **Required.** All 15 legacy selects are unnamed: the legacy exports a\n * `SelectLabel` that nobody renders and passes no `aria-label`.\n */\n label: ReactNode;\n /** Render the label for screen readers only. */\n labelHidden?: boolean;\n /** Shown while nothing is selected. */\n placeholder?: string;\n /**\n * Why the selection is wrong. Rendered under the control, and it is what\n * makes `isInvalid` reach a screen reader.\n *\n * React Aria marks the ROOT `data-invalid` and leaves the button untouched,\n * and an `aria-invalid` handed to that button is dropped by\n * `filterDOMProps`, whose allowlist stops at the labelling attributes -- the\n * same silent loss as `aria-current` on a pagination button. So without a\n * message, an invalid select is invalid only in colour, which is WCAG 1.4.1\n * on its own. React Aria wires this into `aria-describedby` for us.\n */\n errorMessage?: ReactNode;\n /**\n * Control scale of the trigger. Height and inline padding come from the\n * same `--density-control-*` tokens as Input and Button, so a Select, a\n * field and a button on one rung are the same height.\n *\n * Type does not scale with it, matching Input.\n *\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Extra classes for the wrapper. Merged last. */\n className?: string;\n}\n\nexport function Select({\n children,\n label,\n labelHidden,\n placeholder,\n errorMessage,\n size = \"md\",\n className,\n ...props\n}: SelectProps) {\n return (\n <AriaSelect {...props} data-slot=\"select\" className={cn(selectVariants(), className)}>\n <AriaLabel\n data-slot=\"select-label\"\n className={cn(\n \"text-sm leading-(--leading-normal) text-foreground\",\n labelHidden && \"sr-only\",\n )}\n >\n {label}\n </AriaLabel>\n\n <AriaButton data-slot=\"select-trigger\" className={selectTriggerVariants({ size })}>\n <AriaSelectValue data-slot=\"select-value\">\n {({ isPlaceholder, selectedText }) =>\n isPlaceholder ? (\n <span data-slot=\"select-placeholder\" className=\"text-muted-foreground\">\n {placeholder}\n </span>\n ) : (\n selectedText\n )\n }\n </AriaSelectValue>\n {/*\n aria-hidden: the chevron is decoration. The button already\n announces itself as a combobox, and a second name source only\n makes it announce twice.\n */}\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"text-muted-foreground\"\n >\n <path d=\"m4 6 4 4 4-4\" />\n </svg>\n </AriaButton>\n\n {errorMessage == null ? null : (\n // Not conditional on isInvalid: React Aria renders FieldError only\n // while the field IS invalid, so gating it here as well would just be\n // two sources of truth for one state.\n <AriaFieldError\n data-slot=\"select-error\"\n className=\"text-sm leading-(--leading-normal) text-destructive-strong\"\n >\n {errorMessage}\n </AriaFieldError>\n )}\n\n <AriaPopover data-slot=\"select-popover\" className={selectPopoverVariants()}>\n <AriaListBox data-slot=\"select-list\" className=\"outline-none\">\n {children}\n </AriaListBox>\n </AriaPopover>\n </AriaSelect>\n );\n}\n\n/**\n * One option. Its `id` is the value.\n *\n * @example\n * <SelectItem id=\"gb\">United Kingdom</SelectItem>\n */\nexport interface SelectItemProps\n extends Omit<AriaListBoxItemProps, \"className\" | \"style\" | \"children\"> {\n children?: ReactNode;\n /**\n * The option's text, for the trigger and for typeahead.\n *\n * Derived from `children` when they are a string. Pass it explicitly\n * when the option renders anything richer than text.\n */\n textValue?: string;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport function SelectItem({ className, children, textValue, ...props }: SelectItemProps) {\n return (\n <AriaListBoxItem\n {...props}\n /*\n textValue, derived from string children.\n React Aria infers an item's text from its children -- but ONLY\n when they are a string. This item uses FUNCTION children so the\n tick can be rendered in JS, which defeats that inference: the\n trigger showed an empty value after selecting, and typeahead had\n nothing to match on. Neither is visible in the type system.\n */\n textValue={textValue ?? (typeof children === \"string\" ? children : undefined)}\n data-slot=\"select-item\"\n className={cn(selectItemVariants(), className)}\n >\n {({ isSelected }) => (\n <>\n {/*\n The tick is rendered in JS rather than toggled with CSS, so\n the precedence is explicit and a test can read it from the\n DOM. ps-8 above reserves the gutter, so selecting an option\n does not shift its label.\n */}\n {isSelected ? (\n <svg\n data-slot=\"select-item-check\"\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"absolute start-2 size-3.5\"\n >\n <path d=\"m3.5 8.5 3 3 6-6\" />\n </svg>\n ) : null}\n {children}\n </>\n )}\n </AriaListBoxItem>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,MAAa,iBAAiB,IAAI,CAAC,yDAAyD,CAAC;AAE7F,MAAa,wBAAwB,IACnC;CAGE;CACA,qBAAqB;CACrB;CACA;CACA;CACA;AACF,GACA;CACE,UAAU;;;;;;;;;;;AAWR,MAAM;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI;CACN,EACF;CACA,iBAAiB,EAAE,MAAM,KAAK;AAChC,CACF;AAEA,MAAa,wBAAwB,IAAI;CAGvC;CACA;CACA;CACA;AACF,CAAC;AAED,MAAa,qBAAqB,IAAI;CACpC;CACA;CACA;CAIA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAqDD,SAAgBA,SAAO,EACrB,UACA,OACA,aACA,aACA,cACA,OAAO,MACP,WACA,GAAG,SACW;CACd,OACE,qBAACC,QAAD;EAAY,GAAI;EAAO,aAAU;EAAS,WAAW,GAAG,eAAe,GAAG,SAAS;EAAnF,UAAA;GACE,oBAACC,OAAD;IACE,aAAU;IACV,WAAW,GACT,sDACA,eAAe,SACjB;IAEC,UAAA;GACQ,CAAA;GAEX,qBAACC,QAAD;IAAY,aAAU;IAAiB,WAAW,sBAAsB,EAAE,KAAK,CAAC;IAAhF,UAAA,CACE,oBAACC,aAAD;KAAiB,aAAU;KACvB,WAAA,EAAE,eAAe,mBACjB,gBACE,oBAAC,QAAD;MAAM,aAAU;MAAqB,WAAU;MAC5C,UAAA;KACG,CAAA,IAEN;IAGW,CAAA,GAMjB,oBAAC,OAAD;KACE,eAAY;KACZ,SAAQ;KACR,MAAK;KACL,QAAO;KACP,aAAY;KACZ,eAAc;KACd,gBAAe;KACf,WAAU;KAEV,UAAA,oBAAC,QAAD,EAAM,GAAE,eAAgB,CAAA;IACrB,CAAA,CACK;;GAEX,gBAAgB,OAAO,OAItB,oBAACC,YAAD;IACE,aAAU;IACV,WAAU;IAET,UAAA;GACa,CAAA;GAGlB,oBAACC,SAAD;IAAa,aAAU;IAAiB,WAAW,sBAAsB;IACvE,UAAA,oBAACC,SAAD;KAAa,aAAU;KAAc,WAAU;KAC5C;IACU,CAAA;GACF,CAAA;EACH;;AAEhB;AAsBA,SAAgB,WAAW,EAAE,WAAW,UAAU,WAAW,GAAG,SAA0B;CACxF,OACE,oBAACC,aAAD;EACE,GAAI;EASJ,WAAW,cAAc,OAAO,aAAa,WAAW,WAAW,KAAA;EACnE,aAAU;EACV,WAAW,GAAG,mBAAmB,GAAG,SAAS;EAE3C,WAAA,EAAE,iBACF,qBAAA,UAAA,EAAA,UAAA,CAOG,aACC,oBAAC,OAAD;GACE,aAAU;GACV,eAAY;GACZ,SAAQ;GACR,MAAK;GACL,QAAO;GACP,aAAY;GACZ,eAAc;GACd,gBAAe;GACf,WAAU;GAEV,UAAA,oBAAC,QAAD,EAAM,GAAE,mBAAoB,CAAA;EACzB,CAAA,IACH,MACH,QACD,EAAA,CAAA;CAEW,CAAA;AAErB"}
1
+ {"version":3,"file":"select.js","names":["Select","AriaSelect","AriaLabel","AriaButton","AriaSelectValue","AriaFieldError","AriaPopover","AriaListBox","AriaListBoxItem"],"sources":["../../src/select/select.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport type { ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n FieldError as AriaFieldError,\n Label as AriaLabel,\n ListBox as AriaListBox,\n ListBoxItem as AriaListBoxItem,\n Popover as AriaPopover,\n Select as AriaSelect,\n SelectValue as AriaSelectValue,\n type ListBoxItemProps as AriaListBoxItemProps,\n type SelectProps as AriaSelectProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { fieldSurfaceVariants } from \"../lib/field-surface.js\";\n\n/**\n * Select, ported from the legacy shadcn/Radix component.\n *\n * FOUR PARTS COLLAPSE INTO ONE. The census reads `SelectItem` 35 and\n * `Select`, `SelectTrigger`, `SelectValue`, `SelectContent` 15 each --\n * those four always appear together in the same shape, exactly like\n * Dialog's header trio, so they are one component and a prop.\n * `SelectGroup`, `SelectLabel`, `SelectSeparator`, `SelectScrollUpButton`\n * and `SelectScrollDownButton` have ZERO call sites and are not ported.\n *\n * `label` IS REQUIRED. The legacy exports a `SelectLabel` that nobody\n * renders and passes no `aria-label` anywhere, so all 15 selects are\n * unnamed comboboxes -- the same finding as Progress, Slider and\n * ScrollArea. Pass `labelHidden` when the surrounding UI already says it.\n *\n * The trigger reuses `fieldSurfaceVariants`, shared with Input and\n * Textarea. A select and a text field sit in the same form row and must\n * be the same box; the legacy styled them separately and drifted --\n * `h-10` here against Input's `h-9`, for no stated reason.\n *\n * The popover is `card`, not a `popover` role: `--popover` is\n * byte-identical to `--card` in the legacy theme. Item hover is `muted`,\n * not `accent`, for the same reason.\n */\n/*\n * `group` is load-bearing, not decoration. React Aria puts `data-invalid` on\n * THIS element, never on the trigger -- so the trigger's own\n * `data-[invalid]:` rule, inherited from the shared field surface, can never\n * match and `isInvalid` painted nothing at all. The trigger reads the state\n * from here instead.\n */\nexport const selectVariants = cva([\"group flex w-full flex-col gap-1.5 box-border font-sans\"]);\n\nexport const selectTriggerVariants = cva(\n [\n // See selectVariants: the invalid state lives on the ROOT, so this reads it\n // from the group rather than from itself.\n \"group-data-[invalid]:border-destructive-strong\",\n fieldSurfaceVariants(),\n \"flex items-center justify-between gap-2 text-start\",\n \"text-base leading-(--leading-normal) cursor-pointer\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"[&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n ],\n {\n variants: {\n /**\n * The same rungs and the same tokens as Input and Button. 14 of the\n * product's 32 `SelectTrigger` call sites override the height by hand,\n * and a Select sits in the same form row as a field -- giving Input the\n * axis without giving it here would be exactly the drift\n * `fieldSurfaceVariants` exists to prevent.\n *\n * Type stays `text-base` at every rung, matching Input. A row whose two\n * controls disagree about type size reads as a mistake.\n */\n size: {\n sm: \"h-(--density-control-h-sm) px-(--density-control-pad-sm)\",\n md: \"h-(--density-control-h-md) px-(--density-control-pad-md)\",\n lg: \"h-(--density-control-h-lg) px-(--density-control-pad-lg)\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\nexport const selectPopoverVariants = cva([\n // --trigger-width is set inline by React Aria, so the list matches the\n // control it came from rather than guessing a width.\n \"w-(--trigger-width) min-w-32 overflow-y-auto\",\n \"rounded-md border-(length:--border-width) border-border p-1\",\n \"bg-card text-card-foreground shadow-floating\",\n \"box-border font-sans text-sm leading-(--leading-normal)\",\n]);\n\nexport const selectItemVariants = cva([\n \"relative flex cursor-pointer select-none items-center gap-2\",\n \"rounded-2xs py-1.5 pe-2 ps-8\",\n \"box-border text-sm leading-(--leading-normal) text-card-foreground\",\n // data-focused, not :hover. React Aria drives one highlight from both\n // pointer and keyboard, so the row under the mouse and the row the\n // arrows reached are the same row.\n \"outline-none data-[focused]:bg-muted\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n]);\n\n/**\n * A single-choice picker.\n *\n * @example\n * <Select label=\"Country\" placeholder=\"Pick one\"\n * selectedKey={code} onSelectionChange={setCode}>\n * <SelectItem id=\"gb\">United Kingdom</SelectItem>\n * <SelectItem id=\"us\">United States</SelectItem>\n * </Select>\n */\nexport interface SelectProps\n extends Omit<AriaSelectProps<object>, \"className\" | \"style\" | \"children\" | \"items\"> {\n /** `SelectItem` children. */\n children?: ReactNode;\n /**\n * What is being chosen, and the control's accessible name.\n *\n * **Required.** All 15 legacy selects are unnamed: the legacy exports a\n * `SelectLabel` that nobody renders and passes no `aria-label`.\n */\n label: ReactNode;\n /** Render the label for screen readers only. */\n labelHidden?: boolean;\n /** Shown while nothing is selected. */\n placeholder?: string;\n /**\n * Why the selection is wrong. Rendered under the control, and it is what\n * makes `isInvalid` reach a screen reader.\n *\n * React Aria marks the ROOT `data-invalid` and leaves the button untouched,\n * and an `aria-invalid` handed to that button is dropped by\n * `filterDOMProps`, whose allowlist stops at the labelling attributes -- the\n * same silent loss as `aria-current` on a pagination button. So without a\n * message, an invalid select is invalid only in colour, which is WCAG 1.4.1\n * on its own. React Aria wires this into `aria-describedby` for us.\n */\n errorMessage?: ReactNode;\n /**\n * Control scale of the trigger. Height and inline padding come from the\n * same `--density-control-*` tokens as Input and Button, so a Select, a\n * field and a button on one rung are the same height.\n *\n * Type does not scale with it, matching Input.\n *\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Extra classes for the wrapper. Merged last. */\n className?: string;\n}\n\nexport function Select({\n children,\n label,\n labelHidden,\n placeholder,\n errorMessage,\n size = \"md\",\n className,\n ...props\n}: SelectProps) {\n return (\n <AriaSelect {...props} data-slot=\"select\" className={cn(selectVariants(), className)}>\n <AriaLabel\n data-slot=\"select-label\"\n className={cnAll(\n \"text-sm leading-(--leading-normal) text-foreground\",\n labelHidden && \"sr-only\",\n )}\n >\n {label}\n </AriaLabel>\n\n <AriaButton data-slot=\"select-trigger\" className={selectTriggerVariants({ size })}>\n <AriaSelectValue data-slot=\"select-value\">\n {({ isPlaceholder, selectedText }) =>\n isPlaceholder ? (\n <span data-slot=\"select-placeholder\" className=\"text-muted-foreground\">\n {placeholder}\n </span>\n ) : (\n selectedText\n )\n }\n </AriaSelectValue>\n {/*\n aria-hidden: the chevron is decoration. The button already\n announces itself as a combobox, and a second name source only\n makes it announce twice.\n */}\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"text-muted-foreground\"\n >\n <path d=\"m4 6 4 4 4-4\" />\n </svg>\n </AriaButton>\n\n {errorMessage == null ? null : (\n // Not conditional on isInvalid: React Aria renders FieldError only\n // while the field IS invalid, so gating it here as well would just be\n // two sources of truth for one state.\n <AriaFieldError\n data-slot=\"select-error\"\n className=\"text-sm leading-(--leading-normal) text-destructive-strong\"\n >\n {errorMessage}\n </AriaFieldError>\n )}\n\n <AriaPopover data-slot=\"select-popover\" className={selectPopoverVariants()}>\n <AriaListBox data-slot=\"select-list\" className=\"outline-none\">\n {children}\n </AriaListBox>\n </AriaPopover>\n </AriaSelect>\n );\n}\n\n/**\n * One option. Its `id` is the value.\n *\n * @example\n * <SelectItem id=\"gb\">United Kingdom</SelectItem>\n */\nexport interface SelectItemProps\n extends Omit<AriaListBoxItemProps, \"className\" | \"style\" | \"children\"> {\n children?: ReactNode;\n /**\n * The option's text, for the trigger and for typeahead.\n *\n * Derived from `children` when they are a string. Pass it explicitly\n * when the option renders anything richer than text.\n */\n textValue?: string;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nexport function SelectItem({ className, children, textValue, ...props }: SelectItemProps) {\n return (\n <AriaListBoxItem\n {...props}\n /*\n textValue, derived from string children.\n React Aria infers an item's text from its children -- but ONLY\n when they are a string. This item uses FUNCTION children so the\n tick can be rendered in JS, which defeats that inference: the\n trigger showed an empty value after selecting, and typeahead had\n nothing to match on. Neither is visible in the type system.\n */\n textValue={textValue ?? (typeof children === \"string\" ? children : undefined)}\n data-slot=\"select-item\"\n className={cn(selectItemVariants(), className)}\n >\n {({ isSelected }) => (\n <>\n {/*\n The tick is rendered in JS rather than toggled with CSS, so\n the precedence is explicit and a test can read it from the\n DOM. ps-8 above reserves the gutter, so selecting an option\n does not shift its label.\n */}\n {isSelected ? (\n <svg\n data-slot=\"select-item-check\"\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"absolute start-2 size-3.5\"\n >\n <path d=\"m3.5 8.5 3 3 6-6\" />\n </svg>\n ) : null}\n {children}\n </>\n )}\n </AriaListBoxItem>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,MAAa,iBAAiB,IAAI,CAAC,yDAAyD,CAAC;AAE7F,MAAa,wBAAwB,IACnC;CAGE;CACA,qBAAqB;CACrB;CACA;CACA;CACA;AACF,GACA;CACE,UAAU;;;;;;;;;;;AAWR,MAAM;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI;CACN,EACF;CACA,iBAAiB,EAAE,MAAM,KAAK;AAChC,CACF;AAEA,MAAa,wBAAwB,IAAI;CAGvC;CACA;CACA;CACA;AACF,CAAC;AAED,MAAa,qBAAqB,IAAI;CACpC;CACA;CACA;CAIA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAqDD,SAAgBA,SAAO,EACrB,UACA,OACA,aACA,aACA,cACA,OAAO,MACP,WACA,GAAG,SACW;CACd,OACE,qBAACC,QAAD;EAAY,GAAI;EAAO,aAAU;EAAS,WAAW,GAAG,eAAe,GAAG,SAAS;EAAnF,UAAA;GACE,oBAACC,OAAD;IACE,aAAU;IACV,WAAW,MACT,sDACA,eAAe,SACjB;IAEC,UAAA;GACQ,CAAA;GAEX,qBAACC,QAAD;IAAY,aAAU;IAAiB,WAAW,sBAAsB,EAAE,KAAK,CAAC;IAAhF,UAAA,CACE,oBAACC,aAAD;KAAiB,aAAU;KACvB,WAAA,EAAE,eAAe,mBACjB,gBACE,oBAAC,QAAD;MAAM,aAAU;MAAqB,WAAU;MAC5C,UAAA;KACG,CAAA,IAEN;IAGW,CAAA,GAMjB,oBAAC,OAAD;KACE,eAAY;KACZ,SAAQ;KACR,MAAK;KACL,QAAO;KACP,aAAY;KACZ,eAAc;KACd,gBAAe;KACf,WAAU;KAEV,UAAA,oBAAC,QAAD,EAAM,GAAE,eAAgB,CAAA;IACrB,CAAA,CACK;;GAEX,gBAAgB,OAAO,OAItB,oBAACC,YAAD;IACE,aAAU;IACV,WAAU;IAET,UAAA;GACa,CAAA;GAGlB,oBAACC,SAAD;IAAa,aAAU;IAAiB,WAAW,sBAAsB;IACvE,UAAA,oBAACC,SAAD;KAAa,aAAU;KAAc,WAAU;KAC5C;IACU,CAAA;GACF,CAAA;EACH;;AAEhB;AAsBA,SAAgB,WAAW,EAAE,WAAW,UAAU,WAAW,GAAG,SAA0B;CACxF,OACE,oBAACC,aAAD;EACE,GAAI;EASJ,WAAW,cAAc,OAAO,aAAa,WAAW,WAAW,KAAA;EACnE,aAAU;EACV,WAAW,GAAG,mBAAmB,GAAG,SAAS;EAE3C,WAAA,EAAE,iBACF,qBAAA,UAAA,EAAA,UAAA,CAOG,aACC,oBAAC,OAAD;GACE,aAAU;GACV,eAAY;GACZ,SAAQ;GACR,MAAK;GACL,QAAO;GACP,aAAY;GACZ,eAAc;GACd,gBAAe;GACf,WAAU;GAEV,UAAA,oBAAC,QAAD,EAAM,GAAE,mBAAoB,CAAA;EACzB,CAAA,IACH,MACH,QACD,EAAA,CAAA;CAEW,CAAA;AAErB"}
@@ -1,4 +1,5 @@
1
1
  "use client";
2
+ import { cn } from "../lib/cn.js";
2
3
  import { Dialog } from "../dialog/dialog.js";
3
4
  import { Button } from "../button/button.js";
4
5
  import { ScrollArea } from "../scroll-area/scroll-area.js";
@@ -83,11 +84,11 @@ function Sheet({ side = "right", className, onBack, backLabel = "Back", scrollRe
83
84
  children: backLabel
84
85
  }) : void 0,
85
86
  overlayClassName: sheetOverlayVariants({ side }),
86
- className: [sheetPanelVariants({ side }), className].filter(Boolean).join(" "),
87
+ className: cn(sheetPanelVariants({ side }), className),
87
88
  children: /* @__PURE__ */ jsx(ScrollArea, {
88
89
  scrollResetKey,
89
90
  label: bodyLabel ?? (typeof title === "string" ? title : ""),
90
- className: ["min-h-0 flex-1", bodyClassName].filter(Boolean).join(" "),
91
+ className: cn("min-h-0 flex-1", bodyClassName),
91
92
  children
92
93
  })
93
94
  });