@icomponents/react 0.1.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.
Files changed (117) hide show
  1. package/README.md +40 -0
  2. package/package.json +165 -0
  3. package/src/assets/custom/icon-dir.tsx +105 -0
  4. package/src/assets/custom/icon-layout-compact.tsx +84 -0
  5. package/src/assets/custom/icon-layout-default.tsx +48 -0
  6. package/src/assets/custom/icon-layout-full.tsx +68 -0
  7. package/src/assets/custom/icon-sidebar-floating.tsx +62 -0
  8. package/src/assets/custom/icon-sidebar-inset.tsx +53 -0
  9. package/src/assets/custom/icon-sidebar-sidebar.tsx +48 -0
  10. package/src/assets/custom/icon-theme-dark.tsx +36 -0
  11. package/src/assets/custom/icon-theme-light.tsx +35 -0
  12. package/src/assets/custom/icon-theme-system.tsx +113 -0
  13. package/src/components/admin-page-header.tsx +29 -0
  14. package/src/components/color-theme-switch.tsx +93 -0
  15. package/src/components/coming-soon.tsx +16 -0
  16. package/src/components/command-menu.tsx +98 -0
  17. package/src/components/config-drawer.tsx +401 -0
  18. package/src/components/confirm-dialog.dom.test.tsx +60 -0
  19. package/src/components/confirm-dialog.tsx +75 -0
  20. package/src/components/data-table/bulk-actions.tsx +193 -0
  21. package/src/components/data-table/column-header.tsx +66 -0
  22. package/src/components/data-table/data-table.dom.test.tsx +54 -0
  23. package/src/components/data-table/data-table.tsx +189 -0
  24. package/src/components/data-table/faceted-filter.tsx +156 -0
  25. package/src/components/data-table/index.ts +5 -0
  26. package/src/components/data-table/pagination.tsx +127 -0
  27. package/src/components/data-table/tanstack-table.d.ts +12 -0
  28. package/src/components/data-table/toolbar.tsx +88 -0
  29. package/src/components/data-table/view-options.tsx +49 -0
  30. package/src/components/date-picker.tsx +39 -0
  31. package/src/components/detail-card.tsx +63 -0
  32. package/src/components/language-switch.tsx +38 -0
  33. package/src/components/layout/app-sidebar.tsx +59 -0
  34. package/src/components/layout/app-title.tsx +62 -0
  35. package/src/components/layout/header.tsx +61 -0
  36. package/src/components/layout/main.tsx +34 -0
  37. package/src/components/layout/nav-group.tsx +185 -0
  38. package/src/components/layout/nav-user.tsx +115 -0
  39. package/src/components/layout/screen-header.tsx +119 -0
  40. package/src/components/layout/team-switcher.tsx +84 -0
  41. package/src/components/layout/top-nav.tsx +68 -0
  42. package/src/components/layout/types.ts +44 -0
  43. package/src/components/learn-more.tsx +35 -0
  44. package/src/components/long-text.tsx +80 -0
  45. package/src/components/navigation-progress.tsx +18 -0
  46. package/src/components/password-input.tsx +36 -0
  47. package/src/components/profile-dropdown.tsx +106 -0
  48. package/src/components/search.tsx +38 -0
  49. package/src/components/select-dropdown.tsx +62 -0
  50. package/src/components/sign-out-dialog.tsx +31 -0
  51. package/src/components/skip-to-main.tsx +10 -0
  52. package/src/components/stat-tile.tsx +56 -0
  53. package/src/components/status-badge.test.ts +23 -0
  54. package/src/components/status-badge.tsx +49 -0
  55. package/src/components/theme-switch.tsx +61 -0
  56. package/src/components/ui/alert-dialog.tsx +134 -0
  57. package/src/components/ui/alert.tsx +59 -0
  58. package/src/components/ui/avatar.tsx +38 -0
  59. package/src/components/ui/badge.tsx +39 -0
  60. package/src/components/ui/base-ui-state-attrs.dom.test.tsx +80 -0
  61. package/src/components/ui/button.tsx +51 -0
  62. package/src/components/ui/calendar.tsx +174 -0
  63. package/src/components/ui/card.tsx +74 -0
  64. package/src/components/ui/checkbox.tsx +26 -0
  65. package/src/components/ui/collapsible.tsx +18 -0
  66. package/src/components/ui/command.tsx +160 -0
  67. package/src/components/ui/dialog.tsx +129 -0
  68. package/src/components/ui/dropdown-menu.tsx +254 -0
  69. package/src/components/ui/form.tsx +155 -0
  70. package/src/components/ui/input-otp.tsx +67 -0
  71. package/src/components/ui/input.tsx +20 -0
  72. package/src/components/ui/label.tsx +21 -0
  73. package/src/components/ui/popover.tsx +54 -0
  74. package/src/components/ui/radio-group.tsx +37 -0
  75. package/src/components/ui/scroll-area.tsx +57 -0
  76. package/src/components/ui/select.tsx +213 -0
  77. package/src/components/ui/separator.tsx +23 -0
  78. package/src/components/ui/sheet.tsx +128 -0
  79. package/src/components/ui/sidebar.tsx +702 -0
  80. package/src/components/ui/skeleton.tsx +13 -0
  81. package/src/components/ui/sonner.tsx +26 -0
  82. package/src/components/ui/switch.tsx +25 -0
  83. package/src/components/ui/table.tsx +89 -0
  84. package/src/components/ui/tabs.tsx +51 -0
  85. package/src/components/ui/textarea.tsx +17 -0
  86. package/src/components/ui/tooltip.tsx +65 -0
  87. package/src/config/fonts.ts +2 -0
  88. package/src/context/direction-provider.tsx +61 -0
  89. package/src/context/font-provider.tsx +70 -0
  90. package/src/context/language-provider.tsx +73 -0
  91. package/src/context/layout-provider.tsx +81 -0
  92. package/src/context/search-provider.tsx +48 -0
  93. package/src/context/theme-provider.tsx +108 -0
  94. package/src/hooks/sort-url.test.ts +16 -0
  95. package/src/hooks/use-dialog-state.tsx +15 -0
  96. package/src/hooks/use-mobile.tsx +16 -0
  97. package/src/hooks/use-table-url-state.ts +243 -0
  98. package/src/i18n/common.ts +164 -0
  99. package/src/i18n/create-i18n.test.ts +50 -0
  100. package/src/i18n/create-i18n.ts +67 -0
  101. package/src/i18n/types.ts +22 -0
  102. package/src/i18n/use-translation.ts +6 -0
  103. package/src/lib/clipboard.test.ts +46 -0
  104. package/src/lib/clipboard.ts +29 -0
  105. package/src/lib/cookies.ts +39 -0
  106. package/src/lib/format-cell.test.ts +57 -0
  107. package/src/lib/format-cell.ts +63 -0
  108. package/src/lib/handle-server-error.test.ts +85 -0
  109. package/src/lib/handle-server-error.ts +39 -0
  110. package/src/lib/random-id.ts +13 -0
  111. package/src/lib/show-submitted-data.tsx +14 -0
  112. package/src/lib/utils.test.ts +35 -0
  113. package/src/lib/utils.ts +78 -0
  114. package/src/secure-context.contract.test.ts +63 -0
  115. package/src/testids.contract.test.ts +113 -0
  116. package/src/testids.ts +8 -0
  117. package/src/theme-contract.test.ts +72 -0
