feat(admin-web): add web/admin to repo
Initial commit of the Super-Admin web panel (Next.js + TypeScript). CI admin-web-check job was failing because the directory was never tracked in git. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root;
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal;
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/45 backdrop-blur-[2px] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-[calc(100%-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl border border-border/80 bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
));
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
||||
|
||||
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col gap-2 text-center sm:text-start", className)} {...props} />
|
||||
);
|
||||
|
||||
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-base font-medium leading-snug text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground leading-relaxed", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(buttonVariants({ variant: "outline" }), "mt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { AlertCircle, CheckCircle2, Info, TriangleAlert, X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative flex w-full gap-3 rounded-xl border px-4 py-3 text-sm shadow-sm [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-border/80 bg-card text-foreground [&>svg]:text-muted-foreground",
|
||||
info: "border-[#0C447C]/25 bg-[#0C447C]/5 text-[#0C447C] [&>svg]:text-[#0C447C]",
|
||||
success:
|
||||
"border-[#0F6E56]/25 bg-[#E1F5EE] text-[#0F6E56] [&>svg]:text-[#0F6E56]",
|
||||
warning:
|
||||
"border-[#BA7517]/30 bg-amber-50 text-[#BA7517] [&>svg]:text-[#BA7517]",
|
||||
destructive:
|
||||
"border-[#A32D2D]/25 bg-red-50 text-[#A32D2D] [&>svg]:text-[#A32D2D]",
|
||||
},
|
||||
},
|
||||
defaultVariants: { variant: "default" },
|
||||
}
|
||||
);
|
||||
|
||||
const iconByVariant = {
|
||||
default: Info,
|
||||
info: Info,
|
||||
success: CheckCircle2,
|
||||
warning: TriangleAlert,
|
||||
destructive: AlertCircle,
|
||||
} as const;
|
||||
|
||||
export interface AlertProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof alertVariants> {
|
||||
title?: string;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
|
||||
({ className, variant = "default", title, onDismiss, children, ...props }, ref) => {
|
||||
const Icon = iconByVariant[variant ?? "default"];
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
>
|
||||
<Icon className="mt-0.5 h-4 w-4" aria-hidden />
|
||||
<div className="min-w-0 flex-1 space-y-0.5">
|
||||
{title ? <p className="font-medium leading-snug">{title}</p> : null}
|
||||
{children ? (
|
||||
<div className={cn("text-[13px] leading-relaxed opacity-95", title && "opacity-90")}>
|
||||
{children}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{onDismiss ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDismiss}
|
||||
className="absolute end-2 top-2 rounded-md p-1 opacity-60 transition hover:bg-black/5 hover:opacity-100"
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
Alert.displayName = "Alert";
|
||||
|
||||
export { Alert, alertVariants };
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-transparent bg-primary text-primary-foreground",
|
||||
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: { variant: "default" },
|
||||
}
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
export function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:opacity-90",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:opacity-90",
|
||||
outline: "border border-input bg-background hover:bg-accent",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:opacity-90",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: { variant: "default", size: "default" },
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
||||
);
|
||||
|
||||
const CardTitle = ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<h3
|
||||
className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const CardContent = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("p-6 pt-0", className)} {...props} />
|
||||
);
|
||||
|
||||
export { Card, CardHeader, CardTitle, CardContent };
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-card p-1 text-foreground shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-[11px] font-medium leading-none text-muted-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
type LabeledFieldProps = {
|
||||
label: string;
|
||||
htmlFor?: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
hint?: string;
|
||||
};
|
||||
|
||||
export function LabeledField({ label, htmlFor, children, className, hint }: LabeledFieldProps) {
|
||||
return (
|
||||
<div className={cn("space-y-2", className)}>
|
||||
<Label htmlFor={htmlFor}>{label}</Label>
|
||||
{children}
|
||||
{hint ? <p className="text-[10px] text-muted-foreground">{hint}</p> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useLocale } from "next-intl";
|
||||
import { Toaster } from "sonner";
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
Info,
|
||||
Loader2,
|
||||
TriangleAlert,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function iconWrap(className: string, icon: ReactNode) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-9 w-9 shrink-0 items-center justify-center rounded-full",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function MeeziToaster() {
|
||||
const locale = useLocale();
|
||||
const isRtl = locale !== "en";
|
||||
const isEn = locale === "en";
|
||||
|
||||
const fontClass = isEn
|
||||
? "font-[family-name:var(--font-inter)]"
|
||||
: "font-[family-name:var(--font-vazirmatn)]";
|
||||
|
||||
const toastBase = cn(
|
||||
"group relative flex w-[min(calc(100vw-2rem),400px)] items-start gap-3 overflow-hidden",
|
||||
"rounded-xl border border-border/60 bg-card/95 py-3.5 ps-3.5 pe-10",
|
||||
"shadow-[0_10px_40px_-8px_rgba(15,23,42,0.16)] backdrop-blur-md",
|
||||
"transition-[transform,opacity] duration-200",
|
||||
fontClass
|
||||
);
|
||||
|
||||
const titleClass = "text-[13px] font-semibold leading-snug tracking-tight text-foreground";
|
||||
const descriptionClass = "text-xs leading-relaxed text-muted-foreground";
|
||||
|
||||
return (
|
||||
<Toaster
|
||||
dir={isRtl ? "rtl" : "ltr"}
|
||||
position={isRtl ? "top-left" : "top-right"}
|
||||
closeButton
|
||||
richColors={false}
|
||||
expand
|
||||
gap={12}
|
||||
offset={20}
|
||||
visibleToasts={4}
|
||||
className={fontClass}
|
||||
toastOptions={{
|
||||
unstyled: true,
|
||||
style: {
|
||||
fontFamily: isEn
|
||||
? "var(--font-inter), system-ui, sans-serif"
|
||||
: "var(--font-vazirmatn), system-ui, sans-serif",
|
||||
},
|
||||
classNames: {
|
||||
toast: toastBase,
|
||||
title: titleClass,
|
||||
description: descriptionClass,
|
||||
content: "flex flex-1 flex-col gap-0.5 min-w-0",
|
||||
closeButton: cn(
|
||||
"absolute end-2.5 top-2.5 flex h-7 w-7 items-center justify-center rounded-lg",
|
||||
"border-0 bg-muted/50 text-muted-foreground opacity-80",
|
||||
"transition hover:bg-muted hover:opacity-100",
|
||||
fontClass
|
||||
),
|
||||
actionButton: cn(
|
||||
"rounded-lg bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground",
|
||||
fontClass
|
||||
),
|
||||
cancelButton: cn(
|
||||
"rounded-lg border border-border/80 bg-background px-3 py-1.5 text-xs font-medium",
|
||||
fontClass
|
||||
),
|
||||
success: "border-s-[3px] border-s-[#0F6E56]",
|
||||
error: "border-s-[3px] border-s-[#A32D2D]",
|
||||
warning: "border-s-[3px] border-s-[#BA7517]",
|
||||
info: "border-s-[3px] border-s-[#0C447C]",
|
||||
loading: "border-s-[3px] border-s-primary/40",
|
||||
},
|
||||
}}
|
||||
icons={{
|
||||
success: iconWrap(
|
||||
"bg-[#E1F5EE]",
|
||||
<CheckCircle2 className="h-4 w-4 text-[#0F6E56]" strokeWidth={2.25} />
|
||||
),
|
||||
error: iconWrap(
|
||||
"bg-red-50",
|
||||
<AlertCircle className="h-4 w-4 text-[#A32D2D]" strokeWidth={2.25} />
|
||||
),
|
||||
warning: iconWrap(
|
||||
"bg-amber-50",
|
||||
<TriangleAlert className="h-4 w-4 text-[#BA7517]" strokeWidth={2.25} />
|
||||
),
|
||||
info: iconWrap(
|
||||
"bg-[#0C447C]/10",
|
||||
<Info className="h-4 w-4 text-[#0C447C]" strokeWidth={2.25} />
|
||||
),
|
||||
loading: iconWrap(
|
||||
"bg-primary/10",
|
||||
<Loader2 className="h-4 w-4 animate-spin text-primary" strokeWidth={2.25} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user