SPB Git forge

spb/countryatlas

Public
20commits 1branches 0releases
268.3 MBsize
maindefault branch
12 days agolast push
TypeScript 57% Python 38.6% JavaScript 3.6% CSS 0.6%
4.0 KB · 101 lines tsx
Raw Blame History
1import type { ReactNode } from 'react';2import { t } from '@/i18n';3import { cn } from '@/lib/cn';45/** Dense, plain admin primitives: heading row, key/value list, compact table, status pill. */6export function AdminSection({ title, sub, actions, children, className }: { title: string; sub?: ReactNode; actions?: ReactNode; children: ReactNode; className?: string }) {7  return (8    <section className={cn('border-t border-rule py-4 first:border-t-0', className)}>9      <div className="mb-2 flex flex-wrap items-end justify-between gap-2">10        <div>11          <h2 className="text-base font-semibold text-ink">{title}</h2>12          {sub ? <p className="text-xs text-ink-3">{sub}</p> : null}13        </div>14        {actions ? <div className="flex flex-wrap items-center gap-2">{actions}</div> : null}15      </div>16      {children}17    </section>18  );19}2021export function Kv({ rows, className }: { rows: Array<[string, ReactNode]>; className?: string }) {22  return (23    <dl className={cn('grid grid-cols-[minmax(0,auto)_minmax(0,1fr)] gap-x-4 gap-y-1 text-xs', className)}>24      {rows.map(([k, v]) => (25        <div key={k} className="contents">26          <dt className="text-ink-3">{k}</dt>27          <dd className="tnum break-all text-ink">{v ?? t('admin.na')}</dd>28        </div>29      ))}30    </dl>31  );32}3334export interface Col<T> {35  key: string;36  header: ReactNode;37  cell: (row: T) => ReactNode;38  numeric?: boolean;39  className?: string;40}4142/** Compact table with horizontal scroll on narrow screens (admin only — dense by design). */43export function AdminTable<T>({ rows, columns, rowKey, className, empty }: { rows: T[]; columns: Col<T>[]; rowKey: (r: T, i: number) => string; className?: string; empty?: ReactNode }) {44  if (rows.length === 0) return <p className="py-3 text-xs text-ink-3">{empty ?? t('common.noDataLong')}</p>;45  return (46    <div className={cn('-mx-4 overflow-x-auto px-4 sm:mx-0 sm:px-0', className)}>47      <table className="w-full min-w-[640px] border-collapse text-xs">48        <thead>49          <tr className="border-b border-rule text-left text-2xs uppercase tracking-wide text-ink-3">50            {columns.map((c) => (51              <th key={c.key} scope="col" className={cn('py-1.5 pr-3 font-medium', c.numeric && 'text-right', c.className)}>52                {c.header}53              </th>54            ))}55          </tr>56        </thead>57        <tbody className="divide-y divide-rule">58          {rows.map((r, i) => (59            <tr key={rowKey(r, i)} className="hover:bg-surface-2/60">60              {columns.map((c) => (61                <td key={c.key} className={cn('py-1.5 pr-3 align-top', c.numeric && 'tnum text-right', c.className)}>62                  {c.cell(r)}63                </td>64              ))}65            </tr>66          ))}67        </tbody>68      </table>69    </div>70  );71}7273export function Pill({ tone, children }: { tone: 'ok' | 'warn' | 'bad' | 'muted'; children: ReactNode }) {74  return (75    <span className={cn('inline-flex items-center rounded-sm border px-1.5 py-px text-2xs font-medium', tone === 'ok' && 'border-up/40 text-up', tone === 'warn' && 'border-warn/40 text-warn', tone === 'bad' && 'border-down/40 text-down', tone === 'muted' && 'border-rule text-ink-3')}>76      {children}77    </span>78  );79}8081export function statusTone(s: string | null | undefined): 'ok' | 'warn' | 'bad' | 'muted' {82  const v = (s ?? '').toLowerCase();83  if (v === 'ok' || v === 'verified') return 'ok';84  if (v === 'partial' || v === 'warning' || v === 'stale') return 'warn';85  if (v === 'failed' || v === 'quarantined' || v === 'error') return 'bad';86  return 'muted';87}8889export function bytes(n: number | null | undefined): string {90  if (n == null) return t('admin.na');91  if (n >= 1e9) return `${(n / 1e9).toFixed(2)} GB`;92  if (n >= 1e6) return `${(n / 1e6).toFixed(1)} MB`;93  if (n >= 1e3) return `${(n / 1e3).toFixed(0)} kB`;94  return `${n} B`;95}9697export function ts(iso: string | null | undefined): string {98  if (!iso) return t('admin.na');99  return iso.replace('T', ' ').replace(/\.\d+/, '').replace(/\+00:00$/, 'Z');100}101