@@ -0,0 +1,155 @@
1
+ import { mergeProps } from "@base-ui/react/merge-props";
2
+ import { useRender } from "@base-ui/react/use-render";
3
+ import { Label } from "@icomponents/react/components/ui/label";
4
+ import { cn } from "@icomponents/react/lib/utils";
5
+ import * as React from "react";
6
+ import {
7
+ Controller,
8
+ FormProvider,
9
+ useFormContext,
10
+ useFormState,
11
+ type ControllerProps,
12
+ type FieldPath,
13
+ type FieldValues,
14
+ } from "react-hook-form";
15
+
16
+ const Form = FormProvider;
17
+
18
+ type FormFieldContextValue<
19
+ TFieldValues extends FieldValues = FieldValues,
20
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
21
+ > = {
22
+ name: TName;
23
+ };
24
+
25
+ const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
26
+
27
+ const FormField = <
28
+ TFieldValues extends FieldValues = FieldValues,
29
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
30
+ >({
31
+ ...props
32
+ }: ControllerProps<TFieldValues, TName>) => {
33
+ return (
34
+ <FormFieldContext.Provider value={{ name: props.name }}>
35
+ <Controller {...props} />
36
+ </FormFieldContext.Provider>
37
+ );
38
+ };
39
+
40
+ const useFormField = () => {
41
+ const fieldContext = React.useContext(FormFieldContext);
42
+ const itemContext = React.useContext(FormItemContext);
43
+ const { getFieldState } = useFormContext();
44
+ const formState = useFormState({ name: fieldContext.name });
45
+ const fieldState = getFieldState(fieldContext.name, formState);
46
+
47
+ if (!fieldContext) {
48
+ throw new Error("useFormField should be used within <FormField>");
49
+ }
50
+
51
+ const { id } = itemContext;
52
+
53
+ return {
54
+ id,
55
+ name: fieldContext.name,
56
+ formItemId: `${id}-form-item`,
57
+ formDescriptionId: `${id}-form-item-description`,
58
+ formMessageId: `${id}-form-item-message`,
59
+ ...fieldState,
60
+ };
61
+ };
62
+
63
+ type FormItemContextValue = {
64
+ id: string;
65
+ };
66
+
67
+ const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
68
+
69
+ function FormItem({ className, ...props }: React.ComponentProps<"div">) {
70
+ const id = React.useId();
71
+
72
+ return (
73
+ <FormItemContext.Provider value={{ id }}>
74
+ <div data-slot="form-item" className={cn("grid gap-2", className)} {...props} />
75
+ </FormItemContext.Provider>
76
+ );
77
+ }
78
+
79
+ function FormLabel({ className, ...props }: React.ComponentProps<typeof Label>) {
80
+ const { error, formItemId } = useFormField();
81
+
82
+ return (
83
+ <Label
84
+ data-slot="form-label"
85
+ data-error={!!error}
86
+ className={cn("data-[error=true]:text-destructive", className)}
87
+ htmlFor={formItemId}
88
+ {...props}
89
+ />
90
+ );
91
+ }
92
+
93
+ // The Radix Slot passthrough (merges field a11y props onto the child control) → Base UI `useRender`,
94
+ // with the single child element as the render target.
95
+ function FormControl({ children, ...props }: React.ComponentProps<"div">) {
96
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
97
+
98
+ return useRender({
99
+ render: (children ?? <div />) as React.ReactElement<Record<string, unknown>>,
100
+ props: mergeProps<"div">(
101
+ {
102
+ id: formItemId,
103
+ "aria-describedby": !error
104
+ ? `${formDescriptionId}`
105
+ : `${formDescriptionId} ${formMessageId}`,
106
+ "aria-invalid": !!error,
107
+ },
108
+ props
109
+ ),
110
+ });
111
+ }
112
+
113
+ function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
114
+ const { formDescriptionId } = useFormField();
115
+
116
+ return (
117
+ <p
118
+ data-slot="form-description"
119
+ id={formDescriptionId}
120
+ className={cn("text-sm text-muted-foreground", className)}
121
+ {...props}
122
+ />
123
+ );
124
+ }
125
+
126
+ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
127
+ const { error, formMessageId } = useFormField();
128
+ const body = error ? String(error?.message ?? "") : props.children;
129
+
130
+ if (!body) {
131
+ return null;
132
+ }
133
+
134
+ return (
135
+ <p
136
+ data-slot="form-message"
137
+ id={formMessageId}
138
+ className={cn("text-sm text-destructive", className)}
139
+ {...props}
140
+ >
141
+ {body}
142
+ </p>
143
+ );
144
+ }
145
+
146
+ export {
147
+ useFormField,
148
+ Form,
149
+ FormItem,
150
+ FormLabel,
151
+ FormControl,
152
+ FormDescription,
153
+ FormMessage,
154
+ FormField,
155
+ };
@@ -0,0 +1,67 @@
1
+ import { cn } from "@icomponents/react/lib/utils";
2
+ import { OTPInput, OTPInputContext } from "input-otp";
3
+ import { MinusIcon } from "lucide-react";
4
+ import * as React from "react";
5
+
6
+ function InputOTP({
7
+ className,
8
+ containerClassName,
9
+ ...props
10
+ }: React.ComponentProps<typeof OTPInput> & {
11
+ containerClassName?: string;
12
+ }) {
13
+ return (
14
+ <OTPInput
15
+ data-slot="input-otp"
16
+ containerClassName={cn("flex items-center gap-2 has-disabled:opacity-50", containerClassName)}
17
+ className={cn("disabled:cursor-not-allowed", className)}
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
24
+ return (
25
+ <div data-slot="input-otp-group" className={cn("flex items-center", className)} {...props} />
26
+ );
27
+ }
28
+
29
+ function InputOTPSlot({
30
+ index,
31
+ className,
32
+ ...props
33
+ }: React.ComponentProps<"div"> & {
34
+ index: number;
35
+ }) {
36
+ const inputOTPContext = React.useContext(OTPInputContext);
37
+ const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
38
+
39
+ return (
40
+ <div
41
+ data-slot="input-otp-slot"
42
+ data-active={isActive}
43
+ className={cn(
44
+ "relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md aria-invalid:border-destructive data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-[3px] data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:border-destructive data-[active=true]:aria-invalid:ring-destructive/20 dark:bg-input/30 dark:data-[active=true]:aria-invalid:ring-destructive/40",
45
+ className
46
+ )}
47
+ {...props}
48
+ >
49
+ {char}
50
+ {hasFakeCaret && (
51
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
52
+ <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
53
+ </div>
54
+ )}
55
+ </div>
56
+ );
57
+ }
58
+
59
+ function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
60
+ return (
61
+ <div data-slot="input-otp-separator" role="separator" {...props}>
62
+ <MinusIcon />
63
+ </div>
64
+ );
65
+ }
66
+
67
+ export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
@@ -0,0 +1,20 @@
1
+ import { cn } from "@icomponents/react/lib/utils";
2
+ import * as React from "react";
3
+
4
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
5
+ return (
6
+ <input
7
+ type={type}
8
+ data-slot="input"
9
+ className={cn(
10
+ "flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
11
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
12
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ );
18
+ }
19
+
20
+ export { Input };
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ import { cn } from "@icomponents/react/lib/utils";
4
+ import * as React from "react";
5
+
6
+ // Base UI has no Label primitive; the shadcn wrapper migrates to a native <label> (per the
7
+ // migrate-radix-to-base skill). htmlFor/children behave identically; the utility classes are unchanged.
8
+ function Label({ className, ...props }: React.ComponentProps<"label">) {
9
+ return (
10
+ <label
11
+ data-slot="label"
12
+ className={cn(
13
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
14
+ className
15
+ )}
16
+ {...props}
17
+ />
18
+ );
19
+ }
20
+
21
+ export { Label };
@@ -0,0 +1,54 @@
1
+ import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
2
+ import { cn } from "@icomponents/react/lib/utils";
3
+ import * as React from "react";
4
+
5
+ function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
6
+ return <PopoverPrimitive.Root data-slot="popover" {...props} />;
7
+ }
8
+
9
+ function PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
10
+ return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
11
+ }
12
+
13
+ function PopoverContent({
14
+ className,
15
+ align = "center",
16
+ sideOffset = 4,
17
+ side,
18
+ alignOffset,
19
+ ...props
20
+ }: React.ComponentProps<typeof PopoverPrimitive.Popup> &
21
+ Pick<
22
+ React.ComponentProps<typeof PopoverPrimitive.Positioner>,
23
+ "side" | "align" | "sideOffset" | "alignOffset"
24
+ >) {
25
+ return (
26
+ <PopoverPrimitive.Portal>
27
+ {/* Radix Content → Portal > Positioner > Popup; positioning lives on Positioner. */}
28
+ <PopoverPrimitive.Positioner
29
+ align={align}
30
+ side={side}
31
+ sideOffset={sideOffset}
32
+ alignOffset={alignOffset}
33
+ className="isolate z-50"
34
+ >
35
+ <PopoverPrimitive.Popup
36
+ data-slot="popover-content"
37
+ className={cn(
38
+ "z-50 w-72 origin-(--transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95",
39
+ className
40
+ )}
41
+ {...props}
42
+ />
43
+ </PopoverPrimitive.Positioner>
44
+ </PopoverPrimitive.Portal>
45
+ );
46
+ }
47
+
48
+ // Base UI has no Popover.Anchor part (anchoring is the Positioner `anchor` prop). Unused in this app —
49
+ // kept as an inert passthrough span so the export stays; flag if real anchoring is ever needed.
50
+ function PopoverAnchor({ ...props }: React.ComponentProps<"span">) {
51
+ return <span data-slot="popover-anchor" {...props} />;
52
+ }
53
+
54
+ export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
@@ -0,0 +1,37 @@
1
+ import { Radio as RadioPrimitive } from "@base-ui/react/radio";
2
+ import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
3
+ import { cn } from "@icomponents/react/lib/utils";
4
+ import { CircleIcon } from "lucide-react";
5
+ import * as React from "react";
6
+
7
+ function RadioGroup({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive>) {
8
+ return (
9
+ <RadioGroupPrimitive
10
+ data-slot="radio-group"
11
+ className={cn("grid gap-3", className)}
12
+ {...props}
13
+ />
14
+ );
15
+ }
16
+
17
+ function RadioGroupItem({ className, ...props }: React.ComponentProps<typeof RadioPrimitive.Root>) {
18
+ return (
19
+ <RadioPrimitive.Root
20
+ data-slot="radio-group-item"
21
+ className={cn(
22
+ "aspect-square size-4 shrink-0 rounded-full border border-input text-primary shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
23
+ className
24
+ )}
25
+ {...props}
26
+ >
27
+ <RadioPrimitive.Indicator
28
+ data-slot="radio-group-indicator"
29
+ className="relative flex items-center justify-center"
30
+ >
31
+ <CircleIcon className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-primary" />
32
+ </RadioPrimitive.Indicator>
33
+ </RadioPrimitive.Root>
34
+ );
35
+ }
36
+
37
+ export { RadioGroup, RadioGroupItem };
@@ -0,0 +1,57 @@
1
+ import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
2
+ import { cn } from "@icomponents/react/lib/utils";
3
+ import * as React from "react";
4
+
5
+ interface ScrollAreaProps extends React.ComponentProps<typeof ScrollAreaPrimitive.Root> {
6
+ orientation?: "vertical" | "horizontal";
7
+ }
8
+
9
+ function ScrollArea({ className, children, orientation = "vertical", ...props }: ScrollAreaProps) {
10
+ return (
11
+ <ScrollAreaPrimitive.Root
12
+ data-slot="scroll-area"
13
+ className={cn("relative", className)}
14
+ {...props}
15
+ >
16
+ <ScrollAreaPrimitive.Viewport
17
+ data-slot="scroll-area-viewport"
18
+ className={cn(
19
+ "size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1",
20
+ orientation === "horizontal" && "overflow-x-auto!"
21
+ )}
22
+ >
23
+ {/* Base UI adds Content inside Viewport (needed for overflow measurement). */}
24
+ <ScrollAreaPrimitive.Content>{children}</ScrollAreaPrimitive.Content>
25
+ </ScrollAreaPrimitive.Viewport>
26
+ <ScrollBar orientation={orientation} />
27
+ <ScrollAreaPrimitive.Corner />
28
+ </ScrollAreaPrimitive.Root>
29
+ );
30
+ }
31
+
32
+ function ScrollBar({
33
+ className,
34
+ orientation = "vertical",
35
+ ...props
36
+ }: React.ComponentProps<typeof ScrollAreaPrimitive.Scrollbar>) {
37
+ return (
38
+ <ScrollAreaPrimitive.Scrollbar
39
+ data-slot="scroll-area-scrollbar"
40
+ orientation={orientation}
41
+ className={cn(
42
+ "flex touch-none p-px transition-colors select-none",
43
+ orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent",
44
+ orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent",
45
+ className
46
+ )}
47
+ {...props}
48
+ >
49
+ <ScrollAreaPrimitive.Thumb
50
+ data-slot="scroll-area-thumb"
51
+ className="relative flex-1 rounded-full bg-border"
52
+ />
53
+ </ScrollAreaPrimitive.Scrollbar>
54
+ );
55
+ }
56
+
57
+ export { ScrollArea, ScrollBar };
@@ -0,0 +1,213 @@
1
+ import { Select as SelectPrimitive } from "@base-ui/react/select";
2
+ import { cn } from "@icomponents/react/lib/utils";
3
+ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
4
+ import * as React from "react";
5
+
6
+ // Base UI Select.Root.Props is generic (<Value, Multiple>). Keep the Radix-style single-value API on
7
+ // the wrapper (`value`/`defaultValue`/`onValueChange(value: string)`) and adapt onValueChange to Base
8
+ // UI's widened `(value: string | null, eventDetails)` signature internally — so consumers don't change.
9
+ type SelectProps = {
10
+ value?: string;
11
+ defaultValue?: string;
12
+ onValueChange?: (value: string) => void;
13
+ disabled?: boolean;
14
+ name?: string;
15
+ required?: boolean;
16
+ open?: boolean;
17
+ defaultOpen?: boolean;
18
+ onOpenChange?: (open: boolean) => void;
19
+ children?: React.ReactNode;
20
+ };
21
+
22
+ function Select({ onValueChange, ...props }: SelectProps) {
23
+ return (
24
+ <SelectPrimitive.Root
25
+ data-slot="select"
26
+ onValueChange={
27
+ onValueChange ? (value) => onValueChange(value == null ? "" : String(value)) : undefined
28
+ }
29
+ {...props}
30
+ />
31
+ );
32
+ }
33
+
34
+ function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
35
+ return <SelectPrimitive.Group data-slot="select-group" {...props} />;
36
+ }
37
+
38
+ // Base UI Select.Value has no `placeholder` prop — emulate it with the children render-fn (shows the
39
+ // placeholder when there is no value). NOTE: with no value→label map, a set value renders its raw
40
+ // value; selects here mostly use value===label (page size, status, sort) so it matches. (flag)
41
+ function SelectValue({
42
+ placeholder,
43
+ children,
44
+ ...props
45
+ }: React.ComponentProps<typeof SelectPrimitive.Value> & { placeholder?: React.ReactNode }) {
46
+ return (
47
+ <SelectPrimitive.Value data-slot="select-value" {...props}>
48
+ {children ??
49
+ ((value: unknown) =>
50
+ value == null || value === "" ? placeholder : (value as React.ReactNode))}
51
+ </SelectPrimitive.Value>
52
+ );
53
+ }
54
+
55
+ function SelectTrigger({
56
+ className,
57
+ size = "default",
58
+ children,
59
+ ...props
60
+ }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
61
+ size?: "sm" | "default";
62
+ }) {
63
+ return (
64
+ <SelectPrimitive.Trigger
65
+ data-slot="select-trigger"
66
+ data-size={size}
67
+ className={cn(
68
+ "flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
69
+ className
70
+ )}
71
+ {...props}
72
+ >
73
+ {children}
74
+ <SelectPrimitive.Icon render={<ChevronDownIcon className="size-4 opacity-50" />} />
75
+ </SelectPrimitive.Trigger>
76
+ );
77
+ }
78
+
79
+ function SelectContent({
80
+ className,
81
+ children,
82
+ sideOffset = 4,
83
+ alignItemWithTrigger = false,
84
+ side,
85
+ align,
86
+ alignOffset,
87
+ ...props
88
+ }: React.ComponentProps<typeof SelectPrimitive.Popup> &
89
+ Pick<
90
+ React.ComponentProps<typeof SelectPrimitive.Positioner>,
91
+ "side" | "align" | "sideOffset" | "alignOffset" | "alignItemWithTrigger"
92
+ >) {
93
+ return (
94
+ <SelectPrimitive.Portal>
95
+ {/* Radix Content → Portal > Positioner > Popup; Viewport → List. position="popper" = alignItemWithTrigger={false}. */}
96
+ <SelectPrimitive.Positioner
97
+ side={side}
98
+ align={align}
99
+ sideOffset={sideOffset}
100
+ alignOffset={alignOffset}
101
+ alignItemWithTrigger={alignItemWithTrigger}
102
+ >
103
+ <SelectPrimitive.Popup
104
+ data-slot="select-content"
105
+ className={cn(
106
+ "relative isolate z-50 max-h-(--available-height) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95",
107
+ className
108
+ )}
109
+ {...props}
110
+ >
111
+ <SelectScrollUpButton />
112
+ <SelectPrimitive.List className="p-1">{children}</SelectPrimitive.List>
113
+ <SelectScrollDownButton />
114
+ </SelectPrimitive.Popup>
115
+ </SelectPrimitive.Positioner>
116
+ </SelectPrimitive.Portal>
117
+ );
118
+ }
119
+
120
+ // Base UI Select.GroupLabel requires a <Select.Group> ancestor; render a plain <div> so a standalone
121
+ // <SelectLabel> (shadcn's common usage) does not crash.
122
+ function SelectLabel({ className, ...props }: React.ComponentProps<"div">) {
123
+ return (
124
+ <div
125
+ data-slot="select-label"
126
+ className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
127
+ {...props}
128
+ />
129
+ );
130
+ }
131
+
132
+ function SelectItem({
133
+ className,
134
+ children,
135
+ ...props
136
+ }: React.ComponentProps<typeof SelectPrimitive.Item>) {
137
+ return (
138
+ <SelectPrimitive.Item
139
+ data-slot="select-item"
140
+ className={cn(
141
+ "relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 ps-2 pe-8 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
142
+ className
143
+ )}
144
+ {...props}
145
+ >
146
+ <span className="absolute inset-e-2 flex size-3.5 items-center justify-center">
147
+ <SelectPrimitive.ItemIndicator>
148
+ <CheckIcon className="size-4" />
149
+ </SelectPrimitive.ItemIndicator>
150
+ </span>
151
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
152
+ </SelectPrimitive.Item>
153
+ );
154
+ }
155
+
156
+ function SelectSeparator({
157
+ className,
158
+ ...props
159
+ }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
160
+ return (
161
+ <SelectPrimitive.Separator
162
+ data-slot="select-separator"
163
+ className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
164
+ {...props}
165
+ />
166
+ );
167
+ }
168
+
169
+ function SelectScrollUpButton({
170
+ className,
171
+ ...props
172
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
173
+ return (
174
+ <SelectPrimitive.ScrollUpArrow
175
+ data-slot="select-scroll-up-button"
176
+ className={cn("top-0 flex w-full cursor-default items-center justify-center py-1", className)}
177
+ {...props}
178
+ >
179
+ <ChevronUpIcon className="size-4" />
180
+ </SelectPrimitive.ScrollUpArrow>
181
+ );
182
+ }
183
+
184
+ function SelectScrollDownButton({
185
+ className,
186
+ ...props
187
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
188
+ return (
189
+ <SelectPrimitive.ScrollDownArrow
190
+ data-slot="select-scroll-down-button"
191
+ className={cn(
192
+ "bottom-0 flex w-full cursor-default items-center justify-center py-1",
193
+ className
194
+ )}
195
+ {...props}
196
+ >
197
+ <ChevronDownIcon className="size-4" />
198
+ </SelectPrimitive.ScrollDownArrow>
199
+ );
200
+ }
201
+
202
+ export {
203
+ Select,
204
+ SelectContent,
205
+ SelectGroup,
206
+ SelectItem,
207
+ SelectLabel,
208
+ SelectScrollDownButton,
209
+ SelectScrollUpButton,
210
+ SelectSeparator,
211
+ SelectTrigger,
212
+ SelectValue,
213
+ };
@@ -0,0 +1,23 @@
1
+ import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
2
+ import { cn } from "@icomponents/react/lib/utils";
3
+ import * as React from "react";
4
+
5
+ function Separator({
6
+ className,
7
+ orientation = "horizontal",
8
+ ...props
9
+ }: React.ComponentProps<typeof SeparatorPrimitive>) {
10
+ return (
11
+ <SeparatorPrimitive
12
+ data-slot="separator"
13
+ orientation={orientation}
14
+ className={cn(
15
+ "shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ export { Separator };