mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-06 18:06:36 +08:00
fix(frontend): stabilize multiselect behavior and UI spacing (#154)
* fix(frontend): left-align announcement logo in homepage banner * fix(frontend): keep multiselect panels floating and preserve summaries * fix(frontend): rebalance config card header spacing * fix(frontend): normalize formatting and fix multiselect modal behavior * fix(frontend): restore config divider spacing on desktop
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Link } from "@/shadcn-bridge/heroui/link";
|
import { Link } from "@/shadcn-bridge/heroui/link";
|
||||||
import {
|
import {
|
||||||
Navbar as HeroUINavbar,
|
Navbar as HeroUINavbar,
|
||||||
NavbarBrand,
|
NavbarBrand,
|
||||||
NavbarContent,
|
NavbarContent,
|
||||||
} from "@/shadcn-bridge/heroui/navbar";
|
} from "@/shadcn-bridge/heroui/navbar";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
import { Logo } from "@/components/icons";
|
import { Logo } from "@/components/icons";
|
||||||
import { siteConfig, getCachedConfig } from "@/config/site";
|
import { siteConfig, getCachedConfig } from "@/config/site";
|
||||||
import { useWebViewMode } from "@/hooks/useWebViewMode";
|
import { useWebViewMode } from "@/hooks/useWebViewMode";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
import { useTheme } from "@/shadcn-bridge/heroui/use-theme";
|
import { useTheme } from "@/shadcn-bridge/heroui/use-theme";
|
||||||
|
|
||||||
interface ThemeProviderProps {
|
interface ThemeProviderProps {
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ const alertVariants = cva(
|
|||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "border-default-200 bg-default-50/70 text-foreground",
|
default: "border-default-200 bg-default-50/70 text-foreground",
|
||||||
destructive: "border-danger-200 bg-danger-50 text-danger-700 dark:text-danger-300",
|
destructive:
|
||||||
success: "border-success-200 bg-success-50 text-success-700 dark:text-success-300",
|
"border-danger-200 bg-danger-50 text-danger-700 dark:text-danger-300",
|
||||||
warning: "border-warning-200 bg-warning-50 text-warning-700 dark:text-warning-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: {
|
defaultVariants: {
|
||||||
@@ -45,9 +48,16 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"h5">) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
|
function AlertDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("text-sm opacity-90", className)} data-slot="alert-description" {...props} />
|
<div
|
||||||
|
className={cn("text-sm opacity-90", className)}
|
||||||
|
data-slot="alert-description"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,15 @@ const buttonVariants = cva(
|
|||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
destructive: "bg-danger text-white hover:bg-danger/90",
|
destructive: "bg-danger text-white hover:bg-danger/90",
|
||||||
outline: "border border-input bg-background hover:bg-default-100 hover:text-foreground dark:hover:bg-default-200/40",
|
outline:
|
||||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
"border border-input bg-background hover:bg-default-100 hover:text-foreground dark:hover:bg-default-200/40",
|
||||||
ghost: "hover:bg-default-100 hover:text-foreground dark:hover:bg-default-200/40",
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-default-100 hover:text-foreground dark:hover:bg-default-200/40",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
light: "bg-transparent hover:bg-default-100 dark:hover:bg-default-200/40",
|
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",
|
flat: "bg-default-100 text-foreground hover:bg-default-200 dark:bg-default-100/40",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
@@ -43,9 +47,9 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
|
ref={ref}
|
||||||
className={cn(buttonVariants({ className, size, variant }))}
|
className={cn(buttonVariants({ className, size, variant }))}
|
||||||
data-slot="button"
|
data-slot="button"
|
||||||
ref={ref}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
function CardTitle({ className, ...props }: React.ComponentProps<"h3">) {
|
function CardTitle({ className, ...props }: React.ComponentProps<"h3">) {
|
||||||
return (
|
return (
|
||||||
<h3
|
<h3
|
||||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
className={cn(
|
||||||
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
data-slot="card-title"
|
data-slot="card-title"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -47,7 +50,11 @@ function CardDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|||||||
|
|
||||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("p-6 pt-0", className)} data-slot="card-content" {...props} />
|
<div
|
||||||
|
className={cn("p-6 pt-0", className)}
|
||||||
|
data-slot="card-content"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,4 +68,11 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
|
export {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import { CheckIcon } from "lucide-react";
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
function Checkbox({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -4,23 +4,34 @@ import { XIcon } from "lucide-react";
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
function Dialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
function DialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
function DialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
function DialogClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
function DialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -54,9 +65,7 @@ function DialogContent({
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<DialogPrimitive.Close
|
<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">
|
||||||
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" />
|
<XIcon className="h-4 w-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
@@ -69,7 +78,10 @@ function DialogContent({
|
|||||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
|
className={cn(
|
||||||
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
data-slot="dialog-header"
|
data-slot="dialog-header"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -79,17 +91,26 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
data-slot="dialog-footer"
|
data-slot="dialog-footer"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
function DialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
className={cn(
|
||||||
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
data-slot="dialog-title"
|
data-slot="dialog-title"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,32 +4,54 @@ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
function DropdownMenu({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuTrigger({
|
function DropdownMenuTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
|
return (
|
||||||
|
<DropdownMenuPrimitive.Trigger
|
||||||
|
data-slot="dropdown-menu-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
function DropdownMenuGroup({
|
||||||
return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuPortal({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
function DropdownMenuPortal({
|
||||||
return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
function DropdownMenuSub({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuRadioGroup({
|
function DropdownMenuRadioGroup({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
|
data-slot="dropdown-menu-radio-group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuSubTrigger({
|
function DropdownMenuSubTrigger({
|
||||||
@@ -171,7 +193,11 @@ function DropdownMenuLabel({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label
|
||||||
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-semibold",
|
||||||
|
inset && "pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
data-slot="dropdown-menu-label"
|
data-slot="dropdown-menu-label"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -191,10 +217,16 @@ function DropdownMenuSeparator({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
|
function DropdownMenuShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn("ml-auto text-xs tracking-widest text-default-500", className)}
|
className={cn(
|
||||||
|
"ml-auto text-xs tracking-widest text-default-500",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
data-slot="dropdown-menu-shortcut"
|
data-slot="dropdown-menu-shortcut"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import * as LabelPrimitive from "@radix-ui/react-label";
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
function Label({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<LabelPrimitive.Root
|
<LabelPrimitive.Root
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -13,13 +13,19 @@ function Progress({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ProgressPrimitive.Root
|
<ProgressPrimitive.Root
|
||||||
className={cn("relative h-2 w-full overflow-hidden rounded-full bg-default-200", className)}
|
className={cn(
|
||||||
|
"relative h-2 w-full overflow-hidden rounded-full bg-default-200",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
data-slot="progress"
|
data-slot="progress"
|
||||||
value={value}
|
value={value}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ProgressPrimitive.Indicator
|
<ProgressPrimitive.Indicator
|
||||||
className={cn("h-full w-full flex-1 bg-primary transition-all", indicatorClassName)}
|
className={cn(
|
||||||
|
"h-full w-full flex-1 bg-primary transition-all",
|
||||||
|
indicatorClassName,
|
||||||
|
)}
|
||||||
data-slot="progress-indicator"
|
data-slot="progress-indicator"
|
||||||
style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }}
|
style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import { CircleIcon } from "lucide-react";
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function RadioGroup({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
function RadioGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<RadioGroupPrimitive.Root
|
<RadioGroupPrimitive.Root
|
||||||
className={cn("grid gap-2", className)}
|
className={cn("grid gap-2", className)}
|
||||||
@@ -27,7 +30,10 @@ function RadioGroupItem({
|
|||||||
data-slot="radio-group-item"
|
data-slot="radio-group-item"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center" data-slot="radio-group-indicator">
|
<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" />
|
<CircleIcon className="h-2.5 w-2.5 fill-current text-current" />
|
||||||
</RadioGroupPrimitive.Indicator>
|
</RadioGroupPrimitive.Indicator>
|
||||||
</RadioGroupPrimitive.Item>
|
</RadioGroupPrimitive.Item>
|
||||||
|
|||||||
@@ -4,15 +4,21 @@ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
function Select({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
function SelectGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
function SelectValue({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +50,10 @@ function SelectScrollUpButton({
|
|||||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.ScrollUpButton
|
<SelectPrimitive.ScrollUpButton
|
||||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
data-slot="select-scroll-up-button"
|
data-slot="select-scroll-up-button"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -59,7 +68,10 @@ function SelectScrollDownButton({
|
|||||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.ScrollDownButton
|
<SelectPrimitive.ScrollDownButton
|
||||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
data-slot="select-scroll-down-button"
|
data-slot="select-scroll-down-button"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -103,7 +115,10 @@ function SelectContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
function SelectLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Label
|
<SelectPrimitive.Label
|
||||||
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import * as SwitchPrimitive from "@radix-ui/react-switch";
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
function Switch({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<SwitchPrimitive.Root
|
<SwitchPrimitive.Root
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import { cn } from "@/lib/utils";
|
|||||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full overflow-auto" data-slot="table-wrapper">
|
<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} />
|
<table
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
data-slot="table"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -33,7 +37,10 @@ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
|||||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||||
return (
|
return (
|
||||||
<tfoot
|
<tfoot
|
||||||
className={cn("border-t bg-default-50/70 font-medium [&>tr]:last:border-b-0", className)}
|
className={cn(
|
||||||
|
"border-t bg-default-50/70 font-medium [&>tr]:last:border-b-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
data-slot="table-footer"
|
data-slot="table-footer"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -43,7 +50,10 @@ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
|||||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
className={cn("border-b transition-colors hover:bg-default-50/50", className)}
|
className={cn(
|
||||||
|
"border-b transition-colors hover:bg-default-50/50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
data-slot="table-row"
|
data-slot="table-row"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -66,16 +76,26 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|||||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
className={cn("p-2 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
className={cn(
|
||||||
|
"p-2 align-middle [&:has([role=checkbox])]:pr-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
data-slot="table-cell"
|
data-slot="table-cell"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TableCaption({ className, ...props }: React.ComponentProps<"caption">) {
|
function TableCaption({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"caption">) {
|
||||||
return (
|
return (
|
||||||
<caption className={cn("mt-4 text-sm text-default-500", className)} data-slot="table-caption" {...props} />
|
<caption
|
||||||
|
className={cn("mt-4 text-sm text-default-500", className)}
|
||||||
|
data-slot="table-caption"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,23 @@ import * as TabsPrimitive from "@radix-ui/react-tabs";
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
function Tabs({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<TabsPrimitive.Root className={cn("flex flex-col gap-2", className)} data-slot="tabs" {...props} />
|
<TabsPrimitive.Root
|
||||||
|
className={cn("flex flex-col gap-2", className)}
|
||||||
|
data-slot="tabs"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {
|
function TabsList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||||
return (
|
return (
|
||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -22,7 +32,10 @@ function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
function TabsTrigger({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||||
return (
|
return (
|
||||||
<TabsPrimitive.Trigger
|
<TabsPrimitive.Trigger
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -35,7 +48,10 @@ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPr
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
function TabsContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||||
return (
|
return (
|
||||||
<TabsPrimitive.Content
|
<TabsPrimitive.Content
|
||||||
className={cn("outline-none", className)}
|
className={cn("outline-none", className)}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useNavigate, useLocation } from "react-router-dom";
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
|
||||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||||
import {
|
import {
|
||||||
Dropdown,
|
Dropdown,
|
||||||
@@ -16,8 +18,6 @@ import {
|
|||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from "@/shadcn-bridge/heroui/modal";
|
} from "@/shadcn-bridge/heroui/modal";
|
||||||
import { Input } from "@/shadcn-bridge/heroui/input";
|
import { Input } from "@/shadcn-bridge/heroui/input";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
|
|
||||||
import { Logo } from "@/components/icons";
|
import { Logo } from "@/components/icons";
|
||||||
import { updatePassword } from "@/api";
|
import { updatePassword } from "@/api";
|
||||||
import { safeLogout } from "@/utils/logout";
|
import { safeLogout } from "@/utils/logout";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
|
||||||
|
|
||||||
|
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||||
import { Logo } from "@/components/icons";
|
import { Logo } from "@/components/icons";
|
||||||
import { siteConfig } from "@/config/site";
|
import { siteConfig } from "@/config/site";
|
||||||
import { useScrollTopOnPathChange } from "@/hooks/useScrollTopOnPathChange";
|
import { useScrollTopOnPathChange } from "@/hooks/useScrollTopOnPathChange";
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
|
||||||
import { Input } from "@/shadcn-bridge/heroui/input";
|
|
||||||
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||||
|
import { Input } from "@/shadcn-bridge/heroui/input";
|
||||||
|
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||||
import { title } from "@/components/primitives";
|
import { title } from "@/components/primitives";
|
||||||
import { updatePassword } from "@/api";
|
import { updatePassword } from "@/api";
|
||||||
import DefaultLayout from "@/layouts/default";
|
import DefaultLayout from "@/layouts/default";
|
||||||
|
|||||||
@@ -585,7 +585,7 @@ export default function ConfigPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="shadow-md">
|
<Card className="shadow-md">
|
||||||
<CardHeader className="pb-4">
|
<CardHeader className="pb-6">
|
||||||
<div className="flex justify-between items-center w-full">
|
<div className="flex justify-between items-center w-full">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-semibold">基本设置</h2>
|
<h2 className="text-xl font-semibold">基本设置</h2>
|
||||||
@@ -609,7 +609,7 @@ export default function ConfigPage() {
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<CardBody className="space-y-6 pt-6">
|
<CardBody className="space-y-6 pt-8 md:pt-8">
|
||||||
{CONFIG_ITEMS.map((item, index) => {
|
{CONFIG_ITEMS.map((item, index) => {
|
||||||
// 检查配置项是否应该显示
|
// 检查配置项是否应该显示
|
||||||
if (!shouldShowItem(item)) {
|
if (!shouldShowItem(item)) {
|
||||||
@@ -660,7 +660,7 @@ export default function ConfigPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Card className="mt-6 shadow-md">
|
<Card className="mt-6 shadow-md">
|
||||||
<CardHeader className="pb-4">
|
<CardHeader className="pb-6">
|
||||||
<div className="flex justify-between items-center w-full">
|
<div className="flex justify-between items-center w-full">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-semibold">公告管理</h2>
|
<h2 className="text-xl font-semibold">公告管理</h2>
|
||||||
@@ -673,7 +673,7 @@ export default function ConfigPage() {
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<CardBody className="space-y-4 pt-6">
|
<CardBody className="space-y-4 pt-8 md:pt-8">
|
||||||
{announcementLoading ? (
|
{announcementLoading ? (
|
||||||
<div className="flex justify-center py-8">
|
<div className="flex justify-center py-8">
|
||||||
<Spinner size="lg" />
|
<Spinner size="lg" />
|
||||||
@@ -727,7 +727,7 @@ export default function ConfigPage() {
|
|||||||
|
|
||||||
{/* 备份与恢复 */}
|
{/* 备份与恢复 */}
|
||||||
<Card className="mt-6 shadow-md">
|
<Card className="mt-6 shadow-md">
|
||||||
<CardHeader className="pb-4">
|
<CardHeader className="pb-6">
|
||||||
<div className="flex justify-between items-center w-full">
|
<div className="flex justify-between items-center w-full">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-semibold">数据备份与恢复</h2>
|
<h2 className="text-xl font-semibold">数据备份与恢复</h2>
|
||||||
@@ -740,7 +740,7 @@ export default function ConfigPage() {
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<CardBody className="space-y-6 pt-6">
|
<CardBody className="space-y-6 pt-8 md:pt-8">
|
||||||
{/* 导出部分 */}
|
{/* 导出部分 */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-lg font-medium">导出数据</h3>
|
<h3 className="text-lg font-medium">导出数据</h3>
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
|
||||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
|
||||||
import { Modal, ModalContent, ModalHeader, ModalBody } from "@/shadcn-bridge/heroui/modal";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||||
|
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalBody,
|
||||||
|
} from "@/shadcn-bridge/heroui/modal";
|
||||||
import { PageEmptyState, PageLoadingState } from "@/components/page-state";
|
import { PageEmptyState, PageLoadingState } from "@/components/page-state";
|
||||||
import { AnnouncementBanner } from "@/pages/dashboard/components/announcement-banner";
|
import { AnnouncementBanner } from "@/pages/dashboard/components/announcement-banner";
|
||||||
import { FlowChartCard } from "@/pages/dashboard/components/flow-chart-card";
|
import { FlowChartCard } from "@/pages/dashboard/components/flow-chart-card";
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const AnnouncementBanner = ({
|
|||||||
return (
|
return (
|
||||||
<Card className="mb-4 lg:mb-6 border border-blue-200 dark:border-blue-500/30 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-500/10 dark:to-purple-500/10">
|
<Card className="mb-4 lg:mb-6 border border-blue-200 dark:border-blue-500/30 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-500/10 dark:to-purple-500/10">
|
||||||
<CardBody className="p-4">
|
<CardBody className="p-4">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-center justify-start gap-3">
|
||||||
<div className="p-2 bg-blue-100 dark:bg-blue-500/20 rounded-lg flex-shrink-0">
|
<div className="p-2 bg-blue-100 dark:bg-blue-500/20 rounded-lg flex-shrink-0">
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
|
||||||
import {
|
import {
|
||||||
CartesianGrid,
|
CartesianGrid,
|
||||||
Line,
|
Line,
|
||||||
@@ -9,6 +8,7 @@ import {
|
|||||||
YAxis,
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
|
|
||||||
|
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||||
import { PageEmptyState } from "@/components/page-state";
|
import { PageEmptyState } from "@/components/page-state";
|
||||||
|
|
||||||
interface FlowChartPoint {
|
interface FlowChartPoint {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { ForwardApiItem } from "@/api/types";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
@@ -6,7 +8,6 @@ import {
|
|||||||
getUserPackageInfo,
|
getUserPackageInfo,
|
||||||
type AnnouncementData,
|
type AnnouncementData,
|
||||||
} from "@/api";
|
} from "@/api";
|
||||||
import type { ForwardApiItem } from "@/api/types";
|
|
||||||
import { getAdminFlag } from "@/utils/session";
|
import { getAdminFlag } from "@/utils/session";
|
||||||
|
|
||||||
export interface DashboardUserInfo {
|
export interface DashboardUserInfo {
|
||||||
@@ -180,9 +181,13 @@ export const useDashboardData = (): DashboardDataState => {
|
|||||||
);
|
);
|
||||||
const [userTunnels, setUserTunnels] = useState<DashboardUserTunnel[]>([]);
|
const [userTunnels, setUserTunnels] = useState<DashboardUserTunnel[]>([]);
|
||||||
const [forwardList, setForwardList] = useState<DashboardForward[]>([]);
|
const [forwardList, setForwardList] = useState<DashboardForward[]>([]);
|
||||||
const [statisticsFlows, setStatisticsFlows] = useState<DashboardStatisticsFlow[]>([]);
|
const [statisticsFlows, setStatisticsFlows] = useState<
|
||||||
|
DashboardStatisticsFlow[]
|
||||||
|
>([]);
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
const [announcement, setAnnouncement] = useState<AnnouncementData | null>(null);
|
const [announcement, setAnnouncement] = useState<AnnouncementData | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadAnnouncement = async () => {
|
const loadAnnouncement = async () => {
|
||||||
@@ -214,7 +219,10 @@ export const useDashboardData = (): DashboardDataState => {
|
|||||||
setForwardList(normalizedForwards);
|
setForwardList(normalizedForwards);
|
||||||
setStatisticsFlows(data.statisticsFlows || []);
|
setStatisticsFlows(data.statisticsFlows || []);
|
||||||
|
|
||||||
checkExpirationNotifications(data.userInfo, normalizedTunnelPermissions);
|
checkExpirationNotifications(
|
||||||
|
data.userInfo,
|
||||||
|
normalizedTunnelPermissions,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg || "获取套餐信息失败");
|
toast.error(res.msg || "获取套餐信息失败");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,4 @@
|
|||||||
import { useState, useEffect, useMemo } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
|
||||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
|
||||||
import { Input } from "@/shadcn-bridge/heroui/input";
|
|
||||||
import { Textarea } from "@/shadcn-bridge/heroui/input";
|
|
||||||
import { Select, SelectItem } from "@/shadcn-bridge/heroui/select";
|
|
||||||
import {
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
} from "@/shadcn-bridge/heroui/modal";
|
|
||||||
import { Chip } from "@/shadcn-bridge/heroui/chip";
|
|
||||||
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
|
|
||||||
import { Switch } from "@/shadcn-bridge/heroui/switch";
|
|
||||||
import { Alert } from "@/shadcn-bridge/heroui/alert";
|
|
||||||
import { Accordion, AccordionItem } from "@/shadcn-bridge/heroui/accordion";
|
|
||||||
import { Checkbox } from "@/shadcn-bridge/heroui/checkbox";
|
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
@@ -37,6 +19,24 @@ import {
|
|||||||
import { useSortable } from "@dnd-kit/sortable";
|
import { useSortable } from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
|
||||||
|
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||||
|
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||||
|
import { Input } from "@/shadcn-bridge/heroui/input";
|
||||||
|
import { Textarea } from "@/shadcn-bridge/heroui/input";
|
||||||
|
import { Select, SelectItem } from "@/shadcn-bridge/heroui/select";
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
} from "@/shadcn-bridge/heroui/modal";
|
||||||
|
import { Chip } from "@/shadcn-bridge/heroui/chip";
|
||||||
|
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
|
||||||
|
import { Switch } from "@/shadcn-bridge/heroui/switch";
|
||||||
|
import { Alert } from "@/shadcn-bridge/heroui/alert";
|
||||||
|
import { Accordion, AccordionItem } from "@/shadcn-bridge/heroui/accordion";
|
||||||
|
import { Checkbox } from "@/shadcn-bridge/heroui/checkbox";
|
||||||
import {
|
import {
|
||||||
createForward,
|
createForward,
|
||||||
getForwardList,
|
getForwardList,
|
||||||
@@ -1604,10 +1604,20 @@ export default function ForwardPage() {
|
|||||||
<span className="text-sm text-default-600 shrink-0">
|
<span className="text-sm text-default-600 shrink-0">
|
||||||
已选择 {selectedIds.size} 项
|
已选择 {selectedIds.size} 项
|
||||||
</span>
|
</span>
|
||||||
<Button color="primary" size="sm" variant="flat" onPress={selectAll}>
|
<Button
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
onPress={selectAll}
|
||||||
|
>
|
||||||
全选
|
全选
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="secondary" size="sm" variant="flat" onPress={deselectAll}>
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
onPress={deselectAll}
|
||||||
|
>
|
||||||
清空
|
清空
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||||
import { Input } from "@/shadcn-bridge/heroui/input";
|
import { Input } from "@/shadcn-bridge/heroui/input";
|
||||||
@@ -21,8 +23,6 @@ import {
|
|||||||
} from "@/shadcn-bridge/heroui/table";
|
} from "@/shadcn-bridge/heroui/table";
|
||||||
import { Chip } from "@/shadcn-bridge/heroui/chip";
|
import { Chip } from "@/shadcn-bridge/heroui/chip";
|
||||||
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
|
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
|
||||||
import toast from "react-hot-toast";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
assignGroupPermission,
|
assignGroupPermission,
|
||||||
assignTunnelsToGroup,
|
assignTunnelsToGroup,
|
||||||
@@ -173,6 +173,22 @@ export default function GroupPage() {
|
|||||||
return map;
|
return map;
|
||||||
}, [users]);
|
}, [users]);
|
||||||
|
|
||||||
|
const selectedTunnelSummary = useMemo(() => {
|
||||||
|
const value = Array.from(selectedTunnelKeys)
|
||||||
|
.map((id) => tunnelNameMap.get(Number(id)) || id)
|
||||||
|
.join("、");
|
||||||
|
|
||||||
|
return value || "无";
|
||||||
|
}, [selectedTunnelKeys, tunnelNameMap]);
|
||||||
|
|
||||||
|
const selectedUserSummary = useMemo(() => {
|
||||||
|
const value = Array.from(selectedUserKeys)
|
||||||
|
.map((id) => userNameMap.get(Number(id)) || id)
|
||||||
|
.join("、");
|
||||||
|
|
||||||
|
return value || "无";
|
||||||
|
}, [selectedUserKeys, userNameMap]);
|
||||||
|
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -765,8 +781,10 @@ export default function GroupPage() {
|
|||||||
>
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>分配隧道 - {assignTunnelGroup?.name}</ModalHeader>
|
<ModalHeader>分配隧道 - {assignTunnelGroup?.name}</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody className="min-w-0">
|
||||||
<Select
|
<Select
|
||||||
|
className="min-w-0"
|
||||||
|
classNames={{ trigger: "max-w-full" }}
|
||||||
items={tunnels}
|
items={tunnels}
|
||||||
label="选择隧道"
|
label="选择隧道"
|
||||||
selectedKeys={selectedTunnelKeys}
|
selectedKeys={selectedTunnelKeys}
|
||||||
@@ -779,11 +797,11 @@ export default function GroupPage() {
|
|||||||
>
|
>
|
||||||
{(item) => <SelectItem key={item.id}>{item.name}</SelectItem>}
|
{(item) => <SelectItem key={item.id}>{item.name}</SelectItem>}
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-xs text-default-500 break-all whitespace-normal">
|
<p
|
||||||
当前已选:
|
className="w-full min-w-0 max-w-full text-xs text-default-500 truncate"
|
||||||
{Array.from(selectedTunnelKeys)
|
title={`当前已选:${selectedTunnelSummary}`}
|
||||||
.map((id) => tunnelNameMap.get(Number(id)) || id)
|
>
|
||||||
.join("、") || "无"}
|
当前已选:{selectedTunnelSummary}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-default-500">
|
<p className="text-xs text-default-500">
|
||||||
不选择任何隧道并保存将清空该分组成员。
|
不选择任何隧道并保存将清空该分组成员。
|
||||||
@@ -810,8 +828,10 @@ export default function GroupPage() {
|
|||||||
>
|
>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>分配用户 - {assignUserGroup?.name}</ModalHeader>
|
<ModalHeader>分配用户 - {assignUserGroup?.name}</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody className="min-w-0">
|
||||||
<Select
|
<Select
|
||||||
|
className="min-w-0"
|
||||||
|
classNames={{ trigger: "max-w-full" }}
|
||||||
items={users}
|
items={users}
|
||||||
label="选择用户"
|
label="选择用户"
|
||||||
selectedKeys={selectedUserKeys}
|
selectedKeys={selectedUserKeys}
|
||||||
@@ -824,11 +844,11 @@ export default function GroupPage() {
|
|||||||
>
|
>
|
||||||
{(item) => <SelectItem key={item.id}>{item.user}</SelectItem>}
|
{(item) => <SelectItem key={item.id}>{item.user}</SelectItem>}
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-xs text-default-500 break-all whitespace-normal">
|
<p
|
||||||
当前已选:
|
className="w-full min-w-0 max-w-full text-xs text-default-500 truncate"
|
||||||
{Array.from(selectedUserKeys)
|
title={`当前已选:${selectedUserSummary}`}
|
||||||
.map((id) => userNameMap.get(Number(id)) || id)
|
>
|
||||||
.join("、") || "无"}
|
当前已选:{selectedUserSummary}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-default-500">
|
<p className="text-xs text-default-500">
|
||||||
不选择任何用户并保存将清空该分组成员。
|
不选择任何用户并保存将清空该分组成员。
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
|
||||||
import { Input } from "@/shadcn-bridge/heroui/input";
|
|
||||||
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { Turnstile } from "@marsidev/react-turnstile";
|
import { Turnstile } from "@marsidev/react-turnstile";
|
||||||
|
|
||||||
|
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||||
|
import { Input } from "@/shadcn-bridge/heroui/input";
|
||||||
|
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||||
import { siteConfig } from "@/config/site";
|
import { siteConfig } from "@/config/site";
|
||||||
import { title } from "@/components/primitives";
|
import { title } from "@/components/primitives";
|
||||||
import DefaultLayout from "@/layouts/default";
|
import DefaultLayout from "@/layouts/default";
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||||
import { Input } from "@/shadcn-bridge/heroui/input";
|
import { Input } from "@/shadcn-bridge/heroui/input";
|
||||||
@@ -11,8 +13,6 @@ import {
|
|||||||
ModalFooter,
|
ModalFooter,
|
||||||
} from "@/shadcn-bridge/heroui/modal";
|
} from "@/shadcn-bridge/heroui/modal";
|
||||||
import { Chip } from "@/shadcn-bridge/heroui/chip";
|
import { Chip } from "@/shadcn-bridge/heroui/chip";
|
||||||
import toast from "react-hot-toast";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createSpeedLimit,
|
createSpeedLimit,
|
||||||
getSpeedLimitList,
|
getSpeedLimitList,
|
||||||
|
|||||||
@@ -1,23 +1,4 @@
|
|||||||
import { useState, useEffect, useMemo, useCallback } from "react";
|
import { useState, useEffect, useMemo, useCallback } from "react";
|
||||||
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
|
||||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
|
||||||
import { Input } from "@/shadcn-bridge/heroui/input";
|
|
||||||
import { Textarea } from "@/shadcn-bridge/heroui/input";
|
|
||||||
import {
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
} from "@/shadcn-bridge/heroui/modal";
|
|
||||||
import { Chip } from "@/shadcn-bridge/heroui/chip";
|
|
||||||
import { Switch } from "@/shadcn-bridge/heroui/switch";
|
|
||||||
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
|
|
||||||
import { Alert } from "@/shadcn-bridge/heroui/alert";
|
|
||||||
import { Progress } from "@/shadcn-bridge/heroui/progress";
|
|
||||||
import { Accordion, AccordionItem } from "@/shadcn-bridge/heroui/accordion";
|
|
||||||
import { Select, SelectItem } from "@/shadcn-bridge/heroui/select";
|
|
||||||
import { Checkbox } from "@/shadcn-bridge/heroui/checkbox";
|
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
@@ -37,6 +18,25 @@ import {
|
|||||||
} from "@dnd-kit/sortable";
|
} from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
|
||||||
|
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||||
|
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||||
|
import { Input } from "@/shadcn-bridge/heroui/input";
|
||||||
|
import { Textarea } from "@/shadcn-bridge/heroui/input";
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
} from "@/shadcn-bridge/heroui/modal";
|
||||||
|
import { Chip } from "@/shadcn-bridge/heroui/chip";
|
||||||
|
import { Switch } from "@/shadcn-bridge/heroui/switch";
|
||||||
|
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
|
||||||
|
import { Alert } from "@/shadcn-bridge/heroui/alert";
|
||||||
|
import { Progress } from "@/shadcn-bridge/heroui/progress";
|
||||||
|
import { Accordion, AccordionItem } from "@/shadcn-bridge/heroui/accordion";
|
||||||
|
import { Select, SelectItem } from "@/shadcn-bridge/heroui/select";
|
||||||
|
import { Checkbox } from "@/shadcn-bridge/heroui/checkbox";
|
||||||
import {
|
import {
|
||||||
createNode,
|
createNode,
|
||||||
getNodeList,
|
getNodeList,
|
||||||
@@ -1020,10 +1020,20 @@ export default function NodePage() {
|
|||||||
<span className="text-sm font-medium shrink-0">
|
<span className="text-sm font-medium shrink-0">
|
||||||
已选 {selectedIds.size} 项
|
已选 {selectedIds.size} 项
|
||||||
</span>
|
</span>
|
||||||
<Button color="primary" size="sm" variant="flat" onPress={selectAll}>
|
<Button
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
onPress={selectAll}
|
||||||
|
>
|
||||||
全选
|
全选
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="secondary" size="sm" variant="flat" onPress={deselectAll}>
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
onPress={deselectAll}
|
||||||
|
>
|
||||||
清空
|
清空
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
|
||||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||||
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||||
import { Tabs, Tab } from "@/shadcn-bridge/heroui/tabs";
|
import { Tabs, Tab } from "@/shadcn-bridge/heroui/tabs";
|
||||||
@@ -11,8 +13,6 @@ import {
|
|||||||
ModalFooter,
|
ModalFooter,
|
||||||
} from "@/shadcn-bridge/heroui/modal";
|
} from "@/shadcn-bridge/heroui/modal";
|
||||||
import { Select, SelectItem } from "@/shadcn-bridge/heroui/select";
|
import { Select, SelectItem } from "@/shadcn-bridge/heroui/select";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getNodeList,
|
getNodeList,
|
||||||
createPeerShare,
|
createPeerShare,
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { Card, CardBody } from "@/shadcn-bridge/heroui/card";
|
import { Card, CardBody } from "@/shadcn-bridge/heroui/card";
|
||||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||||
import {
|
import {
|
||||||
@@ -10,9 +13,6 @@ import {
|
|||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from "@/shadcn-bridge/heroui/modal";
|
} from "@/shadcn-bridge/heroui/modal";
|
||||||
import { Input } from "@/shadcn-bridge/heroui/input";
|
import { Input } from "@/shadcn-bridge/heroui/input";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
import { isWebViewFunc } from "@/utils/panel";
|
import { isWebViewFunc } from "@/utils/panel";
|
||||||
import { siteConfig } from "@/config/site";
|
import { siteConfig } from "@/config/site";
|
||||||
import { updatePassword } from "@/api";
|
import { updatePassword } from "@/api";
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Input } from "@/shadcn-bridge/heroui/input";
|
|
||||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
|
||||||
import { Card, CardBody } from "@/shadcn-bridge/heroui/card";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
import { Input } from "@/shadcn-bridge/heroui/input";
|
||||||
|
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||||
|
import { Card, CardBody } from "@/shadcn-bridge/heroui/card";
|
||||||
import { reinitializeBaseURL } from "@/api/network";
|
import { reinitializeBaseURL } from "@/api/network";
|
||||||
import {
|
import {
|
||||||
getPanelAddresses,
|
getPanelAddresses,
|
||||||
|
|||||||
@@ -1,20 +1,4 @@
|
|||||||
import { useState, useEffect, useMemo } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
|
||||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
|
||||||
import { Input, Textarea } from "@/shadcn-bridge/heroui/input";
|
|
||||||
import { Select, SelectItem } from "@/shadcn-bridge/heroui/select";
|
|
||||||
import {
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
} from "@/shadcn-bridge/heroui/modal";
|
|
||||||
import { Chip } from "@/shadcn-bridge/heroui/chip";
|
|
||||||
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
|
|
||||||
import { Divider } from "@/shadcn-bridge/heroui/divider";
|
|
||||||
import { Alert } from "@/shadcn-bridge/heroui/alert";
|
|
||||||
import { Checkbox } from "@/shadcn-bridge/heroui/checkbox";
|
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
@@ -34,6 +18,22 @@ import {
|
|||||||
} from "@dnd-kit/sortable";
|
} from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
|
||||||
|
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||||
|
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||||
|
import { Input, Textarea } from "@/shadcn-bridge/heroui/input";
|
||||||
|
import { Select, SelectItem } from "@/shadcn-bridge/heroui/select";
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
} from "@/shadcn-bridge/heroui/modal";
|
||||||
|
import { Chip } from "@/shadcn-bridge/heroui/chip";
|
||||||
|
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
|
||||||
|
import { Divider } from "@/shadcn-bridge/heroui/divider";
|
||||||
|
import { Alert } from "@/shadcn-bridge/heroui/alert";
|
||||||
|
import { Checkbox } from "@/shadcn-bridge/heroui/checkbox";
|
||||||
import {
|
import {
|
||||||
createTunnel,
|
createTunnel,
|
||||||
getTunnelList,
|
getTunnelList,
|
||||||
@@ -697,10 +697,20 @@ export default function TunnelPage() {
|
|||||||
<span className="text-sm text-default-600 shrink-0">
|
<span className="text-sm text-default-600 shrink-0">
|
||||||
已选择 {selectedIds.size} 项
|
已选择 {selectedIds.size} 项
|
||||||
</span>
|
</span>
|
||||||
<Button color="primary" size="sm" variant="flat" onPress={selectAll}>
|
<Button
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
onPress={selectAll}
|
||||||
|
>
|
||||||
全选
|
全选
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="secondary" size="sm" variant="flat" onPress={deselectAll}>
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
onPress={deselectAll}
|
||||||
|
>
|
||||||
清空
|
清空
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { parseDate } from "@internationalized/date";
|
||||||
|
|
||||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||||
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
|
||||||
import { Input } from "@/shadcn-bridge/heroui/input";
|
import { Input } from "@/shadcn-bridge/heroui/input";
|
||||||
@@ -25,9 +28,6 @@ import { Checkbox } from "@/shadcn-bridge/heroui/checkbox";
|
|||||||
import { DatePicker } from "@/shadcn-bridge/heroui/date-picker";
|
import { DatePicker } from "@/shadcn-bridge/heroui/date-picker";
|
||||||
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
|
import { Spinner } from "@/shadcn-bridge/heroui/spinner";
|
||||||
import { Progress } from "@/shadcn-bridge/heroui/progress";
|
import { Progress } from "@/shadcn-bridge/heroui/progress";
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { parseDate } from "@internationalized/date";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
UserForm,
|
UserForm,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { HeroUIProvider } from "@/shadcn-bridge/heroui/system";
|
|
||||||
import { useHref, useNavigate } from "react-router-dom";
|
import { useHref, useNavigate } from "react-router-dom";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
import { I18nProvider } from "@react-aria/i18n";
|
import { I18nProvider } from "@react-aria/i18n";
|
||||||
|
|
||||||
|
import { HeroUIProvider } from "@/shadcn-bridge/heroui/system";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
|
|
||||||
export interface ProvidersProps {
|
export interface ProvidersProps {
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
type AlertColor = "default" | "success" | "warning" | "danger" | "primary";
|
type AlertColor = "default" | "success" | "warning" | "danger" | "primary";
|
||||||
type AlertVariant = "solid" | "flat" | "faded" | "bordered";
|
type AlertVariant = "solid" | "flat" | "faded" | "bordered";
|
||||||
|
|
||||||
interface AlertProps extends Omit<React.ComponentProps<"div">, "color" | "title"> {
|
interface AlertProps
|
||||||
|
extends Omit<React.ComponentProps<"div">, "color" | "title"> {
|
||||||
color?: AlertColor;
|
color?: AlertColor;
|
||||||
description?: React.ReactNode;
|
description?: React.ReactNode;
|
||||||
title?: React.ReactNode;
|
title?: React.ReactNode;
|
||||||
|
|||||||
@@ -4,8 +4,20 @@ import { Loader2Icon } from "lucide-react";
|
|||||||
import { Button as BaseButton } from "@/components/ui/button";
|
import { Button as BaseButton } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type HeroButtonColor = "default" | "primary" | "secondary" | "success" | "warning" | "danger";
|
type HeroButtonColor =
|
||||||
type HeroButtonVariant = "solid" | "light" | "flat" | "ghost" | "bordered" | "shadow";
|
| "default"
|
||||||
|
| "primary"
|
||||||
|
| "secondary"
|
||||||
|
| "success"
|
||||||
|
| "warning"
|
||||||
|
| "danger";
|
||||||
|
type HeroButtonVariant =
|
||||||
|
| "solid"
|
||||||
|
| "light"
|
||||||
|
| "flat"
|
||||||
|
| "ghost"
|
||||||
|
| "bordered"
|
||||||
|
| "shadow";
|
||||||
type HeroButtonSize = "sm" | "md" | "lg";
|
type HeroButtonSize = "sm" | "md" | "lg";
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
@@ -20,7 +32,17 @@ export interface ButtonProps
|
|||||||
variant?: HeroButtonVariant;
|
variant?: HeroButtonVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapVariant(color: HeroButtonColor, variant: HeroButtonVariant): "default" | "destructive" | "secondary" | "outline" | "ghost" | "light" | "flat" {
|
function mapVariant(
|
||||||
|
color: HeroButtonColor,
|
||||||
|
variant: HeroButtonVariant,
|
||||||
|
):
|
||||||
|
| "default"
|
||||||
|
| "destructive"
|
||||||
|
| "secondary"
|
||||||
|
| "outline"
|
||||||
|
| "ghost"
|
||||||
|
| "light"
|
||||||
|
| "flat" {
|
||||||
if (variant === "bordered") {
|
if (variant === "bordered") {
|
||||||
return "outline";
|
return "outline";
|
||||||
}
|
}
|
||||||
@@ -43,7 +65,10 @@ function mapVariant(color: HeroButtonColor, variant: HeroButtonVariant): "defaul
|
|||||||
return "default";
|
return "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapSize(size: HeroButtonSize, isIconOnly: boolean): "default" | "sm" | "lg" | "icon" {
|
function mapSize(
|
||||||
|
size: HeroButtonSize,
|
||||||
|
isIconOnly: boolean,
|
||||||
|
): "default" | "sm" | "lg" | "icon" {
|
||||||
if (isIconOnly) {
|
if (isIconOnly) {
|
||||||
return "icon";
|
return "icon";
|
||||||
}
|
}
|
||||||
@@ -67,33 +92,46 @@ const solidColorClassMap: Partial<Record<HeroButtonColor, string>> = {
|
|||||||
const borderedColorClassMap: Record<HeroButtonColor, string> = {
|
const borderedColorClassMap: Record<HeroButtonColor, string> = {
|
||||||
default:
|
default:
|
||||||
"border-default-300 text-default-700 hover:bg-default-100 dark:border-default-300 dark:text-default-600 dark:hover:bg-default-200/40",
|
"border-default-300 text-default-700 hover:bg-default-100 dark:border-default-300 dark:text-default-600 dark:hover:bg-default-200/40",
|
||||||
primary: "border-primary text-primary hover:bg-primary-50 dark:border-primary-500/60 dark:text-primary-300 dark:hover:bg-primary-900/20",
|
primary:
|
||||||
|
"border-primary text-primary hover:bg-primary-50 dark:border-primary-500/60 dark:text-primary-300 dark:hover:bg-primary-900/20",
|
||||||
secondary:
|
secondary:
|
||||||
"border-secondary text-secondary hover:bg-secondary-50 dark:border-secondary-500/60 dark:text-secondary-300 dark:hover:bg-secondary-900/20",
|
"border-secondary text-secondary hover:bg-secondary-50 dark:border-secondary-500/60 dark:text-secondary-300 dark:hover:bg-secondary-900/20",
|
||||||
success: "border-success text-success hover:bg-success-50 dark:border-success-500/60 dark:text-success-300 dark:hover:bg-success-900/20",
|
success:
|
||||||
|
"border-success text-success hover:bg-success-50 dark:border-success-500/60 dark:text-success-300 dark:hover:bg-success-900/20",
|
||||||
warning:
|
warning:
|
||||||
"border-warning text-warning-700 hover:bg-warning-50 dark:border-warning-500/60 dark:text-warning-300 dark:hover:bg-warning-900/20",
|
"border-warning text-warning-700 hover:bg-warning-50 dark:border-warning-500/60 dark:text-warning-300 dark:hover:bg-warning-900/20",
|
||||||
danger: "border-danger text-danger hover:bg-danger-50 dark:border-danger-500/60 dark:text-danger-300 dark:hover:bg-danger-900/20",
|
danger:
|
||||||
|
"border-danger text-danger hover:bg-danger-50 dark:border-danger-500/60 dark:text-danger-300 dark:hover:bg-danger-900/20",
|
||||||
};
|
};
|
||||||
|
|
||||||
const lightColorClassMap: Record<HeroButtonColor, string> = {
|
const lightColorClassMap: Record<HeroButtonColor, string> = {
|
||||||
default: "text-default-700 hover:bg-default-100 dark:text-default-600 dark:hover:bg-default-200/40",
|
default:
|
||||||
primary: "text-primary hover:bg-primary-100/70 dark:text-primary-300 dark:hover:bg-primary-900/30",
|
"text-default-700 hover:bg-default-100 dark:text-default-600 dark:hover:bg-default-200/40",
|
||||||
secondary: "text-secondary hover:bg-secondary-100/70 dark:text-secondary-300 dark:hover:bg-secondary-900/30",
|
primary:
|
||||||
success: "text-success hover:bg-success-100/70 dark:text-success-300 dark:hover:bg-success-900/30",
|
"text-primary hover:bg-primary-100/70 dark:text-primary-300 dark:hover:bg-primary-900/30",
|
||||||
warning: "text-warning-700 hover:bg-warning-100/70 dark:text-warning-300 dark:hover:bg-warning-900/30",
|
secondary:
|
||||||
danger: "text-danger hover:bg-danger-100/70 dark:text-danger-300 dark:hover:bg-danger-900/30",
|
"text-secondary hover:bg-secondary-100/70 dark:text-secondary-300 dark:hover:bg-secondary-900/30",
|
||||||
|
success:
|
||||||
|
"text-success hover:bg-success-100/70 dark:text-success-300 dark:hover:bg-success-900/30",
|
||||||
|
warning:
|
||||||
|
"text-warning-700 hover:bg-warning-100/70 dark:text-warning-300 dark:hover:bg-warning-900/30",
|
||||||
|
danger:
|
||||||
|
"text-danger hover:bg-danger-100/70 dark:text-danger-300 dark:hover:bg-danger-900/30",
|
||||||
};
|
};
|
||||||
|
|
||||||
const flatColorClassMap: Record<HeroButtonColor, string> = {
|
const flatColorClassMap: Record<HeroButtonColor, string> = {
|
||||||
default:
|
default:
|
||||||
"bg-default-100 text-default-700 hover:bg-default-200 dark:bg-default-100/45 dark:text-default-700 dark:hover:bg-default-200/60",
|
"bg-default-100 text-default-700 hover:bg-default-200 dark:bg-default-100/45 dark:text-default-700 dark:hover:bg-default-200/60",
|
||||||
primary: "bg-primary-100 text-primary-700 hover:bg-primary-200 dark:bg-primary-900/30 dark:text-primary-300 dark:hover:bg-primary-900/45",
|
primary:
|
||||||
|
"bg-primary-100 text-primary-700 hover:bg-primary-200 dark:bg-primary-900/30 dark:text-primary-300 dark:hover:bg-primary-900/45",
|
||||||
secondary:
|
secondary:
|
||||||
"bg-secondary-100 text-secondary-700 hover:bg-secondary-200 dark:bg-secondary-900/30 dark:text-secondary-300 dark:hover:bg-secondary-900/45",
|
"bg-secondary-100 text-secondary-700 hover:bg-secondary-200 dark:bg-secondary-900/30 dark:text-secondary-300 dark:hover:bg-secondary-900/45",
|
||||||
success: "bg-success-100 text-success-700 hover:bg-success-200 dark:bg-success-900/30 dark:text-success-300 dark:hover:bg-success-900/45",
|
success:
|
||||||
warning: "bg-warning-100 text-warning-700 hover:bg-warning-200 dark:bg-warning-900/30 dark:text-warning-300 dark:hover:bg-warning-900/45",
|
"bg-success-100 text-success-700 hover:bg-success-200 dark:bg-success-900/30 dark:text-success-300 dark:hover:bg-success-900/45",
|
||||||
danger: "bg-danger-100 text-danger-700 hover:bg-danger-200 dark:bg-danger-900/30 dark:text-danger-300 dark:hover:bg-danger-900/45",
|
warning:
|
||||||
|
"bg-warning-100 text-warning-700 hover:bg-warning-200 dark:bg-warning-900/30 dark:text-warning-300 dark:hover:bg-warning-900/45",
|
||||||
|
danger:
|
||||||
|
"bg-danger-100 text-danger-700 hover:bg-danger-200 dark:bg-danger-900/30 dark:text-danger-300 dark:hover:bg-danger-900/45",
|
||||||
};
|
};
|
||||||
|
|
||||||
const shadowColorClassMap: Record<HeroButtonColor, string> = {
|
const shadowColorClassMap: Record<HeroButtonColor, string> = {
|
||||||
@@ -105,7 +143,10 @@ const shadowColorClassMap: Record<HeroButtonColor, string> = {
|
|||||||
danger: "shadow-md shadow-danger-500/35",
|
danger: "shadow-md shadow-danger-500/35",
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapColorClass(color: HeroButtonColor, variant: HeroButtonVariant): string {
|
function mapColorClass(
|
||||||
|
color: HeroButtonColor,
|
||||||
|
variant: HeroButtonVariant,
|
||||||
|
): string {
|
||||||
if (variant === "bordered") {
|
if (variant === "bordered") {
|
||||||
return borderedColorClassMap[color];
|
return borderedColorClassMap[color];
|
||||||
}
|
}
|
||||||
@@ -122,7 +163,10 @@ function mapColorClass(color: HeroButtonColor, variant: HeroButtonVariant): stri
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapShadowClass(color: HeroButtonColor, variant: HeroButtonVariant): string {
|
function mapShadowClass(
|
||||||
|
color: HeroButtonColor,
|
||||||
|
variant: HeroButtonVariant,
|
||||||
|
): string {
|
||||||
if (variant !== "shadow") {
|
if (variant !== "shadow") {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -169,16 +213,23 @@ export const Button = React.forwardRef<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseButton
|
<BaseButton
|
||||||
className={cn(isIconOnly ? "p-0" : "", resolvedColorClass, resolvedShadowClass, className)}
|
|
||||||
disabled={resolvedDisabled}
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
isIconOnly ? "p-0" : "",
|
||||||
|
resolvedColorClass,
|
||||||
|
resolvedShadowClass,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
disabled={resolvedDisabled}
|
||||||
size={resolvedSize}
|
size={resolvedSize}
|
||||||
type={type}
|
type={type}
|
||||||
variant={resolvedVariant}
|
variant={resolvedVariant}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{isLoading ? <Loader2Icon className="mr-2 h-4 w-4 animate-spin" /> : null}
|
{isLoading ? (
|
||||||
|
<Loader2Icon className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
) : null}
|
||||||
{startContent}
|
{startContent}
|
||||||
{isIconOnly ? null : children}
|
{isIconOnly ? null : children}
|
||||||
{isIconOnly ? children : null}
|
{isIconOnly ? children : null}
|
||||||
|
|||||||
@@ -11,10 +11,18 @@ export function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
return <BaseCard className={className} {...props} />;
|
return <BaseCard className={className} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
export function CardHeader({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
return <BaseCardHeader className={cn("p-4 md:p-6", className)} {...props} />;
|
return <BaseCardHeader className={cn("p-4 md:p-6", className)} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CardBody({ className, ...props }: React.ComponentProps<"div">) {
|
export function CardBody({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return <CardContent className={cn("p-4 pt-0 md:p-6 md:pt-0", className)} {...props} />;
|
return (
|
||||||
|
<CardContent
|
||||||
|
className={cn("p-4 pt-0 md:p-6 md:pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import { Checkbox as BaseCheckbox } from "@/components/ui/checkbox";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export interface CheckboxProps
|
export interface CheckboxProps
|
||||||
extends Omit<React.ComponentProps<typeof BaseCheckbox>, "checked" | "onCheckedChange"> {
|
extends Omit<
|
||||||
|
React.ComponentProps<typeof BaseCheckbox>,
|
||||||
|
"checked" | "onCheckedChange"
|
||||||
|
> {
|
||||||
classNames?: Record<string, string>;
|
classNames?: Record<string, string>;
|
||||||
color?: string;
|
color?: string;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
@@ -30,7 +33,13 @@ export function Checkbox({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("inline-flex items-center gap-2", isDisabled ? "opacity-50" : "", className)}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center gap-2",
|
||||||
|
isDisabled ? "opacity-50" : "",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
<BaseCheckbox
|
<BaseCheckbox
|
||||||
checked={Boolean(isSelected)}
|
checked={Boolean(isSelected)}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ import * as React from "react";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type ChipColor = "default" | "primary" | "secondary" | "success" | "warning" | "danger";
|
type ChipColor =
|
||||||
|
| "default"
|
||||||
|
| "primary"
|
||||||
|
| "secondary"
|
||||||
|
| "success"
|
||||||
|
| "warning"
|
||||||
|
| "danger";
|
||||||
type ChipVariant = "solid" | "flat" | "light" | "bordered";
|
type ChipVariant = "solid" | "flat" | "light" | "bordered";
|
||||||
type ChipSize = "sm" | "md" | "lg";
|
type ChipSize = "sm" | "md" | "lg";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { FieldContainer, type FieldMetaProps } from "./shared";
|
||||||
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { FieldContainer, type FieldMetaProps } from "./shared";
|
|
||||||
|
|
||||||
interface CalendarDateLike {
|
interface CalendarDateLike {
|
||||||
day: number;
|
day: number;
|
||||||
month: number;
|
month: number;
|
||||||
@@ -76,7 +76,11 @@ export function DatePicker({
|
|||||||
const month = Number(monthText);
|
const month = Number(monthText);
|
||||||
const day = Number(dayText);
|
const day = Number(dayText);
|
||||||
|
|
||||||
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
if (
|
||||||
|
!Number.isFinite(year) ||
|
||||||
|
!Number.isFinite(month) ||
|
||||||
|
!Number.isFinite(day)
|
||||||
|
) {
|
||||||
onChange(null);
|
onChange(null);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger as BaseDropdownMenuTrigger,
|
DropdownMenuTrigger as BaseDropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type Placement = "bottom-start" | "bottom-end" | "top-start" | "top-end";
|
type Placement = "bottom-start" | "bottom-end" | "top-start" | "top-end";
|
||||||
@@ -17,14 +16,19 @@ interface DropdownContextValue {
|
|||||||
align: "start" | "end";
|
align: "start" | "end";
|
||||||
}
|
}
|
||||||
|
|
||||||
const DropdownContext = React.createContext<DropdownContextValue>({ align: "start" });
|
const DropdownContext = React.createContext<DropdownContextValue>({
|
||||||
|
align: "start",
|
||||||
|
});
|
||||||
|
|
||||||
export interface DropdownProps {
|
export interface DropdownProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
placement?: Placement;
|
placement?: Placement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dropdown({ children, placement = "bottom-start" }: DropdownProps) {
|
export function Dropdown({
|
||||||
|
children,
|
||||||
|
placement = "bottom-start",
|
||||||
|
}: DropdownProps) {
|
||||||
const align = placement.endsWith("end") ? "end" : "start";
|
const align = placement.endsWith("end") ? "end" : "start";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -66,7 +70,10 @@ export function DropdownItem({
|
|||||||
}: DropdownItemProps) {
|
}: DropdownItemProps) {
|
||||||
return (
|
return (
|
||||||
<BaseDropdownMenuItem
|
<BaseDropdownMenuItem
|
||||||
className={cn(color === "danger" ? "text-danger focus:text-danger" : "", className)}
|
className={cn(
|
||||||
|
color === "danger" ? "text-danger focus:text-danger" : "",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
onSelect={(event) => {
|
onSelect={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onPress?.();
|
onPress?.();
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { FieldContainer, type FieldMetaProps } from "./shared";
|
||||||
|
|
||||||
import { Input as BaseInput } from "@/components/ui/input";
|
import { Input as BaseInput } from "@/components/ui/input";
|
||||||
import { Textarea as BaseTextarea } from "@/components/ui/textarea";
|
import { Textarea as BaseTextarea } from "@/components/ui/textarea";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { FieldContainer, type FieldMetaProps } from "./shared";
|
|
||||||
|
|
||||||
type ClassNameMap = {
|
type ClassNameMap = {
|
||||||
base?: string;
|
base?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -84,7 +84,9 @@ export function Input({
|
|||||||
classNames?.inputWrapper,
|
classNames?.inputWrapper,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{startContent ? <div className="pl-3 text-default-500">{startContent}</div> : null}
|
{startContent ? (
|
||||||
|
<div className="pl-3 text-default-500">{startContent}</div>
|
||||||
|
) : null}
|
||||||
<BaseInput
|
<BaseInput
|
||||||
aria-invalid={isInvalid}
|
aria-invalid={isInvalid}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -100,7 +102,9 @@ export function Input({
|
|||||||
required={isRequired}
|
required={isRequired}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{endContent ? <div className="pr-3 text-default-500">{endContent}</div> : null}
|
{endContent ? (
|
||||||
|
<div className="pr-3 text-default-500">{endContent}</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</FieldContainer>
|
</FieldContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,5 +20,10 @@ function mapColor(color: LinkColor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Link({ className, color = "default", ...props }: LinkProps) {
|
export function Link({ className, color = "default", ...props }: LinkProps) {
|
||||||
return <a className={cn("transition-colors", mapColor(color), className)} {...props} />;
|
return (
|
||||||
|
<a
|
||||||
|
className={cn("transition-colors", mapColor(color), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,12 @@ export function Navbar({
|
|||||||
style={height ? { minHeight: height } : undefined}
|
style={height ? { minHeight: height } : undefined}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className={cn("mx-auto flex h-full w-full items-center justify-between px-4", maxWidthClass(maxWidth))}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mx-auto flex h-full w-full items-center justify-between px-4",
|
||||||
|
maxWidthClass(maxWidth),
|
||||||
|
)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -85,6 +90,9 @@ export function NavbarContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NavbarBrand({ className, ...props }: React.ComponentProps<"div">) {
|
export function NavbarBrand({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
return <div className={cn("flex items-center", className)} {...props} />;
|
return <div className={cn("flex items-center", className)} {...props} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ import * as React from "react";
|
|||||||
import { Progress as BaseProgress } from "@/components/ui/progress";
|
import { Progress as BaseProgress } from "@/components/ui/progress";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type ProgressColor = "default" | "primary" | "secondary" | "success" | "warning" | "danger";
|
type ProgressColor =
|
||||||
|
| "default"
|
||||||
|
| "primary"
|
||||||
|
| "secondary"
|
||||||
|
| "success"
|
||||||
|
| "warning"
|
||||||
|
| "danger";
|
||||||
|
|
||||||
export interface ProgressProps {
|
export interface ProgressProps {
|
||||||
"aria-label"?: string;
|
"aria-label"?: string;
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { RadioGroup as BaseRadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import {
|
||||||
|
RadioGroup as BaseRadioGroup,
|
||||||
|
RadioGroupItem,
|
||||||
|
} from "@/components/ui/radio-group";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -8,7 +11,9 @@ interface RadioContextValue {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RadioContext = React.createContext<RadioContextValue>({ name: "radio-group" });
|
const RadioContext = React.createContext<RadioContextValue>({
|
||||||
|
name: "radio-group",
|
||||||
|
});
|
||||||
|
|
||||||
export interface RadioGroupProps {
|
export interface RadioGroupProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -33,7 +38,9 @@ export function RadioGroup({
|
|||||||
<RadioContext.Provider value={{ name: generatedName }}>
|
<RadioContext.Provider value={{ name: generatedName }}>
|
||||||
<BaseRadioGroup
|
<BaseRadioGroup
|
||||||
className={cn(
|
className={cn(
|
||||||
orientation === "horizontal" ? "flex flex-wrap items-center gap-4" : "grid gap-3",
|
orientation === "horizontal"
|
||||||
|
? "flex flex-wrap items-center gap-4"
|
||||||
|
: "grid gap-3",
|
||||||
)}
|
)}
|
||||||
value={value}
|
value={value}
|
||||||
onValueChange={onValueChange}
|
onValueChange={onValueChange}
|
||||||
|
|||||||
@@ -169,19 +169,71 @@ export function Select<T>({
|
|||||||
() => getOptions(children, items),
|
() => getOptions(children, items),
|
||||||
[children, items],
|
[children, items],
|
||||||
);
|
);
|
||||||
|
const containerRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
|
const listboxRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||||
const selected = React.useMemo(() => toSet(selectedKeys), [selectedKeys]);
|
const selected = React.useMemo(() => toSet(selectedKeys), [selectedKeys]);
|
||||||
const disabled = React.useMemo(() => toSet(disabledKeys), [disabledKeys]);
|
const disabled = React.useMemo(() => toSet(disabledKeys), [disabledKeys]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isExpanded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePointerDown = (event: MouseEvent | TouchEvent) => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
const listbox = listboxRef.current;
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = event.target;
|
||||||
|
|
||||||
|
if (!(target instanceof Node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (container.contains(target) || listbox?.contains(target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsExpanded(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
setIsExpanded(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handlePointerDown);
|
||||||
|
document.addEventListener("touchstart", handlePointerDown);
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handlePointerDown);
|
||||||
|
document.removeEventListener("touchstart", handlePointerDown);
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [isExpanded]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isDisabled) {
|
||||||
|
setIsExpanded(false);
|
||||||
|
}
|
||||||
|
}, [isDisabled]);
|
||||||
|
|
||||||
const selectedArray = Array.from(selected);
|
const selectedArray = Array.from(selected);
|
||||||
const singleValue = selectedArray[0] ?? "";
|
const singleValue = selectedArray[0] ?? "";
|
||||||
|
const selectedLabels = options
|
||||||
|
.filter((option) => selected.has(option.key))
|
||||||
|
.map((option) => option.label);
|
||||||
|
const resolvedSelectedValues =
|
||||||
|
selectedLabels.length > 0 ? selectedLabels : selectedArray;
|
||||||
|
const selectedFullText = resolvedSelectedValues.join("、");
|
||||||
const selectedText =
|
const selectedText =
|
||||||
selectedArray.length > 0
|
selectedArray.length > 0 ? selectedFullText : (placeholder ?? "请选择");
|
||||||
? options
|
|
||||||
.filter((option) => selected.has(option.key))
|
|
||||||
.map((option) => option.label)
|
|
||||||
.join("、")
|
|
||||||
: (placeholder ?? "请选择");
|
|
||||||
|
|
||||||
const updateMultipleSelection = (key: string, checked?: boolean) => {
|
const updateMultipleSelection = (key: string, checked?: boolean) => {
|
||||||
if (isDisabled || disabled.has(key)) {
|
if (isDisabled || disabled.has(key)) {
|
||||||
@@ -227,6 +279,70 @@ export function Select<T>({
|
|||||||
onSelectionChange(new Set([event.target.value]));
|
onSelectionChange(new Set([event.target.value]));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderMultipleListbox = () => {
|
||||||
|
if (!isExpanded) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute left-0 top-full z-50 mt-1 max-h-56 w-full space-y-1 overflow-y-auto rounded-md border border-divider bg-background p-2 shadow-md"
|
||||||
|
id={`${generatedId}-listbox`}
|
||||||
|
ref={listboxRef}
|
||||||
|
role="listbox"
|
||||||
|
>
|
||||||
|
{options.length === 0 ? (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1 text-default-500",
|
||||||
|
textSizeClass(size),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
暂无可选项
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
options.map((option) => {
|
||||||
|
const optionDisabled = isDisabled || disabled.has(option.key);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={option.key}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 rounded-md px-2 py-1.5",
|
||||||
|
optionDisabled
|
||||||
|
? "cursor-not-allowed opacity-60"
|
||||||
|
: "hover:bg-default-100",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<BaseCheckbox
|
||||||
|
checked={selected.has(option.key)}
|
||||||
|
disabled={optionDisabled}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
updateMultipleSelection(option.key, value === true)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
"min-w-0 flex-1 truncate text-left text-foreground",
|
||||||
|
textSizeClass(size),
|
||||||
|
optionDisabled
|
||||||
|
? "cursor-not-allowed"
|
||||||
|
: "cursor-pointer",
|
||||||
|
)}
|
||||||
|
disabled={optionDisabled}
|
||||||
|
type="button"
|
||||||
|
onClick={() => updateMultipleSelection(option.key)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldContainer
|
<FieldContainer
|
||||||
className={classNames?.base}
|
className={classNames?.base}
|
||||||
@@ -239,21 +355,23 @@ export function Select<T>({
|
|||||||
>
|
>
|
||||||
{selectionMode === "multiple" ? (
|
{selectionMode === "multiple" ? (
|
||||||
<div
|
<div
|
||||||
|
ref={containerRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full overflow-hidden rounded-md border border-input bg-background shadow-sm",
|
"relative w-full",
|
||||||
isDisabled ? "cursor-not-allowed opacity-60" : "",
|
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
id={generatedId}
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-controls={`${generatedId}-listbox`}
|
aria-controls={`${generatedId}-listbox`}
|
||||||
aria-expanded={isExpanded}
|
aria-expanded={isExpanded}
|
||||||
|
aria-haspopup="listbox"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full min-w-0 items-center gap-2 overflow-hidden px-3 py-2 text-left",
|
"flex w-full min-w-0 items-center gap-2 overflow-hidden rounded-md border border-input bg-background px-3 py-2 text-left shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||||
|
isDisabled ? "cursor-not-allowed opacity-60" : "",
|
||||||
classNames?.trigger,
|
classNames?.trigger,
|
||||||
)}
|
)}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
id={generatedId}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsExpanded((prev) => !prev)}
|
onClick={() => setIsExpanded((prev) => !prev)}
|
||||||
>
|
>
|
||||||
@@ -261,9 +379,11 @@ export function Select<T>({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"block min-w-0 flex-1 truncate",
|
"block min-w-0 flex-1 truncate",
|
||||||
textSizeClass(size),
|
textSizeClass(size),
|
||||||
selectedArray.length > 0 ? "text-foreground" : "text-default-500",
|
selectedArray.length > 0
|
||||||
|
? "text-foreground"
|
||||||
|
: "text-default-500",
|
||||||
)}
|
)}
|
||||||
title={selectedText}
|
title={selectedArray.length > 0 ? selectedFullText : undefined}
|
||||||
>
|
>
|
||||||
{selectedText}
|
{selectedText}
|
||||||
</span>
|
</span>
|
||||||
@@ -274,62 +394,7 @@ export function Select<T>({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
{isExpanded ? (
|
{renderMultipleListbox()}
|
||||||
<div
|
|
||||||
className="max-h-56 space-y-1 overflow-y-auto border-t border-divider p-2"
|
|
||||||
id={`${generatedId}-listbox`}
|
|
||||||
role="listbox"
|
|
||||||
>
|
|
||||||
{options.length === 0 ? (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1 text-default-500",
|
|
||||||
textSizeClass(size),
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
暂无可选项
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
options.map((option) => {
|
|
||||||
const optionDisabled = isDisabled || disabled.has(option.key);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={option.key}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-2 rounded-md px-2 py-1.5",
|
|
||||||
optionDisabled
|
|
||||||
? "cursor-not-allowed opacity-60"
|
|
||||||
: "hover:bg-default-100",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<BaseCheckbox
|
|
||||||
checked={selected.has(option.key)}
|
|
||||||
disabled={optionDisabled}
|
|
||||||
onCheckedChange={(value) =>
|
|
||||||
updateMultipleSelection(option.key, value === true)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className={cn(
|
|
||||||
"min-w-0 flex-1 truncate text-left text-foreground",
|
|
||||||
textSizeClass(size),
|
|
||||||
optionDisabled
|
|
||||||
? "cursor-not-allowed"
|
|
||||||
: "cursor-pointer",
|
|
||||||
)}
|
|
||||||
disabled={optionDisabled}
|
|
||||||
type="button"
|
|
||||||
onClick={() => updateMultipleSelection(option.key)}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<select
|
<select
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export function FieldContainer({
|
|||||||
</Label>
|
</Label>
|
||||||
) : null}
|
) : null}
|
||||||
{children}
|
{children}
|
||||||
{description ? <p className="text-xs text-default-500">{description}</p> : null}
|
{description ? (
|
||||||
|
<p className="text-xs text-default-500">{description}</p>
|
||||||
|
) : null}
|
||||||
{isInvalid && errorMessage ? (
|
{isInvalid && errorMessage ? (
|
||||||
<p className="text-xs text-danger">{errorMessage}</p>
|
<p className="text-xs text-danger">{errorMessage}</p>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -45,14 +47,21 @@ export function FieldContainer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function extractText(content: React.ReactNode): string {
|
export function extractText(content: React.ReactNode): string {
|
||||||
if (content === null || content === undefined || typeof content === "boolean") {
|
if (
|
||||||
|
content === null ||
|
||||||
|
content === undefined ||
|
||||||
|
typeof content === "boolean"
|
||||||
|
) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
if (typeof content === "string" || typeof content === "number") {
|
if (typeof content === "string" || typeof content === "number") {
|
||||||
return String(content);
|
return String(content);
|
||||||
}
|
}
|
||||||
if (Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
return content.map((item) => extractText(item)).join("").trim();
|
return content
|
||||||
|
.map((item) => extractText(item))
|
||||||
|
.join("")
|
||||||
|
.trim();
|
||||||
}
|
}
|
||||||
if (React.isValidElement(content)) {
|
if (React.isValidElement(content)) {
|
||||||
return extractText(content.props.children);
|
return extractText(content.props.children);
|
||||||
|
|||||||
@@ -21,10 +21,17 @@ function iconSize(size: SpinnerSize | undefined) {
|
|||||||
return "h-5 w-5";
|
return "h-5 w-5";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Spinner({ className, label, size = "md", ...props }: SpinnerProps) {
|
export function Spinner({
|
||||||
|
className,
|
||||||
|
label,
|
||||||
|
size = "md",
|
||||||
|
...props
|
||||||
|
}: SpinnerProps) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("inline-flex items-center gap-2", className)} {...props}>
|
<div className={cn("inline-flex items-center gap-2", className)} {...props}>
|
||||||
<Loader2Icon className={cn("animate-spin text-default-500", iconSize(size))} />
|
<Loader2Icon
|
||||||
|
className={cn("animate-spin text-default-500", iconSize(size))}
|
||||||
|
/>
|
||||||
{label ? <span className="text-sm text-default-500">{label}</span> : null}
|
{label ? <span className="text-sm text-default-500">{label}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import * as React from "react";
|
|||||||
import { Switch as BaseSwitch } from "@/components/ui/switch";
|
import { Switch as BaseSwitch } from "@/components/ui/switch";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export interface SwitchProps extends Omit<React.ComponentProps<typeof BaseSwitch>, "checked" | "onCheckedChange"> {
|
export interface SwitchProps
|
||||||
|
extends Omit<
|
||||||
|
React.ComponentProps<typeof BaseSwitch>,
|
||||||
|
"checked" | "onCheckedChange"
|
||||||
|
> {
|
||||||
classNames?: Record<string, string>;
|
classNames?: Record<string, string>;
|
||||||
color?: string;
|
color?: string;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
|
|||||||
@@ -17,10 +17,17 @@ export interface TableProps extends React.ComponentProps<"table"> {
|
|||||||
classNames?: TableClassNames;
|
classNames?: TableClassNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Table({ children, className, classNames, ...props }: TableProps) {
|
export function Table({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
classNames,
|
||||||
|
...props
|
||||||
|
}: TableProps) {
|
||||||
return (
|
return (
|
||||||
<TableStyleContext.Provider value={{ thClassName: classNames?.th }}>
|
<TableStyleContext.Provider value={{ thClassName: classNames?.th }}>
|
||||||
<div className={cn("w-full overflow-auto rounded-md", classNames?.wrapper)}>
|
<div
|
||||||
|
className={cn("w-full overflow-auto rounded-md", classNames?.wrapper)}
|
||||||
|
>
|
||||||
<table className={cn("w-full text-sm", className)} {...props}>
|
<table className={cn("w-full text-sm", className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</table>
|
</table>
|
||||||
@@ -29,7 +36,11 @@ export function Table({ children, className, classNames, ...props }: TableProps)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableHeader({ children, className, ...props }: React.ComponentProps<"thead">) {
|
export function TableHeader({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"thead">) {
|
||||||
const childArray = React.Children.toArray(children);
|
const childArray = React.Children.toArray(children);
|
||||||
const hasRow = childArray.some(
|
const hasRow = childArray.some(
|
||||||
(child) => React.isValidElement(child) && child.type === TableRow,
|
(child) => React.isValidElement(child) && child.type === TableRow,
|
||||||
@@ -42,7 +53,8 @@ export function TableHeader({ children, className, ...props }: React.ComponentPr
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TableBodyProps<T> extends Omit<React.ComponentProps<"tbody">, "children"> {
|
interface TableBodyProps<T>
|
||||||
|
extends Omit<React.ComponentProps<"tbody">, "children"> {
|
||||||
children?: React.ReactNode | ((item: T) => React.ReactNode);
|
children?: React.ReactNode | ((item: T) => React.ReactNode);
|
||||||
emptyContent?: React.ReactNode;
|
emptyContent?: React.ReactNode;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
@@ -120,19 +132,28 @@ export function TableBody<T>({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableColumn({ className, ...props }: React.ComponentProps<"th">) {
|
export function TableColumn({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"th">) {
|
||||||
const { thClassName } = React.useContext(TableStyleContext);
|
const { thClassName } = React.useContext(TableStyleContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
className={cn("px-3 py-2 text-left font-medium text-default-600", thClassName, className)}
|
className={cn(
|
||||||
|
"px-3 py-2 text-left font-medium text-default-600",
|
||||||
|
thClassName,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
export function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||||
return <tr className={cn("border-b last:border-b-0", className)} {...props} />;
|
return (
|
||||||
|
<tr className={cn("border-b last:border-b-0", className)} {...props} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
export function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||||
|
|||||||
@@ -10,11 +10,14 @@ function resolveInitialTheme(): ThemeMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fromStorage = window.localStorage.getItem(STORAGE_KEY);
|
const fromStorage = window.localStorage.getItem(STORAGE_KEY);
|
||||||
|
|
||||||
if (fromStorage === "dark" || fromStorage === "light") {
|
if (fromStorage === "dark" || fromStorage === "light") {
|
||||||
return fromStorage;
|
return fromStorage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentTheme: ThemeMode = resolveInitialTheme();
|
let currentTheme: ThemeMode = resolveInitialTheme();
|
||||||
|
|||||||
Reference in New Issue
Block a user