import Link from "next/link"; import { cn } from "@/lib/utils"; /** * Shared, server-safe building blocks of the master-data screens (customers, sites, teams, * documents). Controls are 44 px high (Brandbook §12.2 touch targets); colors only via tokens. */ export const controlClass = "h-11 w-full min-w-0 rounded-lg border border-input bg-card px-3 text-sm outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 disabled:opacity-60"; export const textareaClass = "min-h-24 w-full rounded-lg border border-input bg-card px-3 py-2 text-sm outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive"; export const buttonLinkClass = "inline-flex min-h-11 items-center justify-center gap-1.5 rounded-lg border border-border bg-background px-4 font-heading text-sm font-semibold transition-colors hover:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50 outline-none"; export const primaryButtonClass = "inline-flex min-h-11 items-center justify-center gap-1.5 rounded-lg bg-[var(--ui-accent)] px-4 font-heading text-sm font-semibold text-[var(--ui-accent-foreground)] transition-opacity hover:opacity-90 focus-visible:ring-3 focus-visible:ring-ring/50 outline-none disabled:opacity-60"; export function Field({ id, label, error, hint, required, className, children, }: { id: string; label: string; error?: string; hint?: string; required?: boolean; className?: string; children: React.ReactNode; }) { return (
{children} {hint && !error &&

{hint}

} {error && ( )}
); } export function FormSection({ title, children, className }: { title: string; children: React.ReactNode; className?: string }) { return (
{title} {children}
); } export function TabNav({ tabs, active, label }: { tabs: { key: string; label: string; href: string; count?: number }[]; active: string; label: string }) { return ( ); } export function Pagination({ page, pageSize, total, hrefFor, labels, }: { page: number; pageSize: number; total: number; hrefFor: (page: number) => string; labels: { prev: string; next: string; summary: string }; }) { if (total <= pageSize && page === 1) return total > 0 ?

{labels.summary}

: null; const last = Math.max(1, Math.ceil(total / pageSize)); return (

{labels.summary}

{page > 1 ? ( {labels.prev} ) : ( {labels.prev} )} {page < last ? ( {labels.next} ) : ( {labels.next} )}
); } export function Banner({ tone, children }: { tone: "ok" | "warn" | "risk" | "info"; children: React.ReactNode }) { const tones = { ok: "border-[var(--ok)] text-[var(--ok)]", warn: "border-[var(--warn)] text-[var(--warn)]", risk: "border-[var(--risk)] text-[var(--risk)]", info: "border-[var(--info)] text-[var(--info)]", }; return (
{children}
); } /** Label/value list for read views. */ export function DefinitionList({ items }: { items: { label: string; value: React.ReactNode }[] }) { return (
{items.map((it) => (
{it.label}
{it.value || "—"}
))}
); } export function Card({ children, className }: { children: React.ReactNode; className?: string }) { return
{children}
; } export function paginationSummary(page: number, pageSize: number, total: number) { const from = total === 0 ? 0 : (page - 1) * pageSize + 1; const to = Math.min(total, page * pageSize); return { from, to, total }; } /** Build `path?…` from params, dropping empty values. */ export function hrefWith(path: string, params: Record): string { const sp = new URLSearchParams(); for (const [k, v] of Object.entries(params)) if (v !== undefined && v !== null && v !== "") sp.set(k, String(v)); const qs = sp.toString(); return qs ? `${path}?${qs}` : path; }