import { useState, type ReactNode } from 'react'; import { ChevronDown, MoreHorizontal } from 'lucide-react'; import type { PillTone } from './sentry-utils'; export type { PillTone }; /* ============================================================ Sentry kit — React bindings for the 23-sentry.html language. Every page composes these; no page invents its own chrome. ============================================================ */ export function Ibox({ children }: { children: ReactNode }) { return {children}; } /* ---------- buttons ---------- */ type BtnProps = { children: ReactNode; onClick?: () => void; disabled?: boolean; title?: string; className?: string; type?: 'button' | 'submit'; }; export function GhostBtn({ children, onClick, disabled, title, className = '', type = 'button' }: BtnProps) { return ( ); } export function QuietBtn({ children, onClick, disabled, title, className = '', type = 'button' }: BtnProps) { return ( ); } export function IconBtn({ children, onClick, disabled, title, className = '', type = 'button' }: BtnProps) { return ( ); } /* Filter/sort chip with trailing chevron — the `.sel` in sentry. */ export function SelectChip({ children, onClick, title, icon }: BtnProps & { icon?: ReactNode }) { return ( ); } /* Segmented icon buttons — chart-type switchers. */ export function Seg({ options, value, onChange }: { options: { key: string; icon: ReactNode; title?: string }[]; value: string; onChange: (key: string) => void; }) { return ( {options.map((o) => ( ))} ); } /* ---------- stat card ---------- */ export function SStat({ icon, label, value, unit, foot, delta, extra, onMenu }: { icon: ReactNode; label: string; value: ReactNode; unit?: string; foot: ReactNode; delta?: { dir: 'up' | 'down'; text: string }; extra?: ReactNode; onMenu?: () => void; }) { return (
{icon} {label}
{value} {unit && {unit}}
{extra &&
{extra}
}
{foot} {delta && {delta.text}}
); } export function SDelta({ dir, children }: { dir: 'up' | 'down'; children: ReactNode }) { return ( {dir === 'up' ? '▲' : '▼'}{children} ); } /* ---------- card ---------- */ export function SCard({ icon, title, accent, trail, children, pad = true, className = '' }: { icon?: ReactNode; title?: ReactNode; accent?: ReactNode; trail?: ReactNode; children: ReactNode; pad?: boolean; className?: string; }) { return (
{(title !== undefined || icon) && (
{icon && {icon}} {title}{accent && <> {accent}} {trail && {trail}}
)}
{children}
); } /* ---------- ticks / bars ---------- */ export function Ticks({ pct, count = 56, warn = false }: { pct: number; count?: number; warn?: boolean }) { const on = Math.round((Math.max(0, Math.min(100, pct)) / 100) * count); return ( {Array.from({ length: count }, (_, i) => ( ))} ); } /* Ascending mini bar glyph used inside table metric cells. */ export function MiniBars({ tone = 'var(--accent-primary)' }: { tone?: string }) { return ( {[9, 5, 12, 7].map((h, i) => ( ))} ); } /* ---------- status pill ---------- */ const PILL_CLS: Record = { ok: 'text-[var(--success)] bg-[var(--success-soft)] border-[color-mix(in_srgb,var(--success)_16%,transparent)]', warn: 'text-[var(--warning)] bg-[var(--warning-soft)] border-[color-mix(in_srgb,var(--warning)_16%,transparent)]', err: 'text-[var(--error)] bg-[var(--error-soft)] border-[color-mix(in_srgb,var(--error)_16%,transparent)]', info: 'text-[var(--info)] bg-[var(--info-soft)] border-[color-mix(in_srgb,var(--info)_16%,transparent)]', off: 'text-[var(--text-tertiary)] bg-[var(--tint-06)] border-[var(--border-subtle)]', }; export function SPill({ tone, children }: { tone: PillTone; children: ReactNode }) { return ( {children} ); } /* ---------- inset callout ---------- */ export function SAnomaly({ icon, title, items }: { icon?: ReactNode; title: ReactNode; items: ReactNode[] }) { return (
{icon} {title}
{items.map((it, i) => (
{it}
))}
); } /* ---------- data table (inset header bar, sortable) ---------- */ export type SCol = { key: string; label: ReactNode; width?: string; sortable?: boolean; sortValue?: (row: T) => string | number; render: (row: T) => ReactNode; }; const CARET = ( ); export function STable({ cols, rows, rowKey, selectable, onRowClick }: { cols: SCol[]; rows: T[]; rowKey?: (row: T, i: number) => string | number; selectable?: boolean; onRowClick?: (row: T) => void; }) { const [sortKey, setSortKey] = useState(null); const [sortDir, setSortDir] = useState<1 | -1>(1); const [checked, setChecked] = useState>(new Set()); const allCols: SCol[] = selectable ? [{ key: '$sel', label: '', width: '30px', render: () => null }, ...cols] : cols; const template = allCols.map((c) => c.width ?? '1fr').join(' '); const sorted = [...rows]; const sc = cols.find((c) => c.key === sortKey); if (sc?.sortValue) { sorted.sort((a, b) => { const x = sc.sortValue!(a); const y = sc.sortValue!(b); return (x > y ? 1 : x < y ? -1 : 0) * sortDir; }); } const toggleAll = () => setChecked(checked.size === rows.length ? new Set() : new Set(rows.map((_, i) => i))); return (
{allCols.map((c) => c.key === '$sel' ? ( 0 ? 'on' : ''}`} onClick={toggleAll} /> ) : ( { if (c.sortable === false) return; setSortDir(sortKey === c.key ? (sortDir === 1 ? -1 : 1) : 1); setSortKey(c.key); }} > {c.label}{c.sortable !== false && CARET} ), )}
{sorted.map((row, i) => { const key = rowKey ? rowKey(row, i) : row.id ?? i; const ri = rows.indexOf(row); return (
onRowClick?.(row)} > {allCols.map((c) => c.key === '$sel' ? ( { e.stopPropagation(); setChecked((prev) => { const n = new Set(prev); if (n.has(ri)) n.delete(ri); else n.add(ri); return n; }); }} /> ) : ( {c.render(row)} ), )}
); })}
); } /* ---------- page header (Welcome back … + actions) ---------- */ export function SPageHead({ title, titleAccent, sub, subAccent, actions }: { title: ReactNode; titleAccent?: ReactNode; sub?: ReactNode; subAccent?: ReactNode; actions?: ReactNode; }) { return (

{title}{titleAccent && <> {titleAccent}}

{sub && (
{sub}{subAccent && <> {subAccent}}
)}
{actions &&
{actions}
}
); } /* Section label — small caps with accent square. */ export function SSection({ children, right }: { children: ReactNode; right?: ReactNode }) { return (
{children}
{right && {right}}
); } /* Legend row for charts. */ export function SLegend({ items }: { items: { color: string; label: string }[] }) { return (
{items.map((it) => ( {it.label} ))}
); }