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}
))}
);
}