fix(frontend): restore button border and color semantics

This commit is contained in:
Sagit
2026-02-19 06:36:41 +00:00
parent 2308b25bcf
commit 9dcf9a1a43
5 changed files with 164 additions and 3 deletions
@@ -47,3 +47,6 @@
- [2026-02-19] 完成 S8:构建验收通过(`npm run build`),`package.json` 已无 `heroui/nextui` 依赖
- [2026-02-19] 验证结果:业务代码中 `@heroui/*` 导入为 `0`,已统一替换为 `@/shadcn-bridge/heroui/*`(22 文件,106 处)
- [2026-02-19] 后续修复:`src/components/ui/button.tsx` 与 `src/shadcn-bridge/heroui/button.tsx` 改为 `forwardRef`,消除 `DropdownMenuTrigger asChild` 场景 ref 警告;复构建通过
- [2026-02-19] 回归修复:定位“按钮边框/颜色丢失”根因是 Tailwind v4 下语义色未生成;新增 `src/styles/tailwind-theme.pcss`(由 `src/styles/globals.css` 引入)承载 `@theme inline` token 映射,恢复 `bg-primary`/`text-foreground`/`border-input` 等语义类输出
- [2026-02-19] 回归修复:增强 `src/shadcn-bridge/heroui/button.tsx` 的 `solid/light/flat/bordered/shadow` 颜色映射,并修正 `src/components/ui/button.tsx` 的 `outline/ghost` hover 语义类,避免依赖未定义的 `accent` 色
- [2026-02-19] 复验结果:`npm run build` 通过;编译产物已包含关键语义类;页面级视觉回归(forward/tunnel/node/user)通过(后端未启动时仅保留 `ERR_CONNECTION_REFUSED` 噪音)
+2 -2
View File
@@ -11,9 +11,9 @@ const buttonVariants = cva(
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-danger text-white hover:bg-danger/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
outline: "border border-input bg-background 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-accent hover:text-accent-foreground",
ghost: "hover:bg-default-100 hover:text-foreground dark:hover:bg-default-200/40",
link: "text-primary underline-offset-4 hover:underline",
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",
@@ -57,6 +57,79 @@ function mapSize(size: HeroButtonSize, isIconOnly: boolean): "default" | "sm" |
return "default";
}
const solidColorClassMap: Partial<Record<HeroButtonColor, string>> = {
default:
"bg-default-200 text-default-800 hover:bg-default-300 dark:bg-default-100 dark:text-default-800 dark:hover:bg-default-200",
success: "bg-success text-white hover:bg-success-600",
warning: "bg-warning text-warning-900 hover:bg-warning-400",
};
const borderedColorClassMap: Record<HeroButtonColor, string> = {
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",
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-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",
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",
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",
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> = {
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 & {
@@ -86,6 +159,8 @@ export const Button = React.forwardRef<
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);
@@ -94,7 +169,7 @@ export const Button = React.forwardRef<
return (
<BaseButton
className={cn(isIconOnly ? "p-0" : "", className)}
className={cn(isIconOnly ? "p-0" : "", resolvedColorClass, resolvedShadowClass, className)}
disabled={resolvedDisabled}
ref={ref}
size={resolvedSize}
+1
View File
@@ -1,5 +1,6 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "./tailwind-theme.pcss";
:root {
--safe-area-top: env(safe-area-inset-top, 0px);
@@ -0,0 +1,82 @@
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-content1: var(--content1);
--color-divider: var(--divider);
--color-default-50: var(--default-50);
--color-default-100: var(--default-100);
--color-default-200: var(--default-200);
--color-default-300: var(--default-300);
--color-default-400: var(--default-400);
--color-default-500: var(--default-500);
--color-default-600: var(--default-600);
--color-default-700: var(--default-700);
--color-default-800: var(--default-800);
--color-default-900: var(--default-900);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-primary-50: var(--primary-50);
--color-primary-100: var(--primary-100);
--color-primary-200: var(--primary-200);
--color-primary-300: var(--primary-300);
--color-primary-400: var(--primary-400);
--color-primary-500: var(--primary-500);
--color-primary-600: var(--primary-600);
--color-primary-700: var(--primary-700);
--color-primary-800: var(--primary-800);
--color-primary-900: var(--primary-900);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary-50: var(--secondary-50);
--color-secondary-100: var(--secondary-100);
--color-secondary-200: var(--secondary-200);
--color-secondary-300: var(--secondary-300);
--color-secondary-400: var(--secondary-400);
--color-secondary-500: var(--secondary-500);
--color-secondary-600: var(--secondary-600);
--color-secondary-700: var(--secondary-700);
--color-secondary-800: var(--secondary-800);
--color-secondary-900: var(--secondary-900);
--color-danger: var(--danger);
--color-danger-50: var(--danger-50);
--color-danger-100: var(--danger-100);
--color-danger-200: var(--danger-200);
--color-danger-300: var(--danger-300);
--color-danger-400: var(--danger-400);
--color-danger-500: var(--danger-500);
--color-danger-600: var(--danger-600);
--color-danger-700: var(--danger-700);
--color-danger-800: var(--danger-800);
--color-danger-900: var(--danger-900);
--color-success: var(--success);
--color-success-50: var(--success-50);
--color-success-100: var(--success-100);
--color-success-200: var(--success-200);
--color-success-300: var(--success-300);
--color-success-400: var(--success-400);
--color-success-500: var(--success-500);
--color-success-600: var(--success-600);
--color-success-700: var(--success-700);
--color-success-800: var(--success-800);
--color-success-900: var(--success-900);
--color-warning: var(--warning);
--color-warning-50: var(--warning-50);
--color-warning-100: var(--warning-100);
--color-warning-200: var(--warning-200);
--color-warning-300: var(--warning-300);
--color-warning-400: var(--warning-400);
--color-warning-500: var(--warning-500);
--color-warning-600: var(--warning-600);
--color-warning-700: var(--warning-700);
--color-warning-800: var(--warning-800);
--color-warning-900: var(--warning-900);
}