Repository Public
1import * as React from 'react';2import { AlertTriangle } from 'lucide-react';3import { cn } from '@/lib/cn';4import { errorMessage } from '@/lib/api';56export function Kbd({ children, className }: { children: React.ReactNode; className?: string }) {7 return <kbd className={cn('inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-[4px] border border-line-2 bg-surface-2 px-1 font-sans text-[10.5px] font-medium text-text-2', className)}>{children}</kbd>;8}9export function Skeleton({ className }: { className?: string }) {10 return <div aria-hidden className={cn('shimmer rounded-sm bg-surface-2', className)} />;11}12export function Spinner({ className }: { className?: string }) {13 return <span role="status" aria-label="Loading" className={cn('inline-block size-4 animate-spin rounded-full border-[1.5px] border-text-2 border-t-transparent', className)} />;14}15export function EmptyState({ icon, illustration, title, description, action, className }: { icon?: React.ReactNode; illustration?: React.ReactNode; title: string; description?: React.ReactNode; action?: React.ReactNode; className?: string }) {16 return (17 <div className={cn('flex flex-col items-center justify-center gap-2 px-6 py-12 text-center', className)}>18 {illustration ? <div className="mb-1">{illustration}</div> : icon && <div className="mb-1 flex size-11 items-center justify-center rounded-full bg-surface-2 text-muted [&>svg]:size-5">{icon}</div>}19 <p className="text-[15px] font-semibold tracking-tight text-text">{title}</p>20 {description && <p className="max-w-sm text-[13px] leading-relaxed text-text-2">{description}</p>}21 {action && <div className="mt-3">{action}</div>}22 </div>23 );24}25export function ErrorState({ error, onRetry, className }: { error: unknown; onRetry?: () => void; className?: string }) {26 return (27 <div role="alert" className={cn('flex flex-col items-center gap-2 px-6 py-10 text-center', className)}>28 <AlertTriangle className="size-5 text-error" />29 <p className="text-sm font-medium">Could not load</p>30 <p className="max-w-sm text-xs text-text-2">{errorMessage(error)}</p>31 {onRetry && (32 <button onClick={onRetry} className="mt-2 h-8 rounded-sm border border-line-2 px-3 text-xs text-text-2 hover:border-line-3 hover:text-text pointer-coarse:h-11">33 Retry34 </button>35 )}36 </div>37 );38}39export function PageHeader({ title, description, actions, meta }: { title: string; description?: React.ReactNode; actions?: React.ReactNode; meta?: React.ReactNode }) {40 return (41 <div className="flex flex-wrap items-start gap-x-4 gap-y-2 border-b border-line px-4 py-3 md:px-6 md:py-4">42 <div className="min-w-0 flex-1">43 {/* phones already show the title in the top bar */}44 <h1 className="hidden text-[15px] font-semibold tracking-tight text-text md:block">{title}</h1>45 {description && <p className="text-xs text-text-2 md:mt-0.5">{description}</p>}46 {meta && <div className="mt-1 text-[11px] text-muted">{meta}</div>}47 </div>48 {actions && <div className="flex flex-wrap items-center gap-2">{actions}</div>}49 </div>50 );51}52export function Section({ title, aside, children, className }: { title: string; aside?: React.ReactNode; children: React.ReactNode; className?: string }) {53 return (54 <section className={cn('min-w-0 overflow-hidden card', className)}>55 <header className="flex min-h-10 items-center gap-3 border-b border-line px-4 py-2">56 <h2 className="flex-1 text-xs font-semibold uppercase tracking-wide text-text-2">{title}</h2>57 {aside && <div className="text-[11px] text-muted">{aside}</div>}58 </header>59 {children}60 </section>61 );62}63