Files
stack/packages/ui/src/components/Button.tsx
Jason Woltje 44011f4e27 feat(ui): align design tokens and update component variants (MS15-UI-001, MS15-UI-002)
Replace hardcoded Tailwind color classes with CSS custom property inline
styles across all packages/ui components (Button, Card, Badge, Input,
Select, Textarea, Avatar, Modal, Toast).

Badge: add new variants (badge-teal, badge-amber, badge-blue, badge-purple,
badge-pulse) with mono font and pill shape.

Button: add success variant, hover states via React state.

Card: flat design, no shadows, semantic border/surface tokens.

New: Dot component (teal, blue, amber, red, muted) with glow effect.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 15:04:00 -06:00

122 lines
2.8 KiB
TypeScript

import { useState, type ButtonHTMLAttributes, type ReactNode, type ReactElement } from "react";
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "ghost" | "danger" | "success";
size?: "sm" | "md" | "lg";
children: ReactNode;
}
interface VariantStyle {
base: React.CSSProperties;
hover: React.CSSProperties;
}
type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "success";
const variantStyles: Record<ButtonVariant, VariantStyle> = {
primary: {
base: {
background: "var(--ms-blue-500)",
color: "#fff",
border: "none",
},
hover: {
background: "var(--ms-blue-400)",
boxShadow: "0 4px 16px rgba(47,128,255,0.3)",
},
},
secondary: {
base: {
background: "transparent",
border: "1px solid var(--border)",
color: "var(--text-2)",
},
hover: {
background: "var(--surface)",
color: "var(--text)",
},
},
ghost: {
base: {
background: "transparent",
border: "1px solid var(--border)",
color: "var(--text-2)",
},
hover: {
background: "var(--surface)",
color: "var(--text)",
},
},
danger: {
base: {
background: "rgba(229,72,77,0.12)",
border: "1px solid rgba(229,72,77,0.3)",
color: "var(--danger)",
},
hover: {
background: "rgba(229,72,77,0.2)",
},
},
success: {
base: {
background: "rgba(20,184,166,0.12)",
border: "1px solid rgba(20,184,166,0.3)",
color: "var(--success)",
},
hover: {
background: "rgba(20,184,166,0.2)",
},
},
};
type ButtonSize = "sm" | "md" | "lg";
const sizeStyles: Record<ButtonSize, string> = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-base",
lg: "px-6 py-3 text-lg",
};
export function Button({
variant = "primary",
size = "md",
children,
className = "",
style,
onMouseEnter,
onMouseLeave,
disabled,
...props
}: ButtonProps): ReactElement {
const [isHovered, setIsHovered] = useState(false);
const vStyles = variantStyles[variant];
const baseClass = `inline-flex items-center justify-center font-medium rounded-md transition-colors ${sizeStyles[size]} ${className}`;
const computedStyle: React.CSSProperties = {
...vStyles.base,
...(isHovered && !disabled ? vStyles.hover : {}),
...(disabled ? { opacity: 0.5, cursor: "not-allowed" } : { cursor: "pointer" }),
...style,
};
return (
<button
className={baseClass}
style={computedStyle}
disabled={disabled}
onMouseEnter={(e) => {
setIsHovered(true);
onMouseEnter?.(e);
}}
onMouseLeave={(e) => {
setIsHovered(false);
onMouseLeave?.(e);
}}
{...props}
>
{children}
</button>
);
}