Files
stack/packages/ui/src/components/DataTable.tsx
Jason Woltje 9b0445cd5b
All checks were successful
ci/woodpecker/push/orchestrator Pipeline was successful
ci/woodpecker/push/api Pipeline was successful
ci/woodpecker/push/web Pipeline was successful
feat(ui,web): add shared components and terminal panel (MS15-UI-003, UI-004, UI-005)
UI-003: MetricsStrip (6-cell KPI grid), ProgressBar (4 variants with
ARIA), FilterTabs (segmented control).

UI-004: SectionHeader (title/subtitle/actions), DataTable (generic
typed table with hover), LogLine (colored log entry grid).

UI-005: TerminalPanel bottom-drawer component with tabs, line coloring,
blinking cursor, slide animation.

All components use CSS custom properties for theming.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 15:08:19 -06:00

103 lines
2.8 KiB
TypeScript

import { useState, type ReactElement, type ReactNode } from "react";
export interface DataTableColumn<T> {
key: string;
header: string;
render?: (row: T) => ReactNode;
className?: string;
}
export interface DataTableProps<T extends Record<string, unknown>> {
columns: DataTableColumn<T>[];
data: T[];
className?: string;
onRowClick?: (row: T) => void;
}
export function DataTable<T extends Record<string, unknown>>({
columns,
data,
className = "",
onRowClick,
}: DataTableProps<T>): ReactElement {
const [hoveredRow, setHoveredRow] = useState<number | null>(null);
return (
<table
className={className}
style={{
width: "100%",
borderCollapse: "collapse",
fontSize: "0.83rem",
}}
>
<thead>
<tr>
{columns.map((col) => (
<th
key={col.key}
className={col.className}
style={{
textAlign: "left",
padding: "8px 12px",
fontSize: "0.7rem",
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.07em",
color: "var(--muted)",
borderBottom: "1px solid var(--border)",
background: "var(--surface)",
}}
>
{col.header}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, rowIndex) => {
const isLast = rowIndex === data.length - 1;
const isHovered = hoveredRow === rowIndex;
return (
<tr
key={rowIndex}
style={{
background: isHovered ? "var(--surface)" : undefined,
cursor: onRowClick !== undefined ? "pointer" : undefined,
}}
onClick={
onRowClick !== undefined
? (): void => {
onRowClick(row);
}
: undefined
}
onMouseEnter={(): void => {
setHoveredRow(rowIndex);
}}
onMouseLeave={(): void => {
setHoveredRow(null);
}}
>
{columns.map((col) => (
<td
key={col.key}
className={col.className}
style={{
padding: "10px 12px",
borderBottom: isLast ? undefined : "1px solid var(--border)",
color: isHovered ? "var(--text)" : "var(--text-2)",
}}
>
{col.render !== undefined ? col.render(row) : (row[col.key] as ReactNode)}
</td>
))}
</tr>
);
})}
</tbody>
</table>
);
}