All checks were successful
React + Vite + TypeScript SPA: scaffold, operations-terminal design system, Bearer-token auth, and the Machines view. - Design system: OKLCH-tinted dark theme (ink-slate + signal-cyan), Hanken Grotesk + JetBrains Mono, status-color language (online/offline/granted/pending/denied/not_required), motion with prefers-reduced-motion honored. - Auth: token in sessionStorage via ref (never React state), protected routes, 401 session teardown, admin-gated per-agent-key UI. - Machines view: data table (sticky header, keyboard-activated rows, skeleton loading, actionable empty/error states), non-blocking detail drawer, delete confirm, admin key management with copy-once reveal. - UI primitives: Modal (focus trap + inert + portal + dialogStack), Drawer, Table, Badge/StatusDot, toast, states. - Typed API client normalizing the two error-envelope shapes. Passed Code Review (no blockers), impeccable critique-and-polish, and local gates (tsc/lint/build green). Dev-only Vite proxy to :3002. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
96 lines
2.9 KiB
TypeScript
96 lines
2.9 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import "./table.css";
|
|
|
|
export interface Column<T> {
|
|
/** Unique column key. */
|
|
key: string;
|
|
/** Header label. Omit for the status / actions rails. */
|
|
header?: ReactNode;
|
|
/** Cell renderer. */
|
|
render: (row: T) => ReactNode;
|
|
/** Extra class on the <td> (e.g. dt__status, dt__actions). */
|
|
cellClass?: string;
|
|
}
|
|
|
|
interface TableProps<T> {
|
|
columns: Column<T>[];
|
|
rows: T[];
|
|
rowKey: (row: T) => string;
|
|
/** Optional per-row activation (opens detail). Bound to click, Enter, Space. */
|
|
onRowClick?: (row: T) => void;
|
|
/** Accessible label for the row's primary activation, e.g. the hostname. */
|
|
rowLabel?: (row: T) => string;
|
|
/** Cap the staggered fade-in so large lists don't crawl in. */
|
|
maxStaggerRows?: number;
|
|
}
|
|
|
|
/**
|
|
* Dense, console-style data table. Sticky header, hover highlight, hover-
|
|
* revealed row actions, and a staggered fade-in on mount (capped so big lists
|
|
* appear promptly). Column-driven so callers compose cells declaratively.
|
|
*/
|
|
export function Table<T>({
|
|
columns,
|
|
rows,
|
|
rowKey,
|
|
onRowClick,
|
|
rowLabel,
|
|
maxStaggerRows = 14,
|
|
}: TableProps<T>) {
|
|
return (
|
|
<div className="dt-wrap">
|
|
<table className="dt">
|
|
<thead>
|
|
<tr>
|
|
{columns.map((c) => (
|
|
<th key={c.key} className={c.cellClass}>
|
|
{c.header}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{rows.map((row, i) => {
|
|
const delay = i < maxStaggerRows ? `${i * 22}ms` : "0ms";
|
|
return (
|
|
<tr
|
|
key={rowKey(row)}
|
|
style={{
|
|
animationDelay: delay,
|
|
cursor: onRowClick ? "pointer" : undefined,
|
|
}}
|
|
onClick={onRowClick ? () => onRowClick(row) : undefined}
|
|
tabIndex={onRowClick ? 0 : undefined}
|
|
aria-label={
|
|
onRowClick && rowLabel
|
|
? `Open detail for ${rowLabel(row)}`
|
|
: undefined
|
|
}
|
|
onKeyDown={
|
|
onRowClick
|
|
? (e) => {
|
|
// Activate on Enter or Space, the standard for a
|
|
// button-like row. Space must not scroll the page.
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
if (e.target !== e.currentTarget) return;
|
|
e.preventDefault();
|
|
onRowClick(row);
|
|
}
|
|
}
|
|
: undefined
|
|
}
|
|
>
|
|
{columns.map((c) => (
|
|
<td key={c.key} className={c.cellClass}>
|
|
{c.render(row)}
|
|
</td>
|
|
))}
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|