spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
1import Link from "next/link";2import type { ReactNode } from "react";3import { cx } from "@/lib/format";45export function Page({ children, className, wide }: { children: ReactNode; className?: string; wide?: boolean }) {6 return <div className={cx("mx-auto w-full px-3 py-5 sm:px-5 sm:py-7", wide ? "max-w-[1440px]" : "max-w-[1200px]", className)}>{children}</div>;7}89export function PageHeader({ title, lead, kicker, actions, className }: { title: ReactNode; lead?: ReactNode; kicker?: ReactNode; actions?: ReactNode; className?: string }) {10 return (11 <div className={cx("mb-5 flex flex-wrap items-end justify-between gap-3 sm:mb-7", className)}>12 <div className="min-w-0">13 {kicker && <div className="mb-1 text-[11px] font-medium uppercase tracking-wide text-ink-3">{kicker}</div>}14 <h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">{title}</h1>15 {lead && <p className="mt-1.5 max-w-2xl text-sm text-ink-2">{lead}</p>}16 </div>17 {actions && <div className="flex flex-wrap items-center gap-2">{actions}</div>}18 </div>19 );20}2122export function Section({ title, hint, href, hrefLabel = "View all", children, className, id, badge }: { title: ReactNode; hint?: ReactNode; href?: string; hrefLabel?: string; children: ReactNode; className?: string; id?: string; badge?: ReactNode }) {23 return (24 <section id={id} className={cx("mt-8 first:mt-0", className)}>25 <div className="mb-2.5 flex items-baseline justify-between gap-3 border-b border-rule pb-2">26 <div className="flex min-w-0 items-baseline gap-2">27 <h2 className="text-[15px] font-semibold tracking-tight">{title}</h2>28 {badge}29 {hint && <span className="hidden truncate text-xs text-ink-3 sm:inline">{hint}</span>}30 </div>31 {href && (32 <Link href={href} className="shrink-0 text-xs text-accent hover:underline">33 {hrefLabel} →34 </Link>35 )}36 </div>37 {children}38 </section>39 );40}4142export function Empty({ children, className }: { children: ReactNode; className?: string }) {43 return <div className={cx("rounded-md border border-dashed border-rule px-4 py-8 text-center text-sm text-ink-3", className)}>{children}</div>;44}4546export function Kv({ items, className, cols = 2 }: { items: Array<[ReactNode, ReactNode]>; className?: string; cols?: 1 | 2 | 3 | 4 }) {47 return (48 <dl className={cx("grid gap-x-6", cols === 1 ? "grid-cols-1" : cols === 2 ? "grid-cols-2" : cols === 3 ? "grid-cols-2 sm:grid-cols-3" : "grid-cols-2 sm:grid-cols-4", className)}>49 {items.map(([k, v], i) => (50 <div key={i} className="flex items-baseline justify-between gap-3 border-b border-rule py-1.5 text-sm">51 <dt className="text-ink-3">{k}</dt>52 <dd className="mono truncate text-right tnum">{v}</dd>53 </div>54 ))}55 </dl>56 );57}5859export function Stat({ label, value, sub, tone, className }: { label: ReactNode; value: ReactNode; sub?: ReactNode; tone?: "pos" | "neg" | "warn" | "muted"; className?: string }) {60 return (61 <div className={cx("min-w-0", className)}>62 <div className="text-[11px] font-medium uppercase tracking-wide text-ink-3">{label}</div>63 <div className={cx("mono mt-0.5 truncate text-xl font-semibold tracking-tight tnum sm:text-2xl", tone === "pos" && "text-positive", tone === "neg" && "text-negative", tone === "warn" && "text-warning", tone === "muted" && "text-ink-2")}>{value}</div>64 {sub && <div className="mt-0.5 truncate text-xs text-ink-3">{sub}</div>}65 </div>66 );67}6869export function Pill({ children, active, href, onClick }: { children: ReactNode; active?: boolean; href?: string; onClick?: () => void }) {70 const cls = cx("inline-flex h-8 items-center whitespace-nowrap rounded-full border px-3 text-xs", active ? "border-ink bg-ink text-canvas" : "border-rule text-ink-2 hover:border-rule-strong hover:text-ink");71 if (href)72 return (73 <Link href={href} className={cls}>74 {children}75 </Link>76 );77 return (78 <button type="button" onClick={onClick} className={cls}>79 {children}80 </button>81 );82}83