Files
srbhr--resume-matcher/apps/frontend/components/ui/button.tsx
T
wehub-resource-sync 5bdf4cc89a
Publish Docker Image / publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:36 +08:00

186 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as React from 'react';
import { cn } from '@/lib/utils';
/**
* Swiss International Style Button Component
*
* Design Principles:
* - Hard shadows (no blur) that create depth
* - Square corners (rounded-none) - Brutalist aesthetic
* - High contrast black borders
* - Hover: translate + shadow removal creates "press" effect
* - Clear semantic variants for different actions
*/
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/**
* Visual variant determining color and purpose:
* - `default`: Hyper Blue (#1D4ED8) - Primary actions (save, submit, create)
* - `destructive`: Alert Red (#DC2626) - Destructive actions (delete, remove)
* - `success`: Signal Green (#15803D) - Positive actions (download, confirm, complete)
* - `warning`: Alert Orange (#F97316) - Caution actions (reset, clear, undo)
* - `outline`: Transparent + black border - Secondary actions (cancel, back)
* - `secondary`: Panel Grey (#E5E5E0) - Tertiary actions
* - `ghost`: No background - Subtle actions (icon buttons, navigation)
* - `link`: Text only with underline - Inline links
*/
variant?:
| 'default'
| 'destructive'
| 'success'
| 'warning'
| 'outline'
| 'secondary'
| 'ghost'
| 'link';
/**
* Button size:
* - `default`: Standard button (h-10)
* - `sm`: Small button (h-8)
* - `lg`: Large button (h-12)
* - `icon`: Square icon button (h-9 w-9)
*/
size?: 'default' | 'sm' | 'lg' | 'icon';
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = 'default', size = 'default', ...props }, ref) => {
// Base styles applied to ALL buttons
// Swiss Design: clean, functional, high contrast
const baseStyles = cn(
// Layout & Typography
'relative inline-flex items-center justify-center gap-2',
'whitespace-nowrap text-sm font-medium font-mono uppercase tracking-wide',
// Transitions — only the properties that actually change on hover/active.
// Avoids the perf footgun of `transition-all` and matches Swiss "snap" feel.
'transition-[transform,box-shadow,background-color] duration-100 ease-out',
// Focus state - sharp blue ring (not soft glow)
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-700 focus-visible:ring-offset-2',
// Disabled state
'disabled:pointer-events-none disabled:opacity-50',
// SVG icon sizing
"[&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0",
// Swiss Design: NO rounded corners
'rounded-none'
);
// Hit-area expansion for icon-only buttons. Many call sites override
// size="icon" with smaller h-X w-X classes for dense toolbars (h-8 w-8,
// h-7 w-7, etc.) — those visible sizes are under WCAG 2.5.8's 44×44 target
// size minimum. The ::before pseudo-element extends the touch area by 6px
// on each side without affecting visible layout, so a 32×32 button gets a
// 44×44 touch target. For h-7 and smaller, the touch area still falls
// short — those need an additional inline override at the call site
// (e.g. before:-inset-[10px]).
const iconHitArea = "before:absolute before:-inset-1.5 before:content-['']";
// Variant styles - each has distinct purpose and color
const variants = {
// PRIMARY - Hyper Blue (#1D4ED8 / blue-700)
// Use for: Save, Submit, Create, Primary CTA
default: cn(
'bg-blue-700 text-white',
'border border-black',
'shadow-sw-sm',
'hover:bg-blue-800',
'hover:translate-y-[1px] hover:translate-x-[1px] hover:shadow-none',
'active:translate-y-[2px] active:translate-x-[2px]'
),
// DESTRUCTIVE - Alert Red (#DC2626 / red-600)
// Use for: Delete, Remove, Destroy, Dangerous actions
destructive: cn(
'bg-red-600 text-white',
'border border-black',
'shadow-sw-sm',
'hover:bg-red-700',
'hover:translate-y-[1px] hover:translate-x-[1px] hover:shadow-none',
'active:translate-y-[2px] active:translate-x-[2px]'
),
// SUCCESS - Signal Green (#15803D / green-700)
// Use for: Download, Confirm, Complete, Positive actions
success: cn(
'bg-green-700 text-white',
'border border-black',
'shadow-sw-sm',
'hover:bg-green-800',
'hover:translate-y-[1px] hover:translate-x-[1px] hover:shadow-none',
'active:translate-y-[2px] active:translate-x-[2px]'
),
// WARNING - Alert Orange (#F97316 / orange-500)
// Use for: Reset, Clear, Undo, Caution actions
warning: cn(
'bg-orange-500 text-white',
'border border-black',
'shadow-sw-sm',
'hover:bg-orange-600',
'hover:translate-y-[1px] hover:translate-x-[1px] hover:shadow-none',
'active:translate-y-[2px] active:translate-x-[2px]'
),
// OUTLINE - Canvas background with black border
// Use for: Cancel, Back, Secondary actions, Navigation
outline: cn(
'bg-background text-black',
'border border-black',
'shadow-sw-sm',
'hover:bg-secondary',
'hover:translate-y-[1px] hover:translate-x-[1px] hover:shadow-none',
'active:translate-y-[2px] active:translate-x-[2px]'
),
// SECONDARY - Panel Grey (#E5E5E0)
// Use for: Less prominent actions, Toolbar buttons
secondary: cn(
'bg-secondary text-black',
'border border-black',
'shadow-sw-sm',
'hover:bg-[#D8D8D2]',
'hover:translate-y-[1px] hover:translate-x-[1px] hover:shadow-none',
'active:translate-y-[2px] active:translate-x-[2px]'
),
// GHOST - No background, minimal styling
// Use for: Icon buttons, Subtle navigation, Toolbars
ghost: cn(
'bg-transparent text-black',
'border-none shadow-none',
'hover:bg-paper-tint',
'active:bg-paper-tint'
),
// LINK - Text only with underline
// Use for: Inline links, Text navigation
link: cn(
'bg-transparent text-blue-700',
'border-none shadow-none',
'underline-offset-4 hover:underline',
'p-0 h-auto'
),
};
// Size styles. Icon variant is 44×44px to meet WCAG 2.2 AA target size
// (success criterion 2.5.8). Call sites that override the visible size
// with smaller h-X w-X classes get the touch-area expansion via the
// iconHitArea overlay above.
const sizes = {
default: 'h-10 px-6 py-2',
sm: 'h-8 px-4 py-1 text-xs',
lg: 'h-12 px-8 py-3 text-base',
icon: cn('h-11 w-11 p-0', iconHitArea),
};
const variantClass = variants[variant];
const sizeClass = sizes[size];
return (
<button ref={ref} className={cn(baseStyles, variantClass, sizeClass, className)} {...props} />
);
}
);
Button.displayName = 'Button';
export { Button };