HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import Link from 'next/link';2import type { ReactNode } from 'react';3import { cn } from '@/lib/cn';4import { DASH } from '@/lib/format';56/* Dense admin primitives on the shared tokens. No marketing, no cards. */78const chip = 'inline-flex items-center whitespace-nowrap rounded-[3px] px-1.5 py-[1px] text-[11px] font-medium leading-4 tracking-wide';910const HEALTH: Record<string, string> = {11 ok: 'text-positive bg-positive-soft',12 success: 'text-positive bg-positive-soft',13 done: 'text-positive bg-positive-soft',14 approved: 'text-positive bg-positive-soft',15 extracted: 'text-positive bg-positive-soft',16 active: 'text-positive bg-positive-soft',17 degraded: 'text-warning bg-warning-soft',18 partial: 'text-warning bg-warning-soft',19 skipped: 'text-warning bg-warning-soft',20 queued: 'text-accent bg-accent-soft',21 running: 'text-accent bg-accent-soft',22 pending: 'text-accent bg-accent-soft',23 failing: 'text-danger bg-danger-soft',24 failed: 'text-danger bg-danger-soft',25 error: 'text-danger bg-danger-soft',26 dead: 'text-danger bg-danger-soft',27 rejected: 'text-danger bg-danger-soft',28 schema_error: 'text-danger bg-danger-soft',29 blocked: 'text-danger bg-danger-soft',30 disabled: 'text-ink-3 bg-surface-2',31 unknown: 'text-ink-3 bg-surface-2',32};3334/** Health / status chip: ok → positive · degraded → warning · failing → danger · disabled/unknown → muted. */35export function StatusChip({ value, className }: { value: string | null | undefined; className?: string }) {36 if (!value) return <span className="text-ink-3">{DASH}</span>;37 return <span className={cn(chip, 'mono', HEALTH[value] ?? 'text-ink-2 bg-surface-2', className)}>{value}</span>;38}3940export function KindChip({ value, className }: { value: string; className?: string }) {41 return <span className={cn(chip, 'mono bg-surface-2 text-ink-2', className)}>{value}</span>;42}4344export function Bool({ v }: { v: boolean | null | undefined }) {45 if (v === null || v === undefined) return <span className="text-ink-3">{DASH}</span>;46 return <span className={v ? 'text-positive' : 'text-ink-3'}>{v ? 'yes' : 'no'}</span>;47}4849/** Inline notice from `?notice=&level=` after a server action. */50export function Notice({ notice, level }: { notice?: string; level?: string }) {51 if (!notice) return null;52 const err = level === 'error';53 return (54 <p role="status" className={cn('mb-4 border-l-2 px-3 py-2 text-sm', err ? 'border-danger bg-danger-soft text-danger' : 'border-positive bg-positive-soft text-ink')}>55 {notice}56 </p>57 );58}5960/** Pretty JSON in a scrollable block; never dumps undefined/null as text. */61export function JsonPre({ value, className, maxHeight = '24rem' }: { value: unknown; className?: string; maxHeight?: string }) {62 if (value === null || value === undefined) return <p className="text-xs text-ink-3">none</p>;63 const text = typeof value === 'string' ? value : JSON.stringify(value, null, 2);64 return (65 <pre className={cn('scrollbar-thin overflow-auto border border-rule bg-surface p-3 text-[12px] leading-relaxed text-ink', className)} style={{ maxHeight }}>66 <code>{text}</code>67 </pre>68 );69}7071/** Small action button used inside server-action forms. */72export function ActionButton({ children, tone = 'neutral', className, disabled, title }: { children: ReactNode; tone?: 'neutral' | 'accent' | 'danger' | 'positive'; className?: string; disabled?: boolean; title?: string }) {73 return (74 <button75 type="submit"76 disabled={disabled}77 title={title}78 className={cn(79 'inline-flex h-8 items-center justify-center whitespace-nowrap border px-2.5 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40',80 tone === 'neutral' && 'border-rule text-ink-2 hover:border-rule-strong hover:text-ink',81 tone === 'accent' && 'border-accent text-accent hover:bg-accent-soft',82 tone === 'positive' && 'border-positive text-positive hover:bg-positive-soft',83 tone === 'danger' && 'border-danger text-danger hover:bg-danger-soft',84 className,85 )}86 >87 {children}88 </button>89 );90}9192/** Title line for an admin section. */93export function AdminTitle({ title, count, children, lede }: { title: string; count?: ReactNode; children?: ReactNode; lede?: ReactNode }) {94 return (95 <div className="mb-4 flex flex-col gap-3 md:flex-row md:items-end md:justify-between">96 <div className="min-w-0">97 <h1 className="text-xl font-semibold tracking-tight">98 {title} {count !== undefined && <span className="tnum text-base font-normal text-ink-3">{count}</span>}99 </h1>100 {lede && <p className="mt-1 text-xs text-ink-3">{lede}</p>}101 </div>102 {children && <div className="flex flex-wrap items-center gap-2">{children}</div>}103 </div>104 );105}106107/** Compact GET filter form (selects/text) for admin listings. */108export function AdminFilters({ action, fields, className }: { action: string; fields: ({ kind: 'select'; name: string; label: string; value?: string; options: { value: string; label: string }[]; any?: string } | { kind: 'text'; name: string; label: string; value?: string; placeholder?: string } | { kind: 'hidden'; name: string; value: string })[]; className?: string }) {109 const cls = 'h-9 w-full border border-rule bg-surface px-2 text-xs text-ink focus:border-accent focus:outline-none';110 return (111 <form action={action} method="get" className={cn('grid grid-cols-2 gap-2 sm:grid-cols-4 lg:grid-cols-6', className)}>112 {fields.map((f) =>113 f.kind === 'hidden' ? (114 <input key={f.name} type="hidden" name={f.name} value={f.value} />115 ) : (116 <label key={f.name} className="block min-w-0">117 <span className="eyebrow block pb-1">{f.label}</span>118 {f.kind === 'select' ? (119 <select name={f.name} defaultValue={f.value ?? ''} className={cls}>120 <option value="">{f.any ?? 'Any'}</option>121 {f.options.map((o) => (122 <option key={o.value} value={o.value}>123 {o.label}124 </option>125 ))}126 </select>127 ) : (128 <input name={f.name} defaultValue={f.value ?? ''} placeholder={f.placeholder} className={cls} />129 )}130 </label>131 ),132 )}133 <div className="flex items-end gap-2">134 <button type="submit" className="h-9 flex-1 bg-ink px-3 text-xs font-medium text-canvas hover:opacity-90">135 Apply136 </button>137 <Link href={action} className="inline-flex h-9 items-center border border-rule px-3 text-xs text-ink-2 hover:text-ink">138 Reset139 </Link>140 </div>141 </form>142 );143}144145export function Mono({ children, className, title }: { children: ReactNode; className?: string; title?: string }) {146 return (147 <span className={cn('mono text-[11.5px] text-ink-2', className)} title={title}>148 {children}149 </span>150 );151}152153/** Truncate long text with the full text as title. */154export function Trunc({ text, max = 80, className }: { text: string | null | undefined; max?: number; className?: string }) {155 if (!text) return <span className="text-ink-3">{DASH}</span>;156 return (157 <span className={className} title={text.length > max ? text : undefined}>158 {text.length > max ? `${text.slice(0, max)}…` : text}159 </span>160 );161}162