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 (
)
}
// 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 && }
))}
)
}
// 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'