@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.
- package/README.md +40 -0
- package/package.json +165 -0
- package/src/assets/custom/icon-dir.tsx +105 -0
- package/src/assets/custom/icon-layout-compact.tsx +84 -0
- package/src/assets/custom/icon-layout-default.tsx +48 -0
- package/src/assets/custom/icon-layout-full.tsx +68 -0
- package/src/assets/custom/icon-sidebar-floating.tsx +62 -0
- package/src/assets/custom/icon-sidebar-inset.tsx +53 -0
- package/src/assets/custom/icon-sidebar-sidebar.tsx +48 -0
- package/src/assets/custom/icon-theme-dark.tsx +36 -0
- package/src/assets/custom/icon-theme-light.tsx +35 -0
- package/src/assets/custom/icon-theme-system.tsx +113 -0
- package/src/components/admin-page-header.tsx +29 -0
- package/src/components/color-theme-switch.tsx +93 -0
- package/src/components/coming-soon.tsx +16 -0
- package/src/components/command-menu.tsx +98 -0
- package/src/components/config-drawer.tsx +401 -0
- package/src/components/confirm-dialog.dom.test.tsx +60 -0
- package/src/components/confirm-dialog.tsx +75 -0
- package/src/components/data-table/bulk-actions.tsx +193 -0
- package/src/components/data-table/column-header.tsx +66 -0
- package/src/components/data-table/data-table.dom.test.tsx +54 -0
- package/src/components/data-table/data-table.tsx +189 -0
- package/src/components/data-table/faceted-filter.tsx +156 -0
- package/src/components/data-table/index.ts +5 -0
- package/src/components/data-table/pagination.tsx +127 -0
- package/src/components/data-table/tanstack-table.d.ts +12 -0
- package/src/components/data-table/toolbar.tsx +88 -0
- package/src/components/data-table/view-options.tsx +49 -0
- package/src/components/date-picker.tsx +39 -0
- package/src/components/detail-card.tsx +63 -0
- package/src/components/language-switch.tsx +38 -0
- package/src/components/layout/app-sidebar.tsx +59 -0
- package/src/components/layout/app-title.tsx +62 -0
- package/src/components/layout/header.tsx +61 -0
- package/src/components/layout/main.tsx +34 -0
- package/src/components/layout/nav-group.tsx +185 -0
- package/src/components/layout/nav-user.tsx +115 -0
- package/src/components/layout/screen-header.tsx +119 -0
- package/src/components/layout/team-switcher.tsx +84 -0
- package/src/components/layout/top-nav.tsx +68 -0
- package/src/components/layout/types.ts +44 -0
- package/src/components/learn-more.tsx +35 -0
- package/src/components/long-text.tsx +80 -0
- package/src/components/navigation-progress.tsx +18 -0
- package/src/components/password-input.tsx +36 -0
- package/src/components/profile-dropdown.tsx +106 -0
- package/src/components/search.tsx +38 -0
- package/src/components/select-dropdown.tsx +62 -0
- package/src/components/sign-out-dialog.tsx +31 -0
- package/src/components/skip-to-main.tsx +10 -0
- package/src/components/stat-tile.tsx +56 -0
- package/src/components/status-badge.test.ts +23 -0
- package/src/components/status-badge.tsx +49 -0
- package/src/components/theme-switch.tsx +61 -0
- package/src/components/ui/alert-dialog.tsx +134 -0
- package/src/components/ui/alert.tsx +59 -0
- package/src/components/ui/avatar.tsx +38 -0
- package/src/components/ui/badge.tsx +39 -0
- package/src/components/ui/base-ui-state-attrs.dom.test.tsx +80 -0
- package/src/components/ui/button.tsx +51 -0
- package/src/components/ui/calendar.tsx +174 -0
- package/src/components/ui/card.tsx +74 -0
- package/src/components/ui/checkbox.tsx +26 -0
- package/src/components/ui/collapsible.tsx +18 -0
- package/src/components/ui/command.tsx +160 -0
- package/src/components/ui/dialog.tsx +129 -0
- package/src/components/ui/dropdown-menu.tsx +254 -0
- package/src/components/ui/form.tsx +155 -0
- package/src/components/ui/input-otp.tsx +67 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/label.tsx +21 -0
- package/src/components/ui/popover.tsx +54 -0
- package/src/components/ui/radio-group.tsx +37 -0
- package/src/components/ui/scroll-area.tsx +57 -0
- package/src/components/ui/select.tsx +213 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +128 -0
- package/src/components/ui/sidebar.tsx +702 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +26 -0
- package/src/components/ui/switch.tsx +25 -0
- package/src/components/ui/table.tsx +89 -0
- package/src/components/ui/tabs.tsx +51 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/tooltip.tsx +65 -0
- package/src/config/fonts.ts +2 -0
- package/src/context/direction-provider.tsx +61 -0
- package/src/context/font-provider.tsx +70 -0
- package/src/context/language-provider.tsx +73 -0
- package/src/context/layout-provider.tsx +81 -0
- package/src/context/search-provider.tsx +48 -0
- package/src/context/theme-provider.tsx +108 -0
- package/src/hooks/sort-url.test.ts +16 -0
- package/src/hooks/use-dialog-state.tsx +15 -0
- package/src/hooks/use-mobile.tsx +16 -0
- package/src/hooks/use-table-url-state.ts +243 -0
- package/src/i18n/common.ts +164 -0
- package/src/i18n/create-i18n.test.ts +50 -0
- package/src/i18n/create-i18n.ts +67 -0
- package/src/i18n/types.ts +22 -0
- package/src/i18n/use-translation.ts +6 -0
- package/src/lib/clipboard.test.ts +46 -0
- package/src/lib/clipboard.ts +29 -0
- package/src/lib/cookies.ts +39 -0
- package/src/lib/format-cell.test.ts +57 -0
- package/src/lib/format-cell.ts +63 -0
- package/src/lib/handle-server-error.test.ts +85 -0
- package/src/lib/handle-server-error.ts +39 -0
- package/src/lib/random-id.ts +13 -0
- package/src/lib/show-submitted-data.tsx +14 -0
- package/src/lib/utils.test.ts +35 -0
- package/src/lib/utils.ts +78 -0
- package/src/secure-context.contract.test.ts +63 -0
- package/src/testids.contract.test.ts +113 -0
- package/src/testids.ts +8 -0
- 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 };
|