- Add .gitignore, .yarnrc.yml, and initial project metadata (AGENTS.md, DESIGN.md, package.json, Yarn lock, Tailwind, PostCSS, Next.js config, TypeScript config) - Add CSV data files (users, employees, contracts, attendances) - Implement API routes for auth, employees, contracts, attendances, analytics, and file upload - Add core pages (dashboard, employees, attendances, profile, login, main layout) - Add UI components (avatar, avatar picker, badge, button, card, input, modal, select, sidebar, navigation shell, top header) - Add attendance features (table, clock widget, unified permit modal with file upload, upload API) - Add employee management (contract timeline with file upload, employee form modal, employee table) - Add dashboard widgets (leaderboard, early bird, latecomer, night owl, day status feed, date filter bar) - Add utilities (avatar generator, analytics calculations, CSV DB layer, auth context) - Add type definitions for auth, employee, contract, attendance, dashboard All changes are synchronized with documentation (AGENTS.md, DESIGN.md, walkthrough).
82 lines
2.4 KiB
TypeScript
82 lines
2.4 KiB
TypeScript
import React from 'react';
|
|
import { clsx } from 'clsx';
|
|
import { twMerge } from 'tailwind-merge';
|
|
|
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'pastel';
|
|
size?: 'sm' | 'md' | 'lg';
|
|
isLoading?: boolean;
|
|
}
|
|
|
|
export const Button: React.FC<ButtonProps> = ({
|
|
children,
|
|
className,
|
|
variant = 'primary',
|
|
size = 'md',
|
|
isLoading = false,
|
|
disabled,
|
|
...props
|
|
}) => {
|
|
const baseStyles =
|
|
'inline-flex items-center justify-center font-semibold rounded-xl transition-all duration-150 active:scale-[0.98] disabled:opacity-50 disabled:pointer-events-none focus:outline-none focus:ring-2 focus:ring-offset-2';
|
|
|
|
const sizeStyles = {
|
|
sm: 'px-3 py-1.5 text-xs gap-1.5',
|
|
md: 'px-4 py-2 text-sm gap-2',
|
|
lg: 'px-6 py-2.5 text-base gap-2.5',
|
|
};
|
|
|
|
const variantStyles = {
|
|
primary:
|
|
'bg-brand-primary text-white hover:bg-brand-hover shadow-sm hover:shadow-brand-glow focus:ring-brand-primary',
|
|
secondary:
|
|
'bg-brand-secondary text-white hover:bg-brand-hover focus:ring-brand-secondary',
|
|
pastel:
|
|
'bg-brand-pastel/30 text-brand-dark hover:bg-brand-pastel/60 border border-brand-pastel focus:ring-brand-pastel',
|
|
outline:
|
|
'border border-slate-200 bg-white text-slate-700 hover:bg-slate-50 hover:border-slate-300 focus:ring-slate-300',
|
|
ghost:
|
|
'bg-transparent text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus:ring-slate-200',
|
|
danger:
|
|
'bg-red-600 text-white hover:bg-red-700 shadow-sm focus:ring-red-500',
|
|
};
|
|
|
|
return (
|
|
<button
|
|
className={twMerge(
|
|
clsx(
|
|
baseStyles,
|
|
sizeStyles[size],
|
|
variantStyles[variant],
|
|
className
|
|
)
|
|
)}
|
|
disabled={disabled || isLoading}
|
|
{...props}
|
|
>
|
|
{isLoading && (
|
|
<svg
|
|
className="animate-spin -ml-1 mr-2 h-4 w-4 text-current"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<circle
|
|
className="opacity-25"
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
strokeWidth="4"
|
|
/>
|
|
<path
|
|
className="opacity-75"
|
|
fill="currentColor"
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
/>
|
|
</svg>
|
|
)}
|
|
{children}
|
|
</button>
|
|
);
|
|
};
|