mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-08 18:56:37 +08:00
feat(frontend): add shadcn ui primitives and compatibility bridge
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "src/styles/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||||
|
import { ChevronDownIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Accordion({ ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||||
|
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionItem({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
className={cn("border-b", className)}
|
||||||
|
data-slot="accordion-item"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionTrigger({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Header className="flex" data-slot="accordion-header">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="accordion-trigger"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDownIcon className="h-4 w-4 shrink-0 text-default-500 transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||||
|
data-slot="accordion-content"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"relative w-full rounded-lg border px-4 py-3 text-sm",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "border-default-200 bg-default-50/70 text-foreground",
|
||||||
|
destructive: "border-danger-200 bg-danger-50 text-danger-700 dark:text-danger-300",
|
||||||
|
success: "border-success-200 bg-success-50 text-success-700 dark:text-success-300",
|
||||||
|
warning: "border-warning-200 bg-warning-50 text-warning-700 dark:text-warning-300",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Alert({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(alertVariants({ className, variant }))}
|
||||||
|
data-slot="alert"
|
||||||
|
role="alert"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertTitle({ className, ...props }: React.ComponentProps<"h5">) {
|
||||||
|
return (
|
||||||
|
<h5
|
||||||
|
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||||
|
data-slot="alert-title"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div className={cn("text-sm opacity-90", className)} data-slot="alert-description" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Alert, AlertDescription, AlertTitle };
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "border-transparent bg-primary text-primary-foreground",
|
||||||
|
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
||||||
|
destructive: "border-transparent bg-danger text-white",
|
||||||
|
outline: "text-foreground",
|
||||||
|
success: "border-transparent bg-success text-white",
|
||||||
|
warning: "border-transparent bg-warning text-white",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(badgeVariants({ className, variant }))}
|
||||||
|
data-slot="badge"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants };
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive: "bg-danger text-white hover:bg-danger/90",
|
||||||
|
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
light: "bg-transparent hover:bg-default-100 dark:hover:bg-default-200/40",
|
||||||
|
flat: "bg-default-100 text-foreground hover:bg-default-200 dark:bg-default-100/40",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2",
|
||||||
|
sm: "h-8 rounded-md px-3 text-xs",
|
||||||
|
lg: "h-10 rounded-md px-8",
|
||||||
|
icon: "h-9 w-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
type ButtonProps = React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Button({ asChild = false, className, size, variant, ...props }: ButtonProps) {
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ className, size, variant }))}
|
||||||
|
data-slot="button"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants };
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"rounded-xl border border-default-200 bg-white text-card-foreground shadow-sm dark:bg-default-50/20",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="card"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("flex flex-col gap-1.5 p-6", className)}
|
||||||
|
data-slot="card-header"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"h3">) {
|
||||||
|
return (
|
||||||
|
<h3
|
||||||
|
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||||
|
data-slot="card-title"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
className={cn("text-sm text-default-500", className)}
|
||||||
|
data-slot="card-description"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div className={cn("p-6 pt-0", className)} data-slot="card-content" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
|
data-slot="card-footer"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||||
|
import { CheckIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
className={cn(
|
||||||
|
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="checkbox"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
className="flex items-center justify-center text-current"
|
||||||
|
data-slot="checkbox-indicator"
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-3.5 w-3.5" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Checkbox };
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
|
import { XIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="dialog-overlay"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
|
showCloseButton?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-default-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 sm:rounded-lg dark:bg-default-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="dialog-content"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none"
|
||||||
|
>
|
||||||
|
<XIcon className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
|
||||||
|
data-slot="dialog-header"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||||
|
data-slot="dialog-footer"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||||
|
data-slot="dialog-title"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
className={cn("text-sm text-default-500", className)}
|
||||||
|
data-slot="dialog-description"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
};
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
|
return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
|
return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuPortal({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
|
return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
|
return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-default-100 data-[state=open]:bg-default-100",
|
||||||
|
inset && "pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-32 overflow-hidden rounded-md border border-default-200 bg-white p-1 text-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out dark:bg-default-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 6,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-32 overflow-hidden rounded-md border border-default-200 bg-white p-1 text-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out dark:bg-default-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="dropdown-menu-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-default-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
inset && "pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="dropdown-menu-item"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
checked={checked}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-default-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-default-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CircleIcon className="h-2 w-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||||
|
data-slot="dropdown-menu-label"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-divider", className)}
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn("ml-auto text-xs tracking-widest text-default-500", className)}
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
className={cn(
|
||||||
|
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-default-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="input"
|
||||||
|
type={type}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input };
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
className={cn(
|
||||||
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="label"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label };
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Progress({
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
indicatorClassName,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ProgressPrimitive.Root> & {
|
||||||
|
indicatorClassName?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
className={cn("relative h-2 w-full overflow-hidden rounded-full bg-default-200", className)}
|
||||||
|
data-slot="progress"
|
||||||
|
value={value}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
className={cn("h-full w-full flex-1 bg-primary transition-all", indicatorClassName)}
|
||||||
|
data-slot="progress-indicator"
|
||||||
|
style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Progress };
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||||
|
import { CircleIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function RadioGroup({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Root
|
||||||
|
className={cn("grid gap-2", className)}
|
||||||
|
data-slot="radio-group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RadioGroupItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
className={cn(
|
||||||
|
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="radio-group-item"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<RadioGroupPrimitive.Indicator className="flex items-center justify-center" data-slot="radio-group-indicator">
|
||||||
|
<CircleIcon className="h-2.5 w-2.5 fill-current text-current" />
|
||||||
|
</RadioGroupPrimitive.Indicator>
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RadioGroup, RadioGroupItem };
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||||
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
|
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectTrigger({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
className={cn(
|
||||||
|
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-default-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="select-trigger"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDownIcon className="h-4 w-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollUpButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollDownButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
position = "popper",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
className={cn(
|
||||||
|
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-default-200 bg-white text-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out dark:bg-default-50",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="select-content"
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
||||||
|
data-slot="select-label"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-default-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="select-item"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-divider", className)}
|
||||||
|
data-slot="select-separator"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Separator({
|
||||||
|
className,
|
||||||
|
decorative = true,
|
||||||
|
orientation = "horizontal",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 bg-divider",
|
||||||
|
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="separator"
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Separator };
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SwitchPrimitive.Root
|
||||||
|
className={cn(
|
||||||
|
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-default-200",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="switch"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SwitchPrimitive.Thumb
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none block h-4 w-4 rounded-full bg-white ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
|
||||||
|
)}
|
||||||
|
data-slot="switch-thumb"
|
||||||
|
/>
|
||||||
|
</SwitchPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Switch };
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||||
|
return (
|
||||||
|
<div className="relative w-full overflow-auto" data-slot="table-wrapper">
|
||||||
|
<table className={cn("w-full caption-bottom text-sm", className)} data-slot="table" {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||||
|
return (
|
||||||
|
<thead
|
||||||
|
className={cn("[&_tr]:border-b", className)}
|
||||||
|
data-slot="table-header"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||||
|
return (
|
||||||
|
<tbody
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
data-slot="table-body"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||||
|
return (
|
||||||
|
<tfoot
|
||||||
|
className={cn("border-t bg-default-50/70 font-medium [&>tr]:last:border-b-0", className)}
|
||||||
|
data-slot="table-footer"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
className={cn("border-b transition-colors hover:bg-default-50/50", className)}
|
||||||
|
data-slot="table-row"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
className={cn(
|
||||||
|
"h-10 px-2 text-left align-middle font-medium text-default-600 [&:has([role=checkbox])]:pr-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="table-head"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
className={cn("p-2 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||||
|
data-slot="table-cell"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCaption({ className, ...props }: React.ComponentProps<"caption">) {
|
||||||
|
return (
|
||||||
|
<caption className={cn("mt-4 text-sm text-default-500", className)} data-slot="table-caption" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
|
TableCell,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Root className={cn("flex flex-col gap-2", className)} data-slot="tabs" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-9 items-center justify-center rounded-lg bg-default-100 p-1 text-default-500",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="tabs-list"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-foreground data-[state=active]:shadow-sm dark:data-[state=active]:bg-default-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="tabs-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
className={cn("outline-none", className)}
|
||||||
|
data-slot="tabs-content"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tabs, TabsContent, TabsList, TabsTrigger };
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[84px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm placeholder:text-default-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
data-slot="textarea"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea };
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Accordion as BaseAccordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem as BaseAccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "@/components/ui/accordion";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export interface AccordionProps extends Omit<React.ComponentProps<"div">, "children"> {
|
||||||
|
children: React.ReactNode;
|
||||||
|
variant?: "bordered" | "light" | "splitted";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccordionItemProps {
|
||||||
|
"aria-label"?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
title: React.ReactNode;
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Accordion({ children, className }: AccordionProps) {
|
||||||
|
return (
|
||||||
|
<BaseAccordion className={cn("w-full", className)} type="multiple">
|
||||||
|
{children}
|
||||||
|
</BaseAccordion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccordionItem({
|
||||||
|
"aria-label": ariaLabel,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
}: AccordionItemProps) {
|
||||||
|
const generatedValue = React.useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseAccordionItem className={className} value={value ?? generatedValue}>
|
||||||
|
<AccordionTrigger aria-label={ariaLabel}>{title}</AccordionTrigger>
|
||||||
|
<AccordionContent>{children}</AccordionContent>
|
||||||
|
</BaseAccordionItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Alert as BaseAlert,
|
||||||
|
AlertDescription,
|
||||||
|
AlertTitle,
|
||||||
|
} from "@/components/ui/alert";
|
||||||
|
|
||||||
|
type AlertColor = "default" | "success" | "warning" | "danger" | "primary";
|
||||||
|
type AlertVariant = "solid" | "flat" | "faded" | "bordered";
|
||||||
|
|
||||||
|
interface AlertProps extends Omit<React.ComponentProps<"div">, "color" | "title"> {
|
||||||
|
color?: AlertColor;
|
||||||
|
description?: React.ReactNode;
|
||||||
|
title?: React.ReactNode;
|
||||||
|
variant?: AlertVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapVariant(color: AlertColor) {
|
||||||
|
if (color === "danger") {
|
||||||
|
return "destructive" as const;
|
||||||
|
}
|
||||||
|
if (color === "success") {
|
||||||
|
return "success" as const;
|
||||||
|
}
|
||||||
|
if (color === "warning") {
|
||||||
|
return "warning" as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "default" as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Alert({
|
||||||
|
children,
|
||||||
|
color = "default",
|
||||||
|
description,
|
||||||
|
title,
|
||||||
|
variant,
|
||||||
|
...props
|
||||||
|
}: AlertProps) {
|
||||||
|
return (
|
||||||
|
<BaseAlert
|
||||||
|
className={variant === "flat" ? "bg-opacity-15" : undefined}
|
||||||
|
variant={mapVariant(color)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{title ? <AlertTitle>{title}</AlertTitle> : null}
|
||||||
|
{description ? <AlertDescription>{description}</AlertDescription> : null}
|
||||||
|
{!description && !title ? children : null}
|
||||||
|
</BaseAlert>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Loader2Icon } from "lucide-react";
|
||||||
|
|
||||||
|
import { Button as BaseButton } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type HeroButtonColor = "default" | "primary" | "secondary" | "success" | "warning" | "danger";
|
||||||
|
type HeroButtonVariant = "solid" | "light" | "flat" | "ghost" | "bordered" | "shadow";
|
||||||
|
type HeroButtonSize = "sm" | "md" | "lg";
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "color"> {
|
||||||
|
color?: HeroButtonColor;
|
||||||
|
endContent?: React.ReactNode;
|
||||||
|
isIconOnly?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
|
onPress?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
|
size?: HeroButtonSize;
|
||||||
|
startContent?: React.ReactNode;
|
||||||
|
variant?: HeroButtonVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapVariant(color: HeroButtonColor, variant: HeroButtonVariant): "default" | "destructive" | "secondary" | "outline" | "ghost" | "light" | "flat" {
|
||||||
|
if (variant === "bordered") {
|
||||||
|
return "outline";
|
||||||
|
}
|
||||||
|
if (variant === "ghost") {
|
||||||
|
return "ghost";
|
||||||
|
}
|
||||||
|
if (variant === "light") {
|
||||||
|
return "light";
|
||||||
|
}
|
||||||
|
if (variant === "flat") {
|
||||||
|
return "flat";
|
||||||
|
}
|
||||||
|
if (color === "danger") {
|
||||||
|
return "destructive";
|
||||||
|
}
|
||||||
|
if (color === "secondary") {
|
||||||
|
return "secondary";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapSize(size: HeroButtonSize, isIconOnly: boolean): "default" | "sm" | "lg" | "icon" {
|
||||||
|
if (isIconOnly) {
|
||||||
|
return "icon";
|
||||||
|
}
|
||||||
|
if (size === "sm") {
|
||||||
|
return "sm";
|
||||||
|
}
|
||||||
|
if (size === "lg") {
|
||||||
|
return "lg";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Button({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
color = "default",
|
||||||
|
disabled,
|
||||||
|
endContent,
|
||||||
|
isIconOnly = false,
|
||||||
|
isLoading = false,
|
||||||
|
isDisabled,
|
||||||
|
onClick,
|
||||||
|
onPress,
|
||||||
|
size = "md",
|
||||||
|
startContent,
|
||||||
|
type = "button",
|
||||||
|
variant = "solid",
|
||||||
|
...props
|
||||||
|
}: ButtonProps & {
|
||||||
|
isDisabled?: boolean;
|
||||||
|
}) {
|
||||||
|
const resolvedVariant = mapVariant(color, variant);
|
||||||
|
const resolvedSize = mapSize(size, isIconOnly);
|
||||||
|
const resolvedDisabled = Boolean(disabled || isDisabled || isLoading);
|
||||||
|
|
||||||
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
onClick?.(event);
|
||||||
|
onPress?.(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseButton
|
||||||
|
className={cn(isIconOnly ? "p-0" : "", className)}
|
||||||
|
disabled={resolvedDisabled}
|
||||||
|
size={resolvedSize}
|
||||||
|
type={type}
|
||||||
|
variant={resolvedVariant}
|
||||||
|
onClick={handleClick}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{isLoading ? <Loader2Icon className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||||
|
{startContent}
|
||||||
|
{isIconOnly ? null : children}
|
||||||
|
{isIconOnly ? children : null}
|
||||||
|
{endContent}
|
||||||
|
</BaseButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Card as BaseCard,
|
||||||
|
CardContent,
|
||||||
|
CardHeader as BaseCardHeader,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return <BaseCard className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return <BaseCardHeader className={cn("p-4 md:p-6", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardBody({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return <CardContent className={cn("p-4 md:p-6", className)} {...props} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { Checkbox as BaseCheckbox } from "@/components/ui/checkbox";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export interface CheckboxProps
|
||||||
|
extends Omit<React.ComponentProps<typeof BaseCheckbox>, "checked" | "onCheckedChange"> {
|
||||||
|
classNames?: Record<string, string>;
|
||||||
|
color?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isSelected?: boolean;
|
||||||
|
onValueChange?: (value: boolean) => void;
|
||||||
|
size?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Checkbox({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
isDisabled,
|
||||||
|
isSelected,
|
||||||
|
onValueChange,
|
||||||
|
...props
|
||||||
|
}: CheckboxProps & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const handleCheckedChange: React.ComponentProps<
|
||||||
|
typeof BaseCheckbox
|
||||||
|
>["onCheckedChange"] = (value: boolean | "indeterminate") => {
|
||||||
|
onValueChange?.(value === true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("inline-flex items-center gap-2", isDisabled ? "opacity-50" : "", className)}>
|
||||||
|
<BaseCheckbox
|
||||||
|
checked={Boolean(isSelected)}
|
||||||
|
disabled={isDisabled}
|
||||||
|
onCheckedChange={handleCheckedChange}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{children ? <span className="text-sm">{children}</span> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type ChipColor = "default" | "primary" | "secondary" | "success" | "warning" | "danger";
|
||||||
|
type ChipVariant = "solid" | "flat" | "light" | "bordered";
|
||||||
|
type ChipSize = "sm" | "md" | "lg";
|
||||||
|
|
||||||
|
export interface ChipProps extends React.ComponentProps<"span"> {
|
||||||
|
color?: ChipColor;
|
||||||
|
size?: ChipSize;
|
||||||
|
variant?: ChipVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
function colorVariant(color: ChipColor) {
|
||||||
|
if (color === "danger") {
|
||||||
|
return "destructive" as const;
|
||||||
|
}
|
||||||
|
if (color === "success") {
|
||||||
|
return "success" as const;
|
||||||
|
}
|
||||||
|
if (color === "warning") {
|
||||||
|
return "warning" as const;
|
||||||
|
}
|
||||||
|
if (color === "secondary") {
|
||||||
|
return "secondary" as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "default" as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Chip({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
color = "default",
|
||||||
|
size = "md",
|
||||||
|
variant = "solid",
|
||||||
|
...props
|
||||||
|
}: ChipProps) {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
className={cn(
|
||||||
|
variant === "flat" || variant === "light" ? "bg-opacity-15" : "",
|
||||||
|
variant === "bordered" ? "border-current bg-transparent" : "",
|
||||||
|
size === "sm" ? "px-2 py-0 text-[10px]" : "",
|
||||||
|
size === "lg" ? "px-3 py-1 text-sm" : "",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
variant={colorVariant(color)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { FieldContainer, type FieldMetaProps } from "./shared";
|
||||||
|
|
||||||
|
interface CalendarDateLike {
|
||||||
|
day: number;
|
||||||
|
month: number;
|
||||||
|
year: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DatePickerProps extends FieldMetaProps {
|
||||||
|
className?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isRequired?: boolean;
|
||||||
|
onChange?: (value: CalendarDateLike | null) => void;
|
||||||
|
showMonthAndYearPickers?: boolean;
|
||||||
|
value?: CalendarDateLike | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateValue(value: CalendarDateLike | null | undefined) {
|
||||||
|
if (!value) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const month = String(value.month).padStart(2, "0");
|
||||||
|
const day = String(value.day).padStart(2, "0");
|
||||||
|
|
||||||
|
return `${value.year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DatePicker({
|
||||||
|
className,
|
||||||
|
description,
|
||||||
|
errorMessage,
|
||||||
|
isDisabled,
|
||||||
|
isInvalid,
|
||||||
|
isRequired,
|
||||||
|
label,
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
}: DatePickerProps) {
|
||||||
|
const id = React.useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldContainer
|
||||||
|
description={description}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
id={id}
|
||||||
|
isInvalid={isInvalid}
|
||||||
|
isRequired={isRequired}
|
||||||
|
label={label}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
aria-invalid={isInvalid}
|
||||||
|
className={cn(className)}
|
||||||
|
disabled={isDisabled}
|
||||||
|
id={id}
|
||||||
|
required={isRequired}
|
||||||
|
type="date"
|
||||||
|
value={formatDateValue(value)}
|
||||||
|
onChange={(event) => {
|
||||||
|
if (!onChange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event.target.value) {
|
||||||
|
onChange(null);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [yearText, monthText, dayText] = event.target.value.split("-");
|
||||||
|
const year = Number(yearText);
|
||||||
|
const month = Number(monthText);
|
||||||
|
const day = Number(dayText);
|
||||||
|
|
||||||
|
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
||||||
|
onChange(null);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange({ day, month, year });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FieldContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
|
export function Divider(props: React.ComponentProps<typeof Separator>) {
|
||||||
|
return <Separator {...props} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu as BaseDropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem as BaseDropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger as BaseDropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type Placement = "bottom-start" | "bottom-end" | "top-start" | "top-end";
|
||||||
|
|
||||||
|
interface DropdownContextValue {
|
||||||
|
align: "start" | "end";
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownContext = React.createContext<DropdownContextValue>({ align: "start" });
|
||||||
|
|
||||||
|
export interface DropdownProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
placement?: Placement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Dropdown({ children, placement = "bottom-start" }: DropdownProps) {
|
||||||
|
const align = placement.endsWith("end") ? "end" : "start";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownContext.Provider value={{ align }}>
|
||||||
|
<BaseDropdownMenu>{children}</BaseDropdownMenu>
|
||||||
|
</DropdownContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DropdownTrigger({ children }: { children: React.ReactNode }) {
|
||||||
|
return <BaseDropdownMenuTrigger asChild>{children}</BaseDropdownMenuTrigger>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DropdownMenuProps {
|
||||||
|
"aria-label"?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DropdownMenu({ children }: DropdownMenuProps) {
|
||||||
|
const { align } = React.useContext(DropdownContext);
|
||||||
|
|
||||||
|
return <DropdownMenuContent align={align}>{children}</DropdownMenuContent>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DropdownItemProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
color?: "default" | "danger";
|
||||||
|
onPress?: () => void;
|
||||||
|
startContent?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DropdownItem({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
color = "default",
|
||||||
|
onPress,
|
||||||
|
startContent,
|
||||||
|
}: DropdownItemProps) {
|
||||||
|
return (
|
||||||
|
<BaseDropdownMenuItem
|
||||||
|
className={cn(color === "danger" ? "text-danger focus:text-danger" : "", className)}
|
||||||
|
onSelect={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
onPress?.();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{startContent}
|
||||||
|
{children}
|
||||||
|
</BaseDropdownMenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DropdownMenuLabel, DropdownMenuSeparator };
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { Input as BaseInput } from "@/components/ui/input";
|
||||||
|
import { Textarea as BaseTextarea } from "@/components/ui/textarea";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { FieldContainer, type FieldMetaProps } from "./shared";
|
||||||
|
|
||||||
|
type ClassNameMap = {
|
||||||
|
base?: string;
|
||||||
|
description?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
input?: string;
|
||||||
|
inputWrapper?: string;
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">,
|
||||||
|
FieldMetaProps {
|
||||||
|
classNames?: ClassNameMap;
|
||||||
|
endContent?: React.ReactNode;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
startContent?: React.ReactNode;
|
||||||
|
variant?: "flat" | "bordered" | "faded" | "underlined";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextareaProps
|
||||||
|
extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "size">,
|
||||||
|
FieldMetaProps {
|
||||||
|
classNames?: ClassNameMap;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
maxRows?: number;
|
||||||
|
minRows?: number;
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
variant?: "flat" | "bordered" | "faded" | "underlined";
|
||||||
|
}
|
||||||
|
|
||||||
|
function inputSizeClass(size: "sm" | "md" | "lg" | undefined) {
|
||||||
|
if (size === "sm") {
|
||||||
|
return "h-8 text-xs";
|
||||||
|
}
|
||||||
|
if (size === "lg") {
|
||||||
|
return "h-10 text-base";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "h-9 text-sm";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Input({
|
||||||
|
className,
|
||||||
|
classNames,
|
||||||
|
description,
|
||||||
|
endContent,
|
||||||
|
errorMessage,
|
||||||
|
id,
|
||||||
|
isDisabled,
|
||||||
|
isInvalid,
|
||||||
|
isRequired,
|
||||||
|
label,
|
||||||
|
size,
|
||||||
|
startContent,
|
||||||
|
variant,
|
||||||
|
...props
|
||||||
|
}: InputProps) {
|
||||||
|
const generatedId = React.useId();
|
||||||
|
const resolvedId = id ?? generatedId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldContainer
|
||||||
|
className={classNames?.base}
|
||||||
|
description={description}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
id={resolvedId}
|
||||||
|
isInvalid={isInvalid}
|
||||||
|
isRequired={isRequired}
|
||||||
|
label={label}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative flex items-center rounded-md",
|
||||||
|
variant === "bordered" ? "border border-input" : "",
|
||||||
|
classNames?.inputWrapper,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{startContent ? <div className="pl-3 text-default-500">{startContent}</div> : null}
|
||||||
|
<BaseInput
|
||||||
|
aria-invalid={isInvalid}
|
||||||
|
className={cn(
|
||||||
|
inputSizeClass(size),
|
||||||
|
variant === "bordered" ? "border-0 shadow-none" : "",
|
||||||
|
startContent ? "pl-2" : "",
|
||||||
|
endContent ? "pr-2" : "",
|
||||||
|
classNames?.input,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
disabled={isDisabled}
|
||||||
|
id={resolvedId}
|
||||||
|
required={isRequired}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{endContent ? <div className="pr-3 text-default-500">{endContent}</div> : null}
|
||||||
|
</div>
|
||||||
|
</FieldContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Textarea({
|
||||||
|
className,
|
||||||
|
classNames,
|
||||||
|
description,
|
||||||
|
errorMessage,
|
||||||
|
id,
|
||||||
|
isDisabled,
|
||||||
|
isInvalid,
|
||||||
|
isRequired,
|
||||||
|
label,
|
||||||
|
maxRows,
|
||||||
|
minRows,
|
||||||
|
size,
|
||||||
|
variant,
|
||||||
|
...props
|
||||||
|
}: TextareaProps) {
|
||||||
|
const generatedId = React.useId();
|
||||||
|
const resolvedId = id ?? generatedId;
|
||||||
|
const rows = props.rows ?? minRows ?? 3;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldContainer
|
||||||
|
className={classNames?.base}
|
||||||
|
description={description}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
id={resolvedId}
|
||||||
|
isInvalid={isInvalid}
|
||||||
|
isRequired={isRequired}
|
||||||
|
label={label}
|
||||||
|
>
|
||||||
|
<BaseTextarea
|
||||||
|
aria-invalid={isInvalid}
|
||||||
|
className={cn(
|
||||||
|
variant === "bordered" ? "border border-input" : "",
|
||||||
|
size === "sm" ? "text-xs" : "",
|
||||||
|
size === "lg" ? "text-base" : "",
|
||||||
|
maxRows ? "max-h-[40vh]" : "",
|
||||||
|
classNames?.input,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
disabled={isDisabled}
|
||||||
|
id={resolvedId}
|
||||||
|
required={isRequired}
|
||||||
|
rows={rows}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</FieldContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type LinkColor = "default" | "foreground" | "primary";
|
||||||
|
|
||||||
|
export interface LinkProps extends React.ComponentProps<"a"> {
|
||||||
|
color?: LinkColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapColor(color: LinkColor) {
|
||||||
|
if (color === "primary") {
|
||||||
|
return "text-primary hover:text-primary/80";
|
||||||
|
}
|
||||||
|
if (color === "foreground") {
|
||||||
|
return "text-foreground hover:text-foreground/80";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "text-default-600 hover:text-default-700";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Link({ className, color = "default", ...props }: LinkProps) {
|
||||||
|
return <a className={cn("transition-colors", mapColor(color), className)} {...props} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { Dialog, DialogContent as BaseDialogContent } from "@/components/ui/dialog";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface DisclosureOptions {
|
||||||
|
isOpen?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenChangeHandler = (open?: boolean) => void;
|
||||||
|
|
||||||
|
export function useDisclosure(options: DisclosureOptions = {}) {
|
||||||
|
const [isOpen, setIsOpen] = React.useState(Boolean(options.isOpen));
|
||||||
|
|
||||||
|
const onOpen = React.useCallback(() => {
|
||||||
|
setIsOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onClose = React.useCallback(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onOpenChange = React.useCallback<OpenChangeHandler>((open) => {
|
||||||
|
setIsOpen((prev) => (typeof open === "boolean" ? open : !prev));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onOpen,
|
||||||
|
onOpenChange,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModalContextValue {
|
||||||
|
classNames?: Record<string, string>;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalContext = React.createContext<ModalContextValue | null>(null);
|
||||||
|
|
||||||
|
function useModalContext() {
|
||||||
|
return React.useContext(ModalContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModalSize = "sm" | "md" | "lg" | "xl" | "2xl" | "4xl" | "full";
|
||||||
|
|
||||||
|
function mapSize(size: ModalSize | undefined) {
|
||||||
|
if (size === "sm") {
|
||||||
|
return "max-w-md";
|
||||||
|
}
|
||||||
|
if (size === "lg") {
|
||||||
|
return "max-w-2xl";
|
||||||
|
}
|
||||||
|
if (size === "xl") {
|
||||||
|
return "max-w-3xl";
|
||||||
|
}
|
||||||
|
if (size === "2xl") {
|
||||||
|
return "max-w-5xl";
|
||||||
|
}
|
||||||
|
if (size === "4xl") {
|
||||||
|
return "max-w-6xl";
|
||||||
|
}
|
||||||
|
if (size === "full") {
|
||||||
|
return "w-[95vw] max-w-[95vw] h-[95vh] max-h-[95vh]";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "max-w-lg";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModalProps {
|
||||||
|
backdrop?: "blur" | "opaque" | "transparent";
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
classNames?: Record<string, string>;
|
||||||
|
isDismissable?: boolean;
|
||||||
|
isOpen?: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
placement?: "center" | "top" | "bottom";
|
||||||
|
scrollBehavior?: "inside" | "outside";
|
||||||
|
size?: ModalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Modal({
|
||||||
|
children,
|
||||||
|
classNames,
|
||||||
|
isOpen = false,
|
||||||
|
onClose,
|
||||||
|
onOpenChange,
|
||||||
|
}: ModalProps) {
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
onOpenChange?.(open);
|
||||||
|
if (!open) {
|
||||||
|
onClose?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const contextValue = {
|
||||||
|
classNames,
|
||||||
|
onClose: () => {
|
||||||
|
handleOpenChange(false);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||||
|
<ModalContext.Provider value={contextValue}>{children}</ModalContext.Provider>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModalContentProps extends Omit<React.ComponentProps<"div">, "children"> {
|
||||||
|
children: React.ReactNode | ((onClose: () => void) => React.ReactNode);
|
||||||
|
scrollBehavior?: "inside" | "outside";
|
||||||
|
size?: ModalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModalContent({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
scrollBehavior,
|
||||||
|
size,
|
||||||
|
...props
|
||||||
|
}: ModalContentProps) {
|
||||||
|
const context = useModalContext();
|
||||||
|
const renderedChildren =
|
||||||
|
typeof children === "function" ? children(() => context?.onClose()) : children;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialogContent
|
||||||
|
className={cn(
|
||||||
|
mapSize(size),
|
||||||
|
context?.classNames?.base,
|
||||||
|
scrollBehavior === "outside" ? "max-h-[90vh] overflow-y-auto" : "",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
showCloseButton={false}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{renderedChildren}
|
||||||
|
</BaseDialogContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModalHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return <div className={cn("text-lg font-semibold", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModalBody({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return <div className={cn("space-y-4", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModalFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("mt-4 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type NavbarPosition = "static" | "sticky";
|
||||||
|
type MaxWidth = "sm" | "md" | "lg" | "xl" | "2xl" | "full";
|
||||||
|
|
||||||
|
export interface NavbarProps extends React.ComponentProps<"nav"> {
|
||||||
|
height?: string;
|
||||||
|
maxWidth?: MaxWidth;
|
||||||
|
position?: NavbarPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
function maxWidthClass(maxWidth: MaxWidth | undefined) {
|
||||||
|
if (maxWidth === "sm") {
|
||||||
|
return "max-w-screen-sm";
|
||||||
|
}
|
||||||
|
if (maxWidth === "md") {
|
||||||
|
return "max-w-screen-md";
|
||||||
|
}
|
||||||
|
if (maxWidth === "lg") {
|
||||||
|
return "max-w-screen-lg";
|
||||||
|
}
|
||||||
|
if (maxWidth === "xl") {
|
||||||
|
return "max-w-screen-xl";
|
||||||
|
}
|
||||||
|
if (maxWidth === "2xl") {
|
||||||
|
return "max-w-screen-2xl";
|
||||||
|
}
|
||||||
|
if (maxWidth === "full") {
|
||||||
|
return "max-w-none";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "max-w-screen-xl";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Navbar({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
height,
|
||||||
|
maxWidth = "xl",
|
||||||
|
position = "static",
|
||||||
|
...props
|
||||||
|
}: NavbarProps) {
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
className={cn(
|
||||||
|
"w-full border-b border-default-200 bg-white/90 backdrop-blur dark:bg-default-50/60",
|
||||||
|
position === "sticky" ? "sticky top-0 z-40" : "",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
style={height ? { minHeight: height } : undefined}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn("mx-auto flex h-full w-full items-center justify-between px-4", maxWidthClass(maxWidth))}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavbarContentProps extends React.ComponentProps<"div"> {
|
||||||
|
justify?: "start" | "center" | "end";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NavbarContent({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
justify = "start",
|
||||||
|
...props
|
||||||
|
}: NavbarContentProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 items-center gap-2",
|
||||||
|
justify === "center" ? "justify-center" : "",
|
||||||
|
justify === "end" ? "justify-end" : "",
|
||||||
|
justify === "start" ? "justify-start" : "",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NavbarBrand({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return <div className={cn("flex items-center", className)} {...props} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { Progress as BaseProgress } from "@/components/ui/progress";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type ProgressColor = "default" | "primary" | "secondary" | "success" | "warning" | "danger";
|
||||||
|
|
||||||
|
export interface ProgressProps {
|
||||||
|
"aria-label"?: string;
|
||||||
|
className?: string;
|
||||||
|
color?: ProgressColor;
|
||||||
|
label?: React.ReactNode;
|
||||||
|
showValueLabel?: boolean;
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
value?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function indicatorClassName(color: ProgressColor) {
|
||||||
|
if (color === "danger") {
|
||||||
|
return "bg-danger";
|
||||||
|
}
|
||||||
|
if (color === "success") {
|
||||||
|
return "bg-success";
|
||||||
|
}
|
||||||
|
if (color === "warning") {
|
||||||
|
return "bg-warning";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "bg-primary";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Progress({
|
||||||
|
"aria-label": ariaLabel,
|
||||||
|
className,
|
||||||
|
color = "primary",
|
||||||
|
label,
|
||||||
|
showValueLabel,
|
||||||
|
size = "md",
|
||||||
|
value = 0,
|
||||||
|
}: ProgressProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn("w-full space-y-1", className)}>
|
||||||
|
{label || showValueLabel ? (
|
||||||
|
<div className="flex items-center justify-between text-xs text-default-500">
|
||||||
|
<span>{label}</span>
|
||||||
|
{showValueLabel ? <span>{Math.round(value)}%</span> : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<BaseProgress
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
className={cn(size === "sm" ? "h-1.5" : "", size === "lg" ? "h-3" : "")}
|
||||||
|
indicatorClassName={indicatorClassName(color)}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { RadioGroup as BaseRadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface RadioContextValue {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RadioContext = React.createContext<RadioContextValue>({ name: "radio-group" });
|
||||||
|
|
||||||
|
export interface RadioGroupProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
label?: React.ReactNode;
|
||||||
|
onValueChange?: (value: string) => void;
|
||||||
|
orientation?: "horizontal" | "vertical";
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RadioGroup({
|
||||||
|
children,
|
||||||
|
label,
|
||||||
|
onValueChange,
|
||||||
|
orientation = "vertical",
|
||||||
|
value,
|
||||||
|
}: RadioGroupProps) {
|
||||||
|
const generatedName = React.useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{label ? <p className="text-sm font-medium">{label}</p> : null}
|
||||||
|
<RadioContext.Provider value={{ name: generatedName }}>
|
||||||
|
<BaseRadioGroup
|
||||||
|
className={cn(
|
||||||
|
orientation === "horizontal" ? "flex flex-wrap items-center gap-4" : "grid gap-3",
|
||||||
|
)}
|
||||||
|
value={value}
|
||||||
|
onValueChange={onValueChange}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</BaseRadioGroup>
|
||||||
|
</RadioContext.Provider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RadioProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Radio({ children, value }: RadioProps) {
|
||||||
|
const context = React.useContext(RadioContext);
|
||||||
|
const id = `${context.name}-${value}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem id={id} value={value} />
|
||||||
|
<Label htmlFor={id}>{children}</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { FieldContainer, extractText, type FieldMetaProps } from "./shared";
|
||||||
|
|
||||||
|
type SelectionMode = "single" | "multiple";
|
||||||
|
|
||||||
|
type SelectionValue = Iterable<React.Key> | Set<React.Key> | Array<React.Key>;
|
||||||
|
|
||||||
|
interface OptionItem {
|
||||||
|
disabled?: boolean;
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClassNameMap {
|
||||||
|
base?: string;
|
||||||
|
trigger?: string;
|
||||||
|
[key: string]: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectProps<T = unknown> extends FieldMetaProps {
|
||||||
|
children?: React.ReactNode | ((item: T) => React.ReactNode);
|
||||||
|
className?: string;
|
||||||
|
classNames?: ClassNameMap;
|
||||||
|
disabledKeys?: SelectionValue;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
items?: Iterable<T>;
|
||||||
|
onChange?: (event: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||||
|
onClick?: (event: React.MouseEvent<HTMLSelectElement>) => void;
|
||||||
|
onSelectionChange?: (keys: Set<React.Key>) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
selectedKeys?: SelectionValue;
|
||||||
|
selectionMode?: SelectionMode;
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
variant?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectItemProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
description?: React.ReactNode;
|
||||||
|
textValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectItem(_props: SelectItemProps) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectItem.displayName = "HeroSelectItem";
|
||||||
|
|
||||||
|
function toSet(value?: SelectionValue) {
|
||||||
|
if (!value) {
|
||||||
|
return new Set<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Set(Array.from(value).map((item) => String(item)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenOptionsFromNode(node: React.ReactNode, options: OptionItem[]) {
|
||||||
|
React.Children.forEach(node, (child, index) => {
|
||||||
|
if (child === null || child === undefined || typeof child === "boolean") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Array.isArray(child)) {
|
||||||
|
flattenOptionsFromNode(child, options);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (React.isValidElement(child)) {
|
||||||
|
if (child.type === React.Fragment) {
|
||||||
|
flattenOptionsFromNode(child.props.children, options);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child.type === SelectItem) {
|
||||||
|
const key = child.key ? String(child.key) : String(index);
|
||||||
|
const props = child.props as SelectItemProps;
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
key,
|
||||||
|
label: props.textValue ?? extractText(props.children) ?? key,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOptions<T>(
|
||||||
|
children: React.ReactNode | ((item: T) => React.ReactNode) | undefined,
|
||||||
|
items: Iterable<T> | undefined,
|
||||||
|
) {
|
||||||
|
const options: OptionItem[] = [];
|
||||||
|
|
||||||
|
if (typeof children === "function" && items) {
|
||||||
|
Array.from(items).forEach((item, index) => {
|
||||||
|
const rendered = children(item);
|
||||||
|
|
||||||
|
if (React.isValidElement(rendered) && rendered.type === SelectItem) {
|
||||||
|
const key = rendered.key ? String(rendered.key) : String(index);
|
||||||
|
const props = rendered.props as SelectItemProps;
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
key,
|
||||||
|
label: props.textValue ?? extractText(props.children) ?? key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof children !== "function") {
|
||||||
|
flattenOptionsFromNode(children, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sizeClass(size: SelectProps["size"]) {
|
||||||
|
if (size === "sm") {
|
||||||
|
return "h-8 text-xs";
|
||||||
|
}
|
||||||
|
if (size === "lg") {
|
||||||
|
return "h-10 text-base";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "h-9 text-sm";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Select<T>({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
classNames,
|
||||||
|
description,
|
||||||
|
disabledKeys,
|
||||||
|
errorMessage,
|
||||||
|
isDisabled,
|
||||||
|
isInvalid,
|
||||||
|
isRequired,
|
||||||
|
items,
|
||||||
|
label,
|
||||||
|
onChange,
|
||||||
|
onClick,
|
||||||
|
onSelectionChange,
|
||||||
|
placeholder,
|
||||||
|
selectedKeys,
|
||||||
|
selectionMode = "single",
|
||||||
|
size,
|
||||||
|
}: SelectProps<T>) {
|
||||||
|
const generatedId = React.useId();
|
||||||
|
const options = React.useMemo(() => getOptions(children, items), [children, items]);
|
||||||
|
const selected = React.useMemo(() => toSet(selectedKeys), [selectedKeys]);
|
||||||
|
const disabled = React.useMemo(() => toSet(disabledKeys), [disabledKeys]);
|
||||||
|
|
||||||
|
const selectedArray = Array.from(selected);
|
||||||
|
const singleValue = selectedArray[0] ?? "";
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
onChange?.(event);
|
||||||
|
|
||||||
|
if (!onSelectionChange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectionMode === "multiple") {
|
||||||
|
const values = Array.from(event.target.selectedOptions).map((option) => option.value);
|
||||||
|
|
||||||
|
onSelectionChange(new Set(values));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event.target.value) {
|
||||||
|
onSelectionChange(new Set());
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectionChange(new Set([event.target.value]));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldContainer
|
||||||
|
className={classNames?.base}
|
||||||
|
description={description}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
id={generatedId}
|
||||||
|
isInvalid={isInvalid}
|
||||||
|
isRequired={isRequired}
|
||||||
|
label={label}
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
className={cn(
|
||||||
|
"w-full rounded-md border border-input bg-background px-3 py-2 shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||||
|
sizeClass(size),
|
||||||
|
selectionMode === "multiple" ? "min-h-32" : "",
|
||||||
|
classNames?.trigger,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
disabled={isDisabled}
|
||||||
|
id={generatedId}
|
||||||
|
multiple={selectionMode === "multiple"}
|
||||||
|
required={isRequired}
|
||||||
|
value={selectionMode === "multiple" ? selectedArray : singleValue}
|
||||||
|
onClick={onClick}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{selectionMode === "single" ? (
|
||||||
|
<option value="">{placeholder ?? "请选择"}</option>
|
||||||
|
) : null}
|
||||||
|
{options.map((option) => (
|
||||||
|
<option key={option.key} disabled={disabled.has(option.key)} value={option.key}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</FieldContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export interface FieldMetaProps {
|
||||||
|
label?: React.ReactNode;
|
||||||
|
description?: React.ReactNode;
|
||||||
|
errorMessage?: React.ReactNode;
|
||||||
|
isInvalid?: boolean;
|
||||||
|
isRequired?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FieldContainerProps extends FieldMetaProps {
|
||||||
|
className?: string;
|
||||||
|
id?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FieldContainer({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
description,
|
||||||
|
errorMessage,
|
||||||
|
id,
|
||||||
|
isInvalid,
|
||||||
|
isRequired,
|
||||||
|
label,
|
||||||
|
}: FieldContainerProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn("w-full space-y-1.5", className)}>
|
||||||
|
{label ? (
|
||||||
|
<Label htmlFor={id}>
|
||||||
|
{label}
|
||||||
|
{isRequired ? <span className="ml-1 text-danger">*</span> : null}
|
||||||
|
</Label>
|
||||||
|
) : null}
|
||||||
|
{children}
|
||||||
|
{description ? <p className="text-xs text-default-500">{description}</p> : null}
|
||||||
|
{isInvalid && errorMessage ? (
|
||||||
|
<p className="text-xs text-danger">{errorMessage}</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractText(content: React.ReactNode): string {
|
||||||
|
if (content === null || content === undefined || typeof content === "boolean") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (typeof content === "string" || typeof content === "number") {
|
||||||
|
return String(content);
|
||||||
|
}
|
||||||
|
if (Array.isArray(content)) {
|
||||||
|
return content.map((item) => extractText(item)).join("").trim();
|
||||||
|
}
|
||||||
|
if (React.isValidElement(content)) {
|
||||||
|
return extractText(content.props.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Loader2Icon } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type SpinnerSize = "sm" | "md" | "lg";
|
||||||
|
|
||||||
|
export interface SpinnerProps extends React.ComponentProps<"div"> {
|
||||||
|
label?: string;
|
||||||
|
size?: SpinnerSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
function iconSize(size: SpinnerSize | undefined) {
|
||||||
|
if (size === "sm") {
|
||||||
|
return "h-4 w-4";
|
||||||
|
}
|
||||||
|
if (size === "lg") {
|
||||||
|
return "h-7 w-7";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "h-5 w-5";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Spinner({ className, label, size = "md", ...props }: SpinnerProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn("inline-flex items-center gap-2", className)} {...props}>
|
||||||
|
<Loader2Icon className={cn("animate-spin text-default-500", iconSize(size))} />
|
||||||
|
{label ? <span className="text-sm text-default-500">{label}</span> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { Switch as BaseSwitch } from "@/components/ui/switch";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export interface SwitchProps extends Omit<React.ComponentProps<typeof BaseSwitch>, "checked" | "onCheckedChange"> {
|
||||||
|
classNames?: Record<string, string>;
|
||||||
|
color?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isSelected?: boolean;
|
||||||
|
onValueChange?: (value: boolean) => void;
|
||||||
|
size?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Switch({
|
||||||
|
className,
|
||||||
|
isDisabled,
|
||||||
|
isSelected,
|
||||||
|
onValueChange,
|
||||||
|
...props
|
||||||
|
}: SwitchProps) {
|
||||||
|
return (
|
||||||
|
<BaseSwitch
|
||||||
|
checked={Boolean(isSelected)}
|
||||||
|
className={cn(className)}
|
||||||
|
disabled={isDisabled}
|
||||||
|
onCheckedChange={onValueChange}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface HeroUIProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
navigate?: unknown;
|
||||||
|
useHref?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeroUIProvider({ children }: HeroUIProviderProps) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface TableClassNames {
|
||||||
|
th?: string;
|
||||||
|
wrapper?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableStyleContextValue {
|
||||||
|
thClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TableStyleContext = React.createContext<TableStyleContextValue>({});
|
||||||
|
|
||||||
|
export interface TableProps extends React.ComponentProps<"table"> {
|
||||||
|
classNames?: TableClassNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Table({ children, className, classNames, ...props }: TableProps) {
|
||||||
|
return (
|
||||||
|
<TableStyleContext.Provider value={{ thClassName: classNames?.th }}>
|
||||||
|
<div className={cn("w-full overflow-auto rounded-md", classNames?.wrapper)}>
|
||||||
|
<table className={cn("w-full text-sm", className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</TableStyleContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableHeader({ children, className, ...props }: React.ComponentProps<"thead">) {
|
||||||
|
const childArray = React.Children.toArray(children);
|
||||||
|
const hasRow = childArray.some(
|
||||||
|
(child) => React.isValidElement(child) && child.type === TableRow,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<thead className={cn("border-b bg-default-50/60", className)} {...props}>
|
||||||
|
{hasRow ? children : <TableRow>{children}</TableRow>}
|
||||||
|
</thead>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableBodyProps<T> extends Omit<React.ComponentProps<"tbody">, "children"> {
|
||||||
|
children?: React.ReactNode | ((item: T) => React.ReactNode);
|
||||||
|
emptyContent?: React.ReactNode;
|
||||||
|
isLoading?: boolean;
|
||||||
|
items?: T[];
|
||||||
|
loadingContent?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableBody<T>({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
emptyContent,
|
||||||
|
isLoading,
|
||||||
|
items,
|
||||||
|
loadingContent,
|
||||||
|
...props
|
||||||
|
}: TableBodyProps<T>) {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<tbody className={className} {...props}>
|
||||||
|
<tr>
|
||||||
|
<td className="p-4 text-center text-default-500" colSpan={999}>
|
||||||
|
{loadingContent ?? "加载中..."}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items && typeof children === "function") {
|
||||||
|
if (items.length === 0) {
|
||||||
|
return (
|
||||||
|
<tbody className={className} {...props}>
|
||||||
|
<tr>
|
||||||
|
<td className="p-4 text-center text-default-500" colSpan={999}>
|
||||||
|
{emptyContent ?? "暂无数据"}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tbody className={className} {...props}>
|
||||||
|
{items.map((item) => {
|
||||||
|
const key =
|
||||||
|
typeof item === "object" && item !== null && "id" in item
|
||||||
|
? String((item as { id: React.Key }).id)
|
||||||
|
: JSON.stringify(item);
|
||||||
|
|
||||||
|
return <React.Fragment key={key}>{children(item)}</React.Fragment>;
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const staticChildren =
|
||||||
|
typeof children === "function" ? [] : React.Children.toArray(children);
|
||||||
|
|
||||||
|
if (staticChildren.length === 0) {
|
||||||
|
return (
|
||||||
|
<tbody className={className} {...props}>
|
||||||
|
<tr>
|
||||||
|
<td className="p-4 text-center text-default-500" colSpan={999}>
|
||||||
|
{emptyContent ?? "暂无数据"}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tbody className={className} {...props}>
|
||||||
|
{typeof children === "function" ? null : children}
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableColumn({ className, ...props }: React.ComponentProps<"th">) {
|
||||||
|
const { thClassName } = React.useContext(TableStyleContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
className={cn("px-3 py-2 text-left font-medium text-default-600", thClassName, className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||||
|
return <tr className={cn("border-b last:border-b-0", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||||
|
return <td className={cn("px-3 py-2 align-middle", className)} {...props} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Tabs as BaseTabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
} from "@/components/ui/tabs";
|
||||||
|
|
||||||
|
interface TabDefinition {
|
||||||
|
content: React.ReactNode;
|
||||||
|
key: string;
|
||||||
|
title: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabsProps {
|
||||||
|
"aria-label"?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
disableCursorAnimation?: boolean;
|
||||||
|
onSelectionChange?: (key: React.Key) => void;
|
||||||
|
selectedKey?: React.Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
title: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Tab(_props: TabProps) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Tab.displayName = "HeroTab";
|
||||||
|
|
||||||
|
function parseTabs(children: React.ReactNode) {
|
||||||
|
const tabs: TabDefinition[] = [];
|
||||||
|
|
||||||
|
React.Children.forEach(children, (child, index) => {
|
||||||
|
if (!React.isValidElement(child) || child.type !== Tab) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = child.key ? String(child.key) : `tab-${index}`;
|
||||||
|
const props = child.props as TabProps;
|
||||||
|
|
||||||
|
tabs.push({
|
||||||
|
content: props.children,
|
||||||
|
key,
|
||||||
|
title: props.title,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Tabs({ children, onSelectionChange, selectedKey }: TabsProps) {
|
||||||
|
const tabs = React.useMemo(() => parseTabs(children), [children]);
|
||||||
|
const fallback = tabs[0]?.key ?? "";
|
||||||
|
const value = selectedKey ? String(selectedKey) : fallback;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseTabs
|
||||||
|
value={value}
|
||||||
|
onValueChange={(nextValue) => {
|
||||||
|
onSelectionChange?.(nextValue);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TabsList
|
||||||
|
className="grid w-full"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `repeat(${Math.max(tabs.length, 1)}, minmax(0, 1fr))`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((item) => (
|
||||||
|
<TabsTrigger key={item.key} value={item.key}>
|
||||||
|
{item.title}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
{tabs.map((item) => (
|
||||||
|
<TabsContent key={item.key} value={item.key}>
|
||||||
|
{item.content}
|
||||||
|
</TabsContent>
|
||||||
|
))}
|
||||||
|
</BaseTabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
type ThemeMode = "light" | "dark";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "flvx:theme";
|
||||||
|
|
||||||
|
function resolveInitialTheme(): ThemeMode {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromStorage = window.localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (fromStorage === "dark" || fromStorage === "light") {
|
||||||
|
return fromStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentTheme: ThemeMode = resolveInitialTheme();
|
||||||
|
const listeners = new Set<(theme: ThemeMode) => void>();
|
||||||
|
|
||||||
|
function broadcast(theme: ThemeMode) {
|
||||||
|
currentTheme = theme;
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.localStorage.setItem(STORAGE_KEY, theme);
|
||||||
|
}
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
listener(theme);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const [theme, setThemeState] = React.useState<ThemeMode>(currentTheme);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const listener = (nextTheme: ThemeMode) => {
|
||||||
|
setThemeState(nextTheme);
|
||||||
|
};
|
||||||
|
|
||||||
|
listeners.add(listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
listeners.delete(listener);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setTheme = React.useCallback((nextTheme: string) => {
|
||||||
|
if (nextTheme !== "dark" && nextTheme !== "light") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
broadcast(nextTheme);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
setTheme,
|
||||||
|
theme,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user