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.1 KB · 64 lines tsx
Raw Blame History
1import type { HTMLAttributes, ReactNode } from "react";2import { cn } from "./cn";34/** Flat bordered surface. No shadow, 3 px radius. */5export function Card({ className, children, ...rest }: HTMLAttributes<HTMLDivElement>) {6  return (7    <div className={cn("rounded-sm border border-hair bg-surface", className)} {...rest}>8      {children}9    </div>10  );11}1213export function CardHeader({ title, description, aside, className, level = 3 }: { title: ReactNode; description?: ReactNode; aside?: ReactNode; className?: string; level?: 2 | 3 }) {14  const H = level === 2 ? "h2" : "h3";15  return (16    <div className={cn("flex items-start justify-between gap-3 border-b border-hair px-3 py-2.5 sm:px-4", className)}>17      <div className="min-w-0">18        <H className="text-[13.5px] font-semibold leading-tight text-ink">{title}</H>19        {description && <p className="mt-0.5 text-[12px] text-ink-3">{description}</p>}20      </div>21      {aside && <div className="shrink-0">{aside}</div>}22    </div>23  );24}2526export function CardBody({ className, children, padded = true }: { className?: string; children: ReactNode; padded?: boolean }) {27  return <div className={cn(padded && "px-3 py-3 sm:px-4", className)}>{children}</div>;28}2930/** Page-level section: heading row + content, hairline top. */31export function Section({ title, description, aside, children, className, id, level = 2 }: { title: ReactNode; description?: ReactNode; aside?: ReactNode; children: ReactNode; className?: string; id?: string; level?: 2 | 3 }) {32  const H = level === 2 ? "h2" : "h3";33  return (34    <section id={id} className={cn("min-w-0 border-t border-hair pt-4", className)}>35      <div className="mb-3 flex items-end justify-between gap-3">36        <div className="min-w-0">37          <H className={cn("font-semibold leading-tight text-ink", level === 2 ? "text-[15px] sm:text-[16px]" : "text-[14px]")}>{title}</H>38          {description && <p className="mt-0.5 text-[12px] text-ink-3">{description}</p>}39        </div>40        {aside && <div className="shrink-0 text-right">{aside}</div>}41      </div>42      {children}43    </section>44  );45}4647/** Uniform page container. */48export function Page({ children, className, wide }: { children: ReactNode; className?: string; wide?: boolean }) {49  return <div className={cn("mx-auto w-full px-3 sm:px-5 lg:px-8", wide ? "max-w-[1600px]" : "max-w-[1280px]", className)}>{children}</div>;50}5152export function PageHeader({ title, eyebrow, description, aside, className }: { title: ReactNode; eyebrow?: ReactNode; description?: ReactNode; aside?: ReactNode; className?: string }) {53  return (54    <header className={cn("flex flex-col gap-3 py-5 sm:flex-row sm:items-end sm:justify-between sm:py-7", className)}>55      <div className="min-w-0">56        {eyebrow && <div className="label-caps mb-1.5">{eyebrow}</div>}57        <h1 className="text-[22px] font-semibold leading-tight tracking-[-0.02em] text-ink sm:text-[28px]">{title}</h1>58        {description && <p className="mt-1.5 max-w-2xl text-[13.5px] text-ink-2">{description}</p>}59      </div>60      {aside && <div className="shrink-0">{aside}</div>}61    </header>62  );63}64