140 lines
3.2 KiB
TypeScript
140 lines
3.2 KiB
TypeScript
import { useId } from "react";
|
|
|
|
import {
|
|
AuthRequiredSurface,
|
|
ForbiddenSurface,
|
|
NotFoundSurface,
|
|
} from "../../components/state-surfaces.jsx";
|
|
import { Button } from "../primitives/core.js";
|
|
import { Pagination } from "../primitives/navigation.js";
|
|
|
|
export type DataTableColumn<Row> = Readonly<{
|
|
id: string;
|
|
header: string;
|
|
cell(row: Row): React.ReactNode;
|
|
}>;
|
|
|
|
export function DataTable<Row>({
|
|
caption,
|
|
columns,
|
|
rows,
|
|
rowKey,
|
|
empty,
|
|
}: Readonly<{
|
|
caption: string;
|
|
columns: readonly DataTableColumn<Row>[];
|
|
rows: readonly Row[];
|
|
rowKey(row: Row): string;
|
|
empty: React.ReactNode;
|
|
}>) {
|
|
if (rows.length === 0) return <>{empty}</>;
|
|
return (
|
|
<div className="ui-data-table__scroll" tabIndex={0}>
|
|
<table className="ui-data-table">
|
|
<caption>{caption}</caption>
|
|
<thead>
|
|
<tr>
|
|
{columns.map((column) => (
|
|
<th key={column.id} scope="col">
|
|
{column.header}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{rows.map((row) => (
|
|
<tr key={rowKey(row)}>
|
|
{columns.map((column) => (
|
|
<td key={column.id}>{column.cell(row)}</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function SearchFilterToolbar({
|
|
label,
|
|
search,
|
|
filters,
|
|
resetLabel,
|
|
onReset,
|
|
resultCount,
|
|
}: Readonly<{
|
|
label: string;
|
|
search: React.ReactNode;
|
|
filters?: React.ReactNode;
|
|
resetLabel: string;
|
|
onReset(): void;
|
|
resultCount: React.ReactNode;
|
|
}>) {
|
|
return (
|
|
<section aria-label={label} className="ui-search-filter-toolbar">
|
|
<div>{search}</div>
|
|
{filters ? <div>{filters}</div> : null}
|
|
<Button onClick={onReset} variant="ghost">
|
|
{resetLabel}
|
|
</Button>
|
|
<output>{resultCount}</output>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export function PaginationBar({
|
|
range,
|
|
...pagination
|
|
}: React.ComponentProps<typeof Pagination> & Readonly<{ range: string }>) {
|
|
return (
|
|
<div className="ui-pagination-bar">
|
|
<p>{range}</p>
|
|
<Pagination {...pagination} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export type DisclosureDefinition = Readonly<{
|
|
id: string;
|
|
title: string;
|
|
content: React.ReactNode;
|
|
}>;
|
|
|
|
export function DisclosureGroup({
|
|
label,
|
|
items,
|
|
}: Readonly<{
|
|
label: string;
|
|
items: readonly DisclosureDefinition[];
|
|
}>) {
|
|
const groupId = useId();
|
|
return (
|
|
<section aria-labelledby={groupId} className="ui-disclosure-group">
|
|
<h2 className="visually-hidden" id={groupId}>
|
|
{label}
|
|
</h2>
|
|
{items.map((item) => (
|
|
<details key={item.id}>
|
|
<summary>{item.title}</summary>
|
|
<div>{item.content}</div>
|
|
</details>
|
|
))}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export type AccessSurfaceProps =
|
|
| Readonly<{ kind: "auth-required"; onAction(): void }>
|
|
| Readonly<{ kind: "forbidden"; onAction(): void }>
|
|
| Readonly<{ kind: "not-found"; onAction(): void }>;
|
|
|
|
export function AccessSurface(props: AccessSurfaceProps) {
|
|
if (props.kind === "auth-required") {
|
|
return <AuthRequiredSurface onSignIn={props.onAction} />;
|
|
}
|
|
if (props.kind === "forbidden") {
|
|
return <ForbiddenSurface onNavigate={props.onAction} />;
|
|
}
|
|
return <NotFoundSurface onNavigate={props.onAction} />;
|
|
}
|