123 lines
4.8 KiB
TypeScript
123 lines
4.8 KiB
TypeScript
import clsx from "clsx";
|
|
import type { HTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from "react";
|
|
|
|
/*
|
|
* One table treatment for the whole console.
|
|
*
|
|
* There were four: billing, licences, accounts and catalogue each wrote their
|
|
* own thead, and they disagreed about the head's type size, its tracking,
|
|
* whether it sat on --panel-2, and whether numbers were tabular. Catalogue's
|
|
* heads were sentence-case body text. A registry whose columns are set four
|
|
* ways does not read as one product.
|
|
*
|
|
* The head is the keyed idiom — mono, small, uppercase, widely tracked — which
|
|
* is what a column head is: a key above a value, exactly as the record line is
|
|
* a key beside one.
|
|
*
|
|
* MOBILE. `stack` collapses the table into one card per row below sm, each cell
|
|
* becoming a label/value pair drawn from TD's `label`. The variants below hang
|
|
* off a `stacked` class on the <table>, so a table that does not opt in is
|
|
* untouched at every width.
|
|
*
|
|
* It is opt-in rather than automatic because a stacked row whose cells have no
|
|
* labels is worse than a scrolling one — the values lose the only thing naming
|
|
* them. Customer screens stack; the staff console's wide registry tables scroll
|
|
* sideways instead, which is the right trade for eight columns read at a desk.
|
|
*/
|
|
|
|
const STACK = "max-sm:[.stacked_&]:block";
|
|
|
|
export function Table({ stack, className, children, ...props }: HTMLAttributes<HTMLTableElement> & { stack?: boolean }) {
|
|
return (
|
|
<div className="overflow-x-auto">
|
|
<table className={clsx("w-full border-collapse text-left text-[0.86rem]", stack && "stacked max-sm:block", className)} {...props}>
|
|
{children}
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function THead({ className, children, ...props }: HTMLAttributes<HTMLTableSectionElement>) {
|
|
return (
|
|
<thead className={clsx("border-b border-rule", "max-sm:[.stacked_&]:hidden", className)} {...props}>
|
|
{children}
|
|
</thead>
|
|
);
|
|
}
|
|
|
|
export function TBody({ className, children, ...props }: HTMLAttributes<HTMLTableSectionElement>) {
|
|
return (
|
|
<tbody className={clsx(STACK, "max-sm:[.stacked_&]:space-y-3 max-sm:[.stacked_&]:p-3", className)} {...props}>
|
|
{children}
|
|
</tbody>
|
|
);
|
|
}
|
|
|
|
export function TR({ className, children, ...props }: HTMLAttributes<HTMLTableRowElement>) {
|
|
return (
|
|
<tr
|
|
className={clsx(
|
|
"border-b border-rule-soft last:border-0 hover:bg-panel-2",
|
|
STACK,
|
|
// Plain bg-panel-2, not an opacity modifier: this app's tokens
|
|
// are whole colours rather than RGB channels, so `/40` has
|
|
// nothing to drop an alpha into. web/ stores channels precisely
|
|
// because it leans on those modifiers; this one must not.
|
|
"max-sm:[.stacked_&]:rounded max-sm:[.stacked_&]:border max-sm:[.stacked_&]:border-rule max-sm:[.stacked_&]:bg-panel-2 max-sm:[.stacked_&]:p-3",
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</tr>
|
|
);
|
|
}
|
|
|
|
interface CellProps {
|
|
/** Right-aligns the cell. For quantities and money, which read down the column. */
|
|
numeric?: boolean;
|
|
}
|
|
|
|
export function TH({ className, numeric, children, ...props }: ThHTMLAttributes<HTMLTableCellElement> & CellProps) {
|
|
return (
|
|
<th
|
|
className={clsx(
|
|
"whitespace-nowrap px-4 py-2.5 font-mono text-[0.62rem] font-normal uppercase tracking-[0.13em] text-ink-3",
|
|
numeric && "text-right",
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</th>
|
|
);
|
|
}
|
|
|
|
export function TD({ className, numeric, label, children, ...props }: TdHTMLAttributes<HTMLTableCellElement> & CellProps & { label?: string }) {
|
|
return (
|
|
<td
|
|
className={clsx(
|
|
"px-4 py-3 align-middle",
|
|
numeric && "text-right tabular-nums",
|
|
// Stacked, a cell is a label above its value and the right
|
|
// alignment that made a money column read down the page is
|
|
// meaningless, so it is dropped.
|
|
STACK,
|
|
"max-sm:[.stacked_&]:px-0 max-sm:[.stacked_&]:py-1 max-sm:[.stacked_&]:text-left",
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{label && (
|
|
<span className="mb-0.5 hidden font-mono text-[0.6rem] uppercase tracking-[0.13em] text-ink-3 max-sm:[.stacked_&]:block">{label}</span>
|
|
)}
|
|
{children}
|
|
</td>
|
|
);
|
|
}
|
|
|
|
/** The secondary line under a cell's main value — an ID, a deployment, a date. */
|
|
export function Sub({ children }: { children: React.ReactNode }) {
|
|
return <div className="text-[0.78rem] text-ink-3">{children}</div>;
|
|
}
|