Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | 196x 217x 217x | import type { ButtonHTMLAttributes, ReactNode } from 'react';
/**
* Reusable button. Styling lives in globals.css (`.site-button*`) so the
* interactive states are CSS-driven; this component only composes class names.
*
* Variants: `ghost`, `outline` (secondary), `brand` (primary).
* Sizes: `sm` 32px, `md` 44px (default), `lg` 52px.
*
* Two forms:
* - Text: pass `children`, optionally with an `iconLeft` / `iconRight` (20px).
* - Icon-only: pass `iconOnly` and a single icon as `children`. Square; always
* supply an `aria-label` since there's no visible text.
*/
type ButtonVariant = 'ghost' | 'outline' | 'brand';
type ButtonSize = 'sm' | 'md' | 'lg';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
iconLeft?: ReactNode;
iconRight?: ReactNode;
iconOnly?: boolean;
}
function ButtonIcon({ children }: { children: ReactNode }) {
return (
<span className="site-button__icon" aria-hidden="true">
{children}
</span>
);
}
export function Button({
variant = 'ghost',
size = 'md',
iconLeft,
iconRight,
iconOnly = false,
className,
type = 'button',
children,
...rest
}: ButtonProps) {
const classes = [
'site-button',
`site-button--${variant}`,
`site-button--${size}`,
iconOnly ? 'site-button--icon' : '',
className ?? '',
]
.filter(Boolean)
.join(' ');
return (
<button type={type} className={classes} {...rest}>
{iconOnly ? (
<ButtonIcon>{children}</ButtonIcon>
) : (
<>
{iconLeft && <ButtonIcon>{iconLeft}</ButtonIcon>}
{children}
{iconRight && <ButtonIcon>{iconRight}</ButtonIcon>}
</>
)}
</button>
);
}
|