mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +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