content-explorer/ui/components/ResultsTable.tsx
tegwick a6e360aaa5 Port the explorer shell: tokens, header, hero, and a generic table.
CONTENT-WP-0002-T01 adapts the pinned OnCo visual system under ui/ without the corpus, mark, or kind routes.

Assistant: grok
Assistant-Session: 01a1346c-fdff-7741-b1af-b7283661554b
2026-10-13 22:51:00 +02:00

100 lines
2.6 KiB
TypeScript

import type { ReactNode } from "react";
import type { SortState } from "@/lib/types";
/*
* Reduced from OnCo src/components/filters/ResultsTable.tsx
* https://github.com/judegomila/OnCo @ b594d7433bf4e408b44215e25e5304696d6f840e
* Copyright (c) 2026 OnCo. MIT License. See ui/NOTICE.
*
* The parent owns sorting. This table does not filter, resize, translate, or page.
*/
export type Column<T> = {
key: string;
label: string;
render: (row: T) => ReactNode;
sortable?: boolean;
};
export function ResultsTable<T>({
columns,
rows,
rowKey,
sort,
onSort,
empty = "No records.",
}: {
columns: Column<T>[];
rows: readonly T[];
rowKey: (row: T) => string;
sort?: SortState;
onSort?: (key: string) => void;
empty?: string;
}) {
return (
<div className="card results-table">
<div className="overflow-x-auto lg:overflow-x-visible">
<table className="records">
<thead>
<tr>
{columns.map((column) => {
const sorted = sort?.key === column.key;
const direction = sort?.dir === -1 ? "descending" : "ascending";
return (
<th
key={column.key}
scope="col"
aria-sort={sorted ? direction : undefined}
>
<ColumnHead column={column} sort={sort} onSort={onSort} />
</th>
);
})}
</tr>
</thead>
<tbody>
{rows.length === 0 ? (
<tr>
<td colSpan={columns.length}>{empty}</td>
</tr>
) : (
rows.map((row) => (
<tr key={rowKey(row)}>
{columns.map((column) => (
<td key={column.key}>{column.render(row)}</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
</div>
);
}
function ColumnHead<T>({
column,
sort,
onSort,
}: {
column: Column<T>;
sort?: SortState;
onSort?: (key: string) => void;
}) {
if (!column.sortable || !onSort) return <>{column.label}</>;
const sorted = sort?.key === column.key;
const title = sorted
? sort?.dir === -1
? `Sorted descending by ${column.label}`
: `Sorted ascending by ${column.label}`
: `Sort by ${column.label}`;
return (
<button type="button" onClick={() => onSort(column.key)} title={title}>
{column.label}
<span aria-hidden="true" className={sorted ? "text-accent" : "text-muted/40"}>
{sorted ? (sort?.dir === -1 ? " ↓" : " ↑") : " ↕"}
</span>
</button>
);
}