From 96b7d7eac20e579052fab15bd9c031334224c324 Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Sun, 19 Apr 2026 04:39:19 +0300 Subject: [PATCH] feat: Add web dashboard UI components with Alert, Input, Label, Select, and loading/error state utilities - Add alert.tsx with Alert, AlertTitle, AlertDescription, AlertAction components using class-variance-authority with default/destructive variants - Add error-alert.tsx with ErrorAlert component featuring AlertCircle icon, retry/dismiss actions, and customizable text - Add input.tsx with Input component supporting file uploads, focus-visible states, aria-invalid styling, and dark mode - Add label.tsx with Label component --- .../web-dashboard/src/components/ui/alert.tsx | 76 +++++++ .../src/components/ui/error-alert.tsx | 55 +++++ .../web-dashboard/src/components/ui/input.tsx | 19 ++ .../web-dashboard/src/components/ui/label.tsx | 24 +++ .../src/components/ui/loading-state.tsx | 98 +++++++++ .../src/components/ui/select.tsx | 192 ++++++++++++++++++ .../src/components/ui/switch.tsx | 33 +++ .../src/components/ui/textarea.tsx | 18 ++ .../src/components/ui/websocket-status.tsx | 46 +++++ 9 files changed, 561 insertions(+) create mode 100644 apps/web-dashboard/src/components/ui/alert.tsx create mode 100644 apps/web-dashboard/src/components/ui/error-alert.tsx create mode 100644 apps/web-dashboard/src/components/ui/input.tsx create mode 100644 apps/web-dashboard/src/components/ui/label.tsx create mode 100644 apps/web-dashboard/src/components/ui/loading-state.tsx create mode 100644 apps/web-dashboard/src/components/ui/select.tsx create mode 100644 apps/web-dashboard/src/components/ui/switch.tsx create mode 100644 apps/web-dashboard/src/components/ui/textarea.tsx create mode 100644 apps/web-dashboard/src/components/ui/websocket-status.tsx diff --git a/apps/web-dashboard/src/components/ui/alert.tsx b/apps/web-dashboard/src/components/ui/alert.tsx new file mode 100644 index 0000000..2d8ecac --- /dev/null +++ b/apps/web-dashboard/src/components/ui/alert.tsx @@ -0,0 +1,76 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "group/alert relative grid w-full gap-0.5 rounded-lg border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground", + className + )} + {...props} + /> + ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription, AlertAction } diff --git a/apps/web-dashboard/src/components/ui/error-alert.tsx b/apps/web-dashboard/src/components/ui/error-alert.tsx new file mode 100644 index 0000000..84b3f63 --- /dev/null +++ b/apps/web-dashboard/src/components/ui/error-alert.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { AlertCircle, RefreshCw, X } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Alert, AlertDescription } from "@/components/ui/alert"; + +interface ErrorAlertProps { + error: string | null; + onRetry?: () => void; + onDismiss?: () => void; + retryText?: string; + className?: string; +} + +export function ErrorAlert({ + error, + onRetry, + onDismiss, + retryText = "Retry", + className = "" +}: ErrorAlertProps) { + if (!error) return null; + + return ( + + + + {error} +
+ {onRetry && ( + + )} + {onDismiss && ( + + )} +
+
+
+ ); +} diff --git a/apps/web-dashboard/src/components/ui/input.tsx b/apps/web-dashboard/src/components/ui/input.tsx new file mode 100644 index 0000000..17beb1e --- /dev/null +++ b/apps/web-dashboard/src/components/ui/input.tsx @@ -0,0 +1,19 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/apps/web-dashboard/src/components/ui/label.tsx b/apps/web-dashboard/src/components/ui/label.tsx new file mode 100644 index 0000000..1ac80f7 --- /dev/null +++ b/apps/web-dashboard/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +"use client" + +import * as React from "react" +import { Label as LabelPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/apps/web-dashboard/src/components/ui/loading-state.tsx b/apps/web-dashboard/src/components/ui/loading-state.tsx new file mode 100644 index 0000000..d159541 --- /dev/null +++ b/apps/web-dashboard/src/components/ui/loading-state.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { Loader2, RefreshCw } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; + +interface LoadingStateProps { + loading: boolean; + error?: string | null; + onRetry?: () => void; + retryText?: string; + loadingText?: string; + children: React.ReactNode; + className?: string; +} + +export function LoadingState({ + loading, + error, + onRetry, + retryText = "Retry", + loadingText = "Loading...", + children, + className = "" +}: LoadingStateProps) { + if (loading) { + return ( + + + +

{loadingText}

+
+
+ ); + } + + if (error) { + return ( + + +
+

{error}

+ {onRetry && ( + + )} +
+
+
+ ); + } + + return <>{children}; +} + +interface InlineLoadingProps { + loading: boolean; + text?: string; + children: React.ReactNode; +} + +export function InlineLoading({ loading, text = "Loading...", children }: InlineLoadingProps) { + if (loading) { + return ( +
+ + {text} +
+ ); + } + + return <>{children}; +} + +interface ActionLoadingProps { + loading: boolean; + action?: string; + children: React.ReactNode; +} + +export function ActionLoading({ loading, action = "processing", children }: ActionLoadingProps) { + if (loading) { + return ( + + ); + } + + return <>{children}; +} diff --git a/apps/web-dashboard/src/components/ui/select.tsx b/apps/web-dashboard/src/components/ui/select.tsx new file mode 100644 index 0000000..d6f8fce --- /dev/null +++ b/apps/web-dashboard/src/components/ui/select.tsx @@ -0,0 +1,192 @@ +"use client" + +import * as React from "react" +import { Select as SelectPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" +import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react" + +function Select({ + ...props +}: React.ComponentProps) { + return +} + +function SelectGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = "item-aligned", + align = "center", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/apps/web-dashboard/src/components/ui/switch.tsx b/apps/web-dashboard/src/components/ui/switch.tsx new file mode 100644 index 0000000..c7bf02a --- /dev/null +++ b/apps/web-dashboard/src/components/ui/switch.tsx @@ -0,0 +1,33 @@ +"use client" + +import * as React from "react" +import { Switch as SwitchPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Switch({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + + + ) +} + +export { Switch } diff --git a/apps/web-dashboard/src/components/ui/textarea.tsx b/apps/web-dashboard/src/components/ui/textarea.tsx new file mode 100644 index 0000000..95ed1c4 --- /dev/null +++ b/apps/web-dashboard/src/components/ui/textarea.tsx @@ -0,0 +1,18 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { + return ( +