spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
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