Files
flvx/vite-frontend/src/shadcn-bridge/heroui/button.tsx
T
sagit 938e9ff4fd feat(ui): redesign with transparent liquid glass (#414)
* docs: add design spec for iOS 26 Liquid Glass UI redesign

* docs: add implementation plan for iOS 26 Liquid Glass UI redesign

* feat(ui): add liquid glass CSS variables and update root layouts

* feat(ui): update Card and Modal components to iOS 26 Liquid Glass style

* feat(ui): redesign dashboard page to Liquid Glass style

* feat(ui): redesign node management page cards and charts

* feat(ui): redesign monitor page to Liquid Glass style

* feat(ui): redesign group and panel-sharing cards to Liquid Glass

* feat(ui): redesign settings and user components to Liquid Glass

* feat(ui): redesign profile and auth pages to Liquid Glass

* fix(ui): revert to LineChart to fix compilation issues

* refactor(ui): extract dashboard user header and cleanup admin layout

* feat(ui): refine monitor page hero metrics and fix build

* fix(ui): real data for monitor hero metrics and card padding adjustments

* fix(ui): apply global card padding and fix index layouts

* feat(ui): implement liquid-glass-react for all Card components

* fix(ui): resolve liquid-glass-react rendering issues causing invisible text and broken layout

* fix(ui): native CSS liquid-glass effect replacing NPM package

* fix(ui): cleanup duplicate closing tags and complete liquid glass tables

* fix(ui): apply perfect rounded corners to list views and table headers in non-compact mode

* fix(ui): apply liquid glass styling to grouped rules view in forward page

* fix(ui): re-implement liquid-glass natively to prevent text distortion and remove package dependency

* fix(ui): remove mouse distortion and preserve proper liquid glass texture

* fix(ui): remove svg filter causing text distortion and replace with pure css liquid glass

* fix(ui): apply properly scoped border radius to table headers

* fix(ui): remove hardcoded bg-default-50/60 from TableHeader

* fix(ui): apply global liquid glass styling and transparent overlays to all modals and dialogs

* fix(ui): remove absolute/relative class conflicts hiding Dialog modales and implement pure transparent layout

* feat(ui): apply global liquid glass styling to cards, buttons, and diagnosis modals
2026-04-05 13:22:12 +08:00

251 lines
7.6 KiB
TypeScript

import * as React from "react";
import { Loader2Icon } from "lucide-react";
import { Button as BaseButton } from "@/components/ui/button";
import { cn } from "@/lib/utils";
type HeroButtonColor =
| "default"
| "primary"
| "secondary"
| "success"
| "warning"
| "danger";
type HeroButtonVariant =
| "solid"
| "light"
| "flat"
| "ghost"
| "bordered"
| "shadow";
type HeroButtonSize = "sm" | "md" | "lg";
export interface ButtonProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "color"> {
color?: HeroButtonColor;
endContent?: React.ReactNode;
isIconOnly?: boolean;
isLoading?: boolean;
onPress?: (event: React.MouseEvent<HTMLButtonElement>) => void;
size?: HeroButtonSize;
startContent?: React.ReactNode;
variant?: HeroButtonVariant;
}
function mapVariant(
color: HeroButtonColor,
variant: HeroButtonVariant,
):
| "default"
| "destructive"
| "secondary"
| "outline"
| "ghost"
| "light"
| "flat" {
if (variant === "bordered") {
return "outline";
}
if (variant === "ghost") {
return "ghost";
}
if (variant === "light") {
return "light";
}
if (variant === "flat") {
return "flat";
}
if (color === "danger") {
return "destructive";
}
if (color === "secondary") {
return "secondary";
}
return "default";
}
function mapSize(
size: HeroButtonSize,
isIconOnly: boolean,
): "default" | "sm" | "lg" | "icon" {
if (isIconOnly) {
return "icon";
}
if (size === "sm") {
return "sm";
}
if (size === "lg") {
return "lg";
}
return "default";
}
const solidColorClassMap: Partial<Record<HeroButtonColor, string>> = {
default:
"bg-default/10 dark:bg-default/20 backdrop-blur-md text-default-800 dark:text-default-300 hover:bg-default/20 dark:hover:bg-default/30",
success:
"bg-success/10 dark:bg-success/20 backdrop-blur-md text-success-700 dark:text-success-400 hover:bg-success/20 dark:hover:bg-success/30",
warning:
"bg-warning/10 dark:bg-warning/20 backdrop-blur-md text-warning-700 dark:text-warning-400 hover:bg-warning/20 dark:hover:bg-warning/30",
primary:
"bg-primary/10 dark:bg-primary/20 backdrop-blur-md text-primary-700 dark:text-primary-400 hover:bg-primary/20 dark:hover:bg-primary/30",
secondary:
"bg-secondary/10 dark:bg-secondary/20 backdrop-blur-md text-secondary-700 dark:text-secondary-400 hover:bg-secondary/20 dark:hover:bg-secondary/30",
danger:
"bg-danger/10 dark:bg-danger/20 backdrop-blur-md text-danger-700 dark:text-danger-400 hover:bg-danger/20 dark:hover:bg-danger/30",
};
const borderedColorClassMap: Record<HeroButtonColor, string> = {
default:
"bg-default/10 dark:bg-default/20 backdrop-blur-md text-default-700 dark:text-default-300 hover:bg-default/20 dark:hover:bg-default/30",
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:
"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",
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",
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> = {
default:
"text-default-700 hover:bg-default-100 dark:text-default-600 dark:hover:bg-default-200/40",
primary:
"text-primary hover:bg-primary-100/70 dark:text-primary-300 dark:hover:bg-primary-900/30",
secondary:
"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> = {
default:
"bg-default/10 dark:bg-default/20 backdrop-blur-md text-default-700 dark:text-default-300 hover:bg-default/20 dark:hover:bg-default/30",
primary:
"bg-primary/10 dark:bg-primary/20 backdrop-blur-md text-primary-700 dark:text-primary-400 hover:bg-primary/20 dark:hover:bg-primary/30",
secondary:
"bg-secondary/10 dark:bg-secondary/20 backdrop-blur-md text-secondary-700 dark:text-secondary-400 hover:bg-secondary/20 dark:hover:bg-secondary/30",
success:
"bg-success/10 dark:bg-success/20 backdrop-blur-md text-success-700 dark:text-success-400 hover:bg-success/20 dark:hover:bg-success/30",
warning:
"bg-warning/10 dark:bg-warning/20 backdrop-blur-md text-warning-700 dark:text-warning-400 hover:bg-warning/20 dark:hover:bg-warning/30",
danger:
"bg-danger/10 dark:bg-danger/20 backdrop-blur-md text-danger-700 dark:text-danger-400 hover:bg-danger/20 dark:hover:bg-danger/30",
};
const shadowColorClassMap: Record<HeroButtonColor, string> = {
default: "shadow-md shadow-default-400/40",
primary: "shadow-md shadow-primary-500/35",
secondary: "shadow-md shadow-secondary-500/35",
success: "shadow-md shadow-success-500/35",
warning: "shadow-md shadow-warning-500/40",
danger: "shadow-md shadow-danger-500/35",
};
function mapColorClass(
color: HeroButtonColor,
variant: HeroButtonVariant,
): string {
if (variant === "bordered") {
return borderedColorClassMap[color];
}
if (variant === "light") {
return lightColorClassMap[color];
}
if (variant === "flat") {
return flatColorClassMap[color];
}
if (variant === "solid" || variant === "shadow") {
return solidColorClassMap[color] ?? "";
}
return "";
}
function mapShadowClass(
color: HeroButtonColor,
variant: HeroButtonVariant,
): string {
if (variant !== "shadow") {
return "";
}
return shadowColorClassMap[color];
}
export const Button = React.forwardRef<
HTMLButtonElement,
ButtonProps & {
isDisabled?: boolean;
}
>(
(
{
children,
className,
color = "default",
disabled,
endContent,
isIconOnly = false,
isLoading = false,
isDisabled,
onClick,
onPress,
size = "md",
startContent,
type = "button",
variant = "solid",
...props
},
ref,
) => {
const resolvedVariant = mapVariant(color, variant);
const resolvedSize = mapSize(size, isIconOnly);
const resolvedDisabled = Boolean(disabled || isDisabled || isLoading);
const resolvedColorClass = mapColorClass(color, variant);
const resolvedShadowClass = mapShadowClass(color, variant);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(event);
onPress?.(event);
};
return (
<BaseButton
ref={ref}
className={cn(
isIconOnly ? "p-0" : "",
resolvedColorClass,
resolvedShadowClass,
className,
)}
disabled={resolvedDisabled}
size={resolvedSize}
type={type}
variant={resolvedVariant}
onClick={handleClick}
{...props}
>
{isLoading ? (
<Loader2Icon className="mr-2 h-4 w-4 animate-spin" />
) : null}
{startContent}
{isIconOnly ? null : children}
{isIconOnly ? children : null}
{endContent}
</BaseButton>
);
},
);
Button.displayName = "HeroBridgeButton";