@openconsole/shadcn 0.0.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/accordion.tsx +66 -0
- package/alert-dialog.tsx +196 -0
- package/alert.tsx +66 -0
- package/aspect-ratio.tsx +11 -0
- package/avatar.tsx +53 -0
- package/badge.tsx +46 -0
- package/breadcrumb.tsx +109 -0
- package/button-group.tsx +83 -0
- package/button.tsx +60 -0
- package/calendar.tsx +219 -0
- package/card.tsx +92 -0
- package/carousel.tsx +241 -0
- package/chart.tsx +374 -0
- package/checkbox.tsx +32 -0
- package/collapsible.tsx +33 -0
- package/command.tsx +184 -0
- package/context-menu.tsx +252 -0
- package/dialog.tsx +143 -0
- package/direction.tsx +22 -0
- package/drawer.tsx +135 -0
- package/dropdown-menu.tsx +257 -0
- package/empty.tsx +104 -0
- package/field.tsx +248 -0
- package/form.tsx +167 -0
- package/hooks/index.ts +1 -0
- package/hooks/use-mobile.ts +19 -0
- package/hover-card.tsx +44 -0
- package/icon.tsx +21 -0
- package/index.ts +59 -0
- package/input-group.tsx +170 -0
- package/input-otp.tsx +77 -0
- package/input.tsx +21 -0
- package/item.tsx +193 -0
- package/kbd.tsx +28 -0
- package/label.tsx +24 -0
- package/lib/index.ts +1 -0
- package/lib/utils.ts +6 -0
- package/menubar.tsx +276 -0
- package/native-select.tsx +62 -0
- package/navigation-menu.tsx +168 -0
- package/package.json +50 -0
- package/pagination.tsx +127 -0
- package/popover.tsx +89 -0
- package/progress.tsx +31 -0
- package/radio-group.tsx +45 -0
- package/resizable.tsx +53 -0
- package/scroll-area.tsx +58 -0
- package/select.tsx +187 -0
- package/separator.tsx +28 -0
- package/sheet.tsx +139 -0
- package/sidebar.tsx +724 -0
- package/skeleton.tsx +13 -0
- package/skill/SKILL.md +599 -0
- package/skill/customization.md +263 -0
- package/skill/rules/base-vs-radix.md +167 -0
- package/skill/rules/composition.md +240 -0
- package/skill/rules/forms.md +271 -0
- package/skill/rules/icons.md +136 -0
- package/skill/rules/styling.md +180 -0
- package/slider.tsx +63 -0
- package/sonner.tsx +40 -0
- package/spinner.tsx +16 -0
- package/switch.tsx +35 -0
- package/table.tsx +116 -0
- package/tabs.tsx +66 -0
- package/textarea.tsx +18 -0
- package/toggle-group.tsx +83 -0
- package/toggle.tsx +47 -0
- package/tooltip.tsx +61 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
package/form.tsx
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Label as LabelPrimitive, Slot } from "radix-ui";
|
|
5
|
+
import {
|
|
6
|
+
Controller,
|
|
7
|
+
FormProvider,
|
|
8
|
+
useFormContext,
|
|
9
|
+
useFormState,
|
|
10
|
+
type ControllerProps,
|
|
11
|
+
type FieldPath,
|
|
12
|
+
type FieldValues,
|
|
13
|
+
} from "react-hook-form";
|
|
14
|
+
|
|
15
|
+
import { cn } from "./lib/utils";
|
|
16
|
+
import { Label } from "./label";
|
|
17
|
+
|
|
18
|
+
const Form = FormProvider;
|
|
19
|
+
|
|
20
|
+
type FormFieldContextValue<
|
|
21
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
22
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
23
|
+
> = {
|
|
24
|
+
name: TName;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const FormFieldContext = React.createContext<FormFieldContextValue | null>(
|
|
28
|
+
null,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const FormField = <
|
|
32
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
33
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
34
|
+
>({
|
|
35
|
+
...props
|
|
36
|
+
}: ControllerProps<TFieldValues, TName>) => {
|
|
37
|
+
return (
|
|
38
|
+
<FormFieldContext.Provider value={{ name: props.name }}>
|
|
39
|
+
<Controller {...props} />
|
|
40
|
+
</FormFieldContext.Provider>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type FormItemContextValue = {
|
|
45
|
+
id: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const FormItemContext = React.createContext<FormItemContextValue | null>(null);
|
|
49
|
+
|
|
50
|
+
const useFormField = () => {
|
|
51
|
+
const fieldContext = React.useContext(FormFieldContext);
|
|
52
|
+
const itemContext = React.useContext(FormItemContext);
|
|
53
|
+
if (!fieldContext) {
|
|
54
|
+
throw new Error("useFormField should be used within <FormField>");
|
|
55
|
+
}
|
|
56
|
+
if (!itemContext) {
|
|
57
|
+
throw new Error("useFormField should be used within <FormItem>");
|
|
58
|
+
}
|
|
59
|
+
const { getFieldState } = useFormContext();
|
|
60
|
+
const formState = useFormState({ name: fieldContext.name });
|
|
61
|
+
const fieldState = getFieldState(fieldContext.name, formState);
|
|
62
|
+
|
|
63
|
+
const { id } = itemContext;
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
id,
|
|
67
|
+
name: fieldContext.name,
|
|
68
|
+
formItemId: `${id}-form-item`,
|
|
69
|
+
formDescriptionId: `${id}-form-item-description`,
|
|
70
|
+
formMessageId: `${id}-form-item-message`,
|
|
71
|
+
...fieldState,
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
|
76
|
+
const id = React.useId();
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<FormItemContext.Provider value={{ id }}>
|
|
80
|
+
<div
|
|
81
|
+
data-slot="form-item"
|
|
82
|
+
className={cn("grid gap-2", className)}
|
|
83
|
+
{...props}
|
|
84
|
+
/>
|
|
85
|
+
</FormItemContext.Provider>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function FormLabel({
|
|
90
|
+
className,
|
|
91
|
+
...props
|
|
92
|
+
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
93
|
+
const { error, formItemId } = useFormField();
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<Label
|
|
97
|
+
data-slot="form-label"
|
|
98
|
+
data-error={!!error}
|
|
99
|
+
className={cn("data-[error=true]:text-destructive", className)}
|
|
100
|
+
htmlFor={formItemId}
|
|
101
|
+
{...props}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function FormControl({ ...props }: React.ComponentProps<typeof Slot.Root>) {
|
|
107
|
+
const { error, formItemId, formDescriptionId, formMessageId } =
|
|
108
|
+
useFormField();
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<Slot.Root
|
|
112
|
+
data-slot="form-control"
|
|
113
|
+
id={formItemId}
|
|
114
|
+
aria-describedby={
|
|
115
|
+
!error
|
|
116
|
+
? `${formDescriptionId}`
|
|
117
|
+
: `${formDescriptionId} ${formMessageId}`
|
|
118
|
+
}
|
|
119
|
+
aria-invalid={!!error}
|
|
120
|
+
{...props}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
126
|
+
const { formDescriptionId } = useFormField();
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<p
|
|
130
|
+
data-slot="form-description"
|
|
131
|
+
id={formDescriptionId}
|
|
132
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
133
|
+
{...props}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
|
139
|
+
const { error, formMessageId } = useFormField();
|
|
140
|
+
const body = error ? String(error?.message ?? "") : props.children;
|
|
141
|
+
|
|
142
|
+
if (!body) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<p
|
|
148
|
+
data-slot="form-message"
|
|
149
|
+
id={formMessageId}
|
|
150
|
+
className={cn("text-sm text-destructive", className)}
|
|
151
|
+
{...props}
|
|
152
|
+
>
|
|
153
|
+
{body}
|
|
154
|
+
</p>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export {
|
|
159
|
+
useFormField,
|
|
160
|
+
Form,
|
|
161
|
+
FormItem,
|
|
162
|
+
FormLabel,
|
|
163
|
+
FormControl,
|
|
164
|
+
FormDescription,
|
|
165
|
+
FormMessage,
|
|
166
|
+
FormField,
|
|
167
|
+
};
|
package/hooks/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./use-mobile";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768;
|
|
4
|
+
|
|
5
|
+
export function useIsMobile() {
|
|
6
|
+
const [isMobile, setIsMobile] = React.useState<boolean>(false);
|
|
7
|
+
|
|
8
|
+
React.useEffect(() => {
|
|
9
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
10
|
+
const onChange = (event: MediaQueryListEvent | MediaQueryList) => {
|
|
11
|
+
setIsMobile(event.matches);
|
|
12
|
+
};
|
|
13
|
+
onChange(mql);
|
|
14
|
+
mql.addEventListener("change", onChange);
|
|
15
|
+
return () => mql.removeEventListener("change", onChange);
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
return isMobile;
|
|
19
|
+
}
|
package/hover-card.tsx
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { HoverCard as HoverCardPrimitive } from "radix-ui"
|
|
5
|
+
|
|
6
|
+
import { cn } from "./lib/utils"
|
|
7
|
+
|
|
8
|
+
function HoverCard({
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
|
11
|
+
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function HoverCardTrigger({
|
|
15
|
+
...props
|
|
16
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
|
17
|
+
return (
|
|
18
|
+
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function HoverCardContent({
|
|
23
|
+
className,
|
|
24
|
+
align = "center",
|
|
25
|
+
sideOffset = 4,
|
|
26
|
+
...props
|
|
27
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
|
28
|
+
return (
|
|
29
|
+
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
|
|
30
|
+
<HoverCardPrimitive.Content
|
|
31
|
+
data-slot="hover-card-content"
|
|
32
|
+
align={align}
|
|
33
|
+
sideOffset={sideOffset}
|
|
34
|
+
className={cn(
|
|
35
|
+
"z-50 w-64 origin-(--radix-hover-card-content-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-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
</HoverCardPrimitive.Portal>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
package/icon.tsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { icons as lucideIcons, type LucideProps } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
interface IconProps extends LucideProps {
|
|
6
|
+
/** lucide-react icon name in PascalCase (e.g. "LayoutDashboard"). */
|
|
7
|
+
name?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Render a lucide-react icon by name.
|
|
12
|
+
*
|
|
13
|
+
* Sider data keeps icon references as plain strings so it stays serializable
|
|
14
|
+
* across the React Server / Client boundary. The actual component lookup
|
|
15
|
+
* happens here, inside the client.
|
|
16
|
+
*/
|
|
17
|
+
export function Icon({ name, ...props }: IconProps) {
|
|
18
|
+
if (!name) return null;
|
|
19
|
+
const LucideIcon = lucideIcons[name as keyof typeof lucideIcons];
|
|
20
|
+
return LucideIcon ? <LucideIcon {...props} /> : null;
|
|
21
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export * from "./lib";
|
|
2
|
+
export * from "./hooks";
|
|
3
|
+
|
|
4
|
+
export * from "./accordion";
|
|
5
|
+
export * from "./alert";
|
|
6
|
+
export * from "./alert-dialog";
|
|
7
|
+
export * from "./aspect-ratio";
|
|
8
|
+
export * from "./avatar";
|
|
9
|
+
export * from "./badge";
|
|
10
|
+
export * from "./breadcrumb";
|
|
11
|
+
export * from "./button";
|
|
12
|
+
export * from "./button-group";
|
|
13
|
+
export * from "./calendar";
|
|
14
|
+
export * from "./card";
|
|
15
|
+
export * from "./carousel";
|
|
16
|
+
export * from "./chart";
|
|
17
|
+
export * from "./checkbox";
|
|
18
|
+
export * from "./collapsible";
|
|
19
|
+
export * from "./command";
|
|
20
|
+
export * from "./context-menu";
|
|
21
|
+
export * from "./dialog";
|
|
22
|
+
export * from "./direction";
|
|
23
|
+
export * from "./drawer";
|
|
24
|
+
export * from "./dropdown-menu";
|
|
25
|
+
export * from "./empty";
|
|
26
|
+
export * from "./field";
|
|
27
|
+
export * from "./form";
|
|
28
|
+
export * from "./hover-card";
|
|
29
|
+
export * from "./icon";
|
|
30
|
+
export * from "./input";
|
|
31
|
+
export * from "./input-group";
|
|
32
|
+
export * from "./input-otp";
|
|
33
|
+
export * from "./item";
|
|
34
|
+
export * from "./kbd";
|
|
35
|
+
export * from "./label";
|
|
36
|
+
export * from "./menubar";
|
|
37
|
+
export * from "./native-select";
|
|
38
|
+
export * from "./navigation-menu";
|
|
39
|
+
export * from "./pagination";
|
|
40
|
+
export * from "./popover";
|
|
41
|
+
export * from "./progress";
|
|
42
|
+
export * from "./radio-group";
|
|
43
|
+
export * from "./resizable";
|
|
44
|
+
export * from "./scroll-area";
|
|
45
|
+
export * from "./select";
|
|
46
|
+
export * from "./separator";
|
|
47
|
+
export * from "./sheet";
|
|
48
|
+
export * from "./sidebar";
|
|
49
|
+
export * from "./skeleton";
|
|
50
|
+
export * from "./slider";
|
|
51
|
+
export * from "./sonner";
|
|
52
|
+
export * from "./spinner";
|
|
53
|
+
export * from "./switch";
|
|
54
|
+
export * from "./table";
|
|
55
|
+
export * from "./tabs";
|
|
56
|
+
export * from "./textarea";
|
|
57
|
+
export * from "./toggle";
|
|
58
|
+
export * from "./toggle-group";
|
|
59
|
+
export * from "./tooltip";
|
package/input-group.tsx
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
|
|
6
|
+
import { cn } from "./lib/utils"
|
|
7
|
+
import { Button } from "./button"
|
|
8
|
+
import { Input } from "./input"
|
|
9
|
+
import { Textarea } from "./textarea"
|
|
10
|
+
|
|
11
|
+
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
data-slot="input-group"
|
|
15
|
+
role="group"
|
|
16
|
+
className={cn(
|
|
17
|
+
"group/input-group relative flex w-full items-center rounded-md border border-input shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30",
|
|
18
|
+
"h-9 min-w-0 has-[>textarea]:h-auto",
|
|
19
|
+
|
|
20
|
+
// Variants based on alignment.
|
|
21
|
+
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
|
22
|
+
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
|
23
|
+
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
|
24
|
+
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
|
25
|
+
|
|
26
|
+
// Focus state.
|
|
27
|
+
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50",
|
|
28
|
+
|
|
29
|
+
// Error state.
|
|
30
|
+
"has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
|
31
|
+
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const inputGroupAddonVariants = cva(
|
|
40
|
+
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
|
|
41
|
+
{
|
|
42
|
+
variants: {
|
|
43
|
+
align: {
|
|
44
|
+
"inline-start":
|
|
45
|
+
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
|
46
|
+
"inline-end":
|
|
47
|
+
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
|
|
48
|
+
"block-start":
|
|
49
|
+
"order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3",
|
|
50
|
+
"block-end":
|
|
51
|
+
"order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
defaultVariants: {
|
|
55
|
+
align: "inline-start",
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
function InputGroupAddon({
|
|
61
|
+
className,
|
|
62
|
+
align = "inline-start",
|
|
63
|
+
...props
|
|
64
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
role="group"
|
|
68
|
+
data-slot="input-group-addon"
|
|
69
|
+
data-align={align}
|
|
70
|
+
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
71
|
+
onClick={(e) => {
|
|
72
|
+
if ((e.target as HTMLElement).closest("button")) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
e.currentTarget.parentElement?.querySelector("input")?.focus()
|
|
76
|
+
}}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const inputGroupButtonVariants = cva(
|
|
83
|
+
"flex items-center gap-2 text-sm shadow-none",
|
|
84
|
+
{
|
|
85
|
+
variants: {
|
|
86
|
+
size: {
|
|
87
|
+
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
|
|
88
|
+
sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
|
|
89
|
+
"icon-xs":
|
|
90
|
+
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
|
91
|
+
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
defaultVariants: {
|
|
95
|
+
size: "xs",
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
function InputGroupButton({
|
|
101
|
+
className,
|
|
102
|
+
type = "button",
|
|
103
|
+
variant = "ghost",
|
|
104
|
+
size = "xs",
|
|
105
|
+
...props
|
|
106
|
+
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
|
107
|
+
VariantProps<typeof inputGroupButtonVariants>) {
|
|
108
|
+
return (
|
|
109
|
+
<Button
|
|
110
|
+
type={type}
|
|
111
|
+
data-size={size}
|
|
112
|
+
variant={variant}
|
|
113
|
+
className={cn(inputGroupButtonVariants({ size }), className)}
|
|
114
|
+
{...props}
|
|
115
|
+
/>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
|
120
|
+
return (
|
|
121
|
+
<span
|
|
122
|
+
className={cn(
|
|
123
|
+
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
124
|
+
className
|
|
125
|
+
)}
|
|
126
|
+
{...props}
|
|
127
|
+
/>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function InputGroupInput({
|
|
132
|
+
className,
|
|
133
|
+
...props
|
|
134
|
+
}: React.ComponentProps<"input">) {
|
|
135
|
+
return (
|
|
136
|
+
<Input
|
|
137
|
+
data-slot="input-group-control"
|
|
138
|
+
className={cn(
|
|
139
|
+
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
|
|
140
|
+
className
|
|
141
|
+
)}
|
|
142
|
+
{...props}
|
|
143
|
+
/>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function InputGroupTextarea({
|
|
148
|
+
className,
|
|
149
|
+
...props
|
|
150
|
+
}: React.ComponentProps<"textarea">) {
|
|
151
|
+
return (
|
|
152
|
+
<Textarea
|
|
153
|
+
data-slot="input-group-control"
|
|
154
|
+
className={cn(
|
|
155
|
+
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
|
|
156
|
+
className
|
|
157
|
+
)}
|
|
158
|
+
{...props}
|
|
159
|
+
/>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export {
|
|
164
|
+
InputGroup,
|
|
165
|
+
InputGroupAddon,
|
|
166
|
+
InputGroupButton,
|
|
167
|
+
InputGroupText,
|
|
168
|
+
InputGroupInput,
|
|
169
|
+
InputGroupTextarea,
|
|
170
|
+
}
|
package/input-otp.tsx
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { OTPInput, OTPInputContext } from "input-otp"
|
|
5
|
+
import { MinusIcon } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "./lib/utils"
|
|
8
|
+
|
|
9
|
+
function InputOTP({
|
|
10
|
+
className,
|
|
11
|
+
containerClassName,
|
|
12
|
+
...props
|
|
13
|
+
}: React.ComponentProps<typeof OTPInput> & {
|
|
14
|
+
containerClassName?: string
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<OTPInput
|
|
18
|
+
data-slot="input-otp"
|
|
19
|
+
containerClassName={cn(
|
|
20
|
+
"flex items-center gap-2 has-disabled:opacity-50",
|
|
21
|
+
containerClassName
|
|
22
|
+
)}
|
|
23
|
+
className={cn("disabled:cursor-not-allowed", className)}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
data-slot="input-otp-group"
|
|
33
|
+
className={cn("flex items-center", className)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function InputOTPSlot({
|
|
40
|
+
index,
|
|
41
|
+
className,
|
|
42
|
+
...props
|
|
43
|
+
}: React.ComponentProps<"div"> & {
|
|
44
|
+
index: number
|
|
45
|
+
}) {
|
|
46
|
+
const inputOTPContext = React.useContext(OTPInputContext)
|
|
47
|
+
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
data-slot="input-otp-slot"
|
|
52
|
+
data-active={isActive}
|
|
53
|
+
className={cn(
|
|
54
|
+
"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",
|
|
55
|
+
className
|
|
56
|
+
)}
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
{char}
|
|
60
|
+
{hasFakeCaret && (
|
|
61
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
62
|
+
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
|
70
|
+
return (
|
|
71
|
+
<div data-slot="input-otp-separator" role="separator" {...props}>
|
|
72
|
+
<MinusIcon />
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
package/input.tsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./lib/utils";
|
|
4
|
+
|
|
5
|
+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
6
|
+
return (
|
|
7
|
+
<input
|
|
8
|
+
type={type}
|
|
9
|
+
data-slot="input"
|
|
10
|
+
className={cn(
|
|
11
|
+
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
12
|
+
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
13
|
+
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { Input };
|