10d0769071
BREAKING: version bump to 1.0.0 New design system: - Emerald/teal color palette with semantic tokens (success, warning, destructive) - Light/dark mode toggle with localStorage persistence and anti-FOUC - CSS custom properties via Tailwind darkMode: 'class' - Zero new dependencies — all primitives hand-rolled Foundation: - 70+ inline SVG icons (no external icon library) - ThemeProvider + useTheme hook - cn() utility for conditional classes UI primitives (web/src/components/ui/): - Button (6 variants, 4 sizes) - Input, Label, Card with slots, Badge (6 variants + sizes) - Switch (animated), Checkbox, Separator - Skeleton, Spinner, EmptyState - Toast system with auto-dismiss (success/error/warning/info) - Dialog with focus trap + escape/click-outside - ConfirmDialog replacing native confirm() - DropdownMenu with keyboard support Layout & navigation: - Sidebar with icon+label nav, active indicator, responsive drawer on mobile - Topbar for <lg viewports with hamburger - PageHeader with title/description/actions pattern - UserMenu with avatar + dropdown (change password, logout) - DirtyBanner redesigned with semantic styling Pages redesigned: - Login: centered card with gradient background - Dashboard: 4 KPI tiles, disk usage bars, services grid, recent activity - Users: table with search, empty state, edit modal - Files: breadcrumbs, drag-drop upload zone, SVG file icons, rename/delete - Samba: card list with badges, edit modal with PathField - NFS: card list with client chips, client editor - Storage: tabs (Mover/SnapRAID/Jobs), real-time job log streaming - Log: timeline grouped by date with filters - Settings: watched mounts, import actions, system info - NotFound: friendly 404 page Polish: - Toast notifications replace all native alert()/confirm() - Tailwind animations (dialog-enter, toast-enter/leave, dropdown-enter) - Custom scrollbar styling - Focus-visible rings - Skeleton loading states across all pages
52 lines
2.0 KiB
TypeScript
52 lines
2.0 KiB
TypeScript
import { ButtonHTMLAttributes, forwardRef } from "react";
|
|
import { cn } from "../../lib/cn";
|
|
import { Loader2 } from "../../lib/icons";
|
|
|
|
export type ButtonVariant = "default" | "secondary" | "ghost" | "outline" | "destructive" | "link";
|
|
export type ButtonSize = "sm" | "md" | "lg" | "icon";
|
|
|
|
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
variant?: ButtonVariant;
|
|
size?: ButtonSize;
|
|
loading?: boolean;
|
|
}
|
|
|
|
const variantClasses: Record<ButtonVariant, string> = {
|
|
default: "bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80",
|
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 active:bg-secondary/70",
|
|
ghost: "hover:bg-accent hover:text-accent-foreground active:bg-accent/80",
|
|
outline: "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground active:bg-accent/80",
|
|
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 active:bg-destructive/80",
|
|
link: "text-primary underline-offset-4 hover:underline active:text-primary/80",
|
|
};
|
|
|
|
const sizeClasses: Record<ButtonSize, string> = {
|
|
sm: "h-8 px-3 text-xs",
|
|
md: "h-10 px-4 text-sm",
|
|
lg: "h-12 px-6 text-base",
|
|
icon: "h-10 w-10",
|
|
};
|
|
|
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
({ className, variant = "default", size = "md", loading, disabled, children, ...props }, ref) => {
|
|
return (
|
|
<button
|
|
ref={ref}
|
|
className={cn(
|
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 active:scale-[0.98]",
|
|
variantClasses[variant],
|
|
sizeClasses[size],
|
|
className
|
|
)}
|
|
disabled={disabled || loading}
|
|
{...props}
|
|
>
|
|
{loading && <Loader2 size={14} className="animate-spin" />}
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|
|
);
|
|
|
|
Button.displayName = "Button";
|