import type { ComponentProps, ReactNode } from 'react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' import { Switch } from '@/components/ui/switch' import { triggerHaptic } from '@/lib/haptics' import type { IconComponent } from '@/lib/icons' import { cn } from '@/lib/utils' import { PAGE_INSET_X } from '../layout-constants' // `bare` drops the page gutters + tall bottom pad for embedding in a tighter // surface (e.g. the boot-failure recovery card owns its own padding). export function SettingsContent({ children, bare = false }: { children: ReactNode; bare?: boolean }) { return (
{children}
) } const PILL_VARIANT = { muted: 'muted', primary: 'default', success: 'success', warn: 'warn', destructive: 'destructive' } as const // Rest props spread through to the Badge's DOM node — REQUIRED for Radix // `asChild` composition (wrapping a Pill in `Tip` clones it with the hover // handlers and ref as props; swallowing them left every tooltip on a Pill // silently dead). export function Pill({ tone = 'muted', children, ...props }: { tone?: keyof typeof PILL_VARIANT; children: ReactNode } & Omit, 'variant'>) { return ( {children} ) } export function SectionHeading({ aside, icon: Icon, meta, title }: { // Right-aligned trailing content on the heading row (e.g. a compact status + // action), so a single-item section needn't repeat its own label as a row. aside?: ReactNode icon: IconComponent meta?: string title: string }) { return (
{title} {meta && {meta}} {aside &&
{aside}
}
) } // A titled section: heading + body with the shared vertical rhythm. Keeps the // heading and its content welded together so pages stop hand-rolling // `
` at every call site. export function SettingsSection({ aside, children, icon, meta, title }: { aside?: ReactNode children: ReactNode icon: IconComponent meta?: string title: string }) { return (
{children}
) } export function NavLink({ icon: Icon, label, active, onClick }: { icon: IconComponent label: string active: boolean onClick: () => void }) { return ( ) } export function ListRow({ title, description, hint, action, below, 'data-tour': dataTour, id, wide = false, className }: { title: ReactNode description?: ReactNode hint?: ReactNode action?: ReactNode below?: ReactNode /** Durable handle for tours (see lib/tour) — usually the field's schema key. */ 'data-tour'?: string id?: string wide?: boolean className?: string }) { return ( // Container-queried, not viewport-queried: the label/control split keys on // the row's own pane width, so a narrow detail column (messaging, split // views) stacks instead of squishing the label against minmax(15rem,…).
{title}
{description && (
{description}
)} {hint &&
{hint}
} {below}
{action &&
{action}
}
) } // A labelled on/off row — the canonical device-pref switch (haptic baked in). export function ToggleRow({ checked, description, disabled, label, onChange }: { checked: boolean description?: string disabled?: boolean label: string onChange: (on: boolean) => void }) { return ( { triggerHaptic('selection') onChange(on) }} /> } description={description} title={label} /> ) } // Skeleton primitives mirroring the settings layout rhythm — a loading page keeps // its shape (like ModelSettings) instead of collapsing to a centered spinner. export function SectionHeadingSkeleton() { return (
) } export function ListRowSkeleton({ wide = false }: { wide?: boolean }) { return (
{!wide && }
) } // A full settings page in its loading shape: an optional leading search field // over one or more sections, each an optional heading above a run of rows. // ``. export function SettingsSkeleton({ search = false, sections = [{ rows: 4 }] }: { search?: boolean sections?: { heading?: boolean; rows: number }[] }) { return ( {search && } {sections.map((section, i) => (
0 && 'mt-6')} key={i}> {section.heading && }
{Array.from({ length: section.rows }, (_, r) => ( ))}
))}
) } // Canonical implementation lives in components/ui; re-exported so the many // settings call sites keep their import path. export { EmptyState } from '@/components/ui/empty-state'