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>
103 lines
2.8 KiB
TypeScript
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>
|
|
);
|
|
}
|