SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
4 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
3.4 KB · 94 lines tsx
Raw Blame History
1import Link from "next/link";2import type { ButtonHTMLAttributes, ReactNode } from "react";3import { cn } from "./cn";45type Variant = "primary" | "secondary" | "ghost" | "accent" | "danger";6type Size = "xs" | "sm" | "md" | "lg";78const base = "inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-sm border font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 select-none";9const variants: Record<Variant, string> = {10  primary: "border-ink bg-ink text-paper hover:bg-ink-2 hover:border-ink-2",11  secondary: "border-hair-2 bg-surface text-ink hover:bg-surface-2",12  ghost: "border-transparent bg-transparent text-ink-2 hover:bg-surface-2 hover:text-ink",13  accent: "border-accent bg-accent text-white hover:bg-accent-ink hover:border-accent-ink",14  danger: "border-danger/40 bg-transparent text-danger hover:bg-danger/10",15};16// Touch first: every size is ≥ 40 px tall under 640 px, the compact heights apply from `sm`.17const sizes: Record<Size, string> = {18  xs: "h-10 px-2 text-[12px] sm:h-7",19  sm: "h-10 px-2.5 text-[13px] sm:h-8",20  md: "h-10 px-3 text-[13.5px] sm:h-9",21  lg: "h-11 px-4 text-[14px]",22};2324export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {25  variant?: Variant;26  size?: Size;27  href?: string;28  external?: boolean;29  children?: ReactNode;30}3132export function Button({ variant = "secondary", size = "md", href, external, className, children, type = "button", ...rest }: ButtonProps) {33  const cls = cn(base, variants[variant], sizes[size], className);34  if (href) {35    if (external) {36      return (37        <a href={href} className={cls} target="_blank" rel="noopener noreferrer">38          {children}39        </a>40      );41    }42    return (43      <Link href={href} className={cls}>44        {children}45      </Link>46    );47  }48  return (49    <button type={type} className={cls} {...rest}>50      {children}51    </button>52  );53}5455export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {56  label: string;57  size?: "sm" | "md" | "lg";58  variant?: "secondary" | "ghost";59  active?: boolean;60}6162/** ≥ 40 px hit area under 640 px (touch); compact 32–36 px from `sm`, 44 px for `lg`. */63export function IconButton({ label, size = "md", variant = "ghost", active, className, children, type = "button", ...rest }: IconButtonProps) {64  const dims = size === "sm" ? "size-10 sm:size-8" : size === "lg" ? "size-11" : "size-10 sm:size-9";65  return (66    <button67      type={type}68      aria-label={label}69      title={label}70      aria-pressed={active}71      className={cn(72        "inline-flex shrink-0 items-center justify-center rounded-sm border transition-colors disabled:opacity-50",73        dims,74        variant === "secondary" ? "border-hair-2 bg-surface text-ink-2 hover:text-ink hover:bg-surface-2" : "border-transparent text-ink-2 hover:bg-surface-2 hover:text-ink",75        active && "bg-surface-3 text-ink",76        className,77      )}78      {...rest}79    >80      {children}81    </button>82  );83}8485/** "View all →" style inline link. */86export function MoreLink({ href, children = "View all", className }: { href: string; children?: ReactNode; className?: string }) {87  return (88    <Link href={href} className={cn("inline-flex min-h-10 items-center gap-1 whitespace-nowrap text-[12.5px] font-medium text-ink-2 hover:text-accent sm:min-h-0", className)}>89      {children}90      <span aria-hidden>→</span>91    </Link>92  );93}94