/** * The design system's component vocabulary, in one place. Every other file * composes these rather than re-spelling the class strings. * * Ported from FlightTube: slate and sky, a 9–15px type ladder, outline-first * controls, borders for separation and shadows only for elevation. */ import type { ReactNode } from "react"; /** Every control in the app is this tall, so a row of mixed ones lines up. */ export const CONTROL_H = "h-[30px]"; export const ICON_BTN = `grid ${CONTROL_H} w-[30px] shrink-0 place-items-center rounded-lg cursor-pointer ` + "disabled:cursor-not-allowed disabled:opacity-40"; export const HEADING = "text-[11px] font-bold uppercase tracking-widest text-slate-500 dark:text-slate-400"; export const LABEL = "text-[12px] text-slate-500 dark:text-slate-400"; export const HELP = "text-[11px] leading-snug text-slate-500 dark:text-slate-400"; export const SECTION = "border-b border-slate-200 px-4 py-4 dark:border-slate-800"; /* No width: callers set it. A `w-full` baked in here loses the specificity coin-toss against a `w-[110px]` or a `flex-1` at the call site. */ export const INPUT = `${CONTROL_H} min-w-0 rounded-lg border border-slate-300 bg-white px-3 text-[12px] outline-none ` + "placeholder:text-slate-400 dark:border-slate-700 dark:bg-slate-800 dark:placeholder:text-slate-500"; export const SUBPANEL = "rounded-lg bg-slate-50 p-3 dark:bg-slate-800/50"; const BTN_BASE = `inline-flex ${CONTROL_H} items-center justify-center rounded-lg text-[12px] ` + "disabled:cursor-not-allowed cursor-pointer"; export const BTN = `${BTN_BASE} border border-slate-300 px-2.5 font-medium ` + "hover:border-sky-500 hover:text-sky-600 disabled:opacity-40 " + "dark:border-slate-700 dark:hover:border-sky-500 dark:hover:text-sky-400"; export const BTN_PRIMARY = `${BTN_BASE} bg-sky-500 px-3 font-semibold text-white hover:bg-sky-400 disabled:opacity-40`; export const BTN_DANGER = `${BTN_BASE} bg-red-600 px-3 font-semibold text-white hover:bg-red-500`; /** Header actions: quieter than a secondary button, still a real target. */ export const BTN_CHROME = `inline-flex ${CONTROL_H} items-center rounded-lg px-2 text-[12px] font-medium text-slate-500 ` + "cursor-pointer hover:bg-slate-100 hover:text-slate-900 disabled:opacity-40 " + "disabled:hover:bg-transparent dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white"; export const BTN_QUIET = "text-[11px] text-slate-500 underline underline-offset-2 cursor-pointer " + "hover:text-sky-600 dark:text-slate-400 dark:hover:text-sky-400"; /** Icon-button skin used throughout the chrome. */ export const ICON_CHROME = `${ICON_BTN} text-slate-500 hover:bg-slate-100 hover:text-slate-900 ` + "dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white"; export function SectionHeading({ children }: { children: ReactNode }) { return

{children}

; } /** Bordered container, borderless children — the group's outline does the framing. */ export function Segmented({ options, value, onChange, }: { options: Array<{ value: T; label: ReactNode; title?: string }>; value: T; onChange: (v: T) => void; }) { return (
{options.map((o) => { const active = o.value === value; return ( ); })}
); } /** States a fact in passing — a quality, a status. Never a control. */ export function Badge({ children, tone = "neutral", title, }: { children: ReactNode; tone?: "neutral" | "accent" | "danger"; title?: string; }) { const skin = tone === "accent" ? "bg-sky-500/15 text-sky-700 dark:text-sky-300" : tone === "danger" ? "bg-red-500/15 text-red-600 dark:text-red-400" : "bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-300"; return ( {children} ); } /** A modal for anything needing a decision or reporting a failure. */ export function Dialog({ title, children, onCancel, confirmLabel, onConfirm, destructive, wide, footer, }: { title: string; children: ReactNode; onCancel: () => void; confirmLabel?: string; onConfirm?: () => void; destructive?: boolean; wide?: boolean; footer?: ReactNode; }) { return (
e.stopPropagation()} className={`w-full ${wide ? "max-w-2xl max-h-[82vh] overflow-y-auto" : "max-w-sm"} rounded-2xl border border-slate-300 bg-white p-5 shadow-2xl dark:border-slate-700 dark:bg-slate-900`} >

{title}

{children}
{footer ?? (
{onConfirm && ( )}
)}
); } /** * An app's mark. Google's favicon service is used rather than the site's own * /favicon.ico, because many work tools sit behind a login that would return * the sign-in page's icon — or a 403 — to a request without a session. */ export function Favicon({ url, name, size = 16, }: { url: string; name: string; size?: number; }) { let host = ""; try { host = new URL(url).hostname; } catch { host = ""; } const letter = name.trim().charAt(0).toUpperCase() || "?"; return ( {letter} {host && ( { // Leave the letter showing rather than a broken-image glyph. e.currentTarget.style.display = "none"; }} /> )} ); }