116 lines
3.1 KiB
TypeScript
116 lines
3.1 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
|
|
import { PageLoader } from '@/components/page-loader'
|
|
import { Button } from '@/components/ui/button'
|
|
import type { LucideIcon } from '@/lib/icons'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
export function SettingsContent({ children }: { children: ReactNode }) {
|
|
return (
|
|
<section className="min-h-0 overflow-hidden">
|
|
<div className="h-full min-h-0 overflow-y-auto px-8 py-6 pb-24">
|
|
<div className="mx-auto w-full max-w-5xl">{children}</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
export function Pill({ tone = 'muted', children }: { tone?: 'muted' | 'primary'; children: ReactNode }) {
|
|
return (
|
|
<span
|
|
className={cn(
|
|
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[0.66rem]',
|
|
tone === 'primary' ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'
|
|
)}
|
|
>
|
|
{children}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
export function SectionHeading({ icon: Icon, title, meta }: { icon: LucideIcon; title: string; meta?: string }) {
|
|
return (
|
|
<div className="mb-3 flex items-center gap-2 pt-3.5 text-sm font-medium">
|
|
<Icon className="size-4 text-muted-foreground" />
|
|
<span>{title}</span>
|
|
{meta && <Pill>{meta}</Pill>}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function NavLink({
|
|
icon: Icon,
|
|
label,
|
|
active,
|
|
onClick
|
|
}: {
|
|
icon: LucideIcon
|
|
label: string
|
|
active: boolean
|
|
onClick: () => void
|
|
}) {
|
|
return (
|
|
<Button
|
|
className={cn(
|
|
'flex min-h-8 w-full justify-start gap-2 rounded-lg px-2.5 text-left text-sm transition',
|
|
active ? 'bg-muted text-foreground' : 'text-foreground/80 hover:bg-muted/70'
|
|
)}
|
|
onClick={onClick}
|
|
size="sm"
|
|
type="button"
|
|
variant="ghost"
|
|
>
|
|
<Icon className="size-4 shrink-0" />
|
|
<span className="min-w-0 flex-1 truncate">{label}</span>
|
|
</Button>
|
|
)
|
|
}
|
|
|
|
export function ListRow({
|
|
title,
|
|
description,
|
|
hint,
|
|
action,
|
|
below,
|
|
wide = false
|
|
}: {
|
|
title: ReactNode
|
|
description?: ReactNode
|
|
hint?: ReactNode
|
|
action?: ReactNode
|
|
below?: ReactNode
|
|
wide?: boolean
|
|
}) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'grid gap-4 py-3.5 sm:grid-cols-[minmax(0,1fr)_minmax(15rem,22rem)] sm:items-center',
|
|
wide && 'sm:grid-cols-1 sm:items-start'
|
|
)}
|
|
>
|
|
<div className="min-w-0">
|
|
<div className="text-sm font-medium text-foreground">{title}</div>
|
|
{description && <div className="mt-1 text-xs leading-5 text-muted-foreground">{description}</div>}
|
|
{hint && <div className="mt-1 block font-mono text-[0.68rem] text-muted-foreground/45">{hint}</div>}
|
|
{below}
|
|
</div>
|
|
{action && <div className={cn('min-w-0', !wide && 'sm:justify-self-end')}>{action}</div>}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function LoadingState({ label }: { label: string }) {
|
|
return <PageLoader label={label} />
|
|
}
|
|
|
|
export function EmptyState({ title, description }: { title: string; description: string }) {
|
|
return (
|
|
<div className="grid min-h-48 place-items-center text-center">
|
|
<div>
|
|
<div className="text-sm font-medium">{title}</div>
|
|
<div className="mt-1 text-xs text-muted-foreground">{description}</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|