feat: Updated marketing site

This commit is contained in:
2026-07-28 12:46:28 +01:00
parent febe48a974
commit f10e61cde4
39 changed files with 599 additions and 1050 deletions
+22 -22
View File
@@ -3,9 +3,9 @@ import { clsx } from "clsx";
type Variant = "success" | "warning" | "danger" | "neutral" | "accent";
interface BadgeProps {
variant?: Variant;
children: React.ReactNode;
className?: string;
variant?: Variant;
children: React.ReactNode;
className?: string;
}
/*
@@ -13,31 +13,31 @@ interface BadgeProps {
* no fill. Every badge in this app holds one short state word, which is what
* the treatment is for.
*
* State is never carried by colour alone the word itself says it, and the
* State is never carried by colour alone the word itself says it, and the
* three state variants add a dot so the distinction survives a monochrome
* screen as well as it survives colour blindness.
*/
const variantClasses: Record<Variant, string> = {
success: "text-success",
warning: "text-warning",
danger: "text-danger",
neutral: "text-text-secondary",
accent: "text-accent",
success: "text-success",
warning: "text-warning",
danger: "text-danger",
neutral: "text-text-secondary",
accent: "text-accent",
};
export function Badge({ variant = "neutral", children, className }: BadgeProps) {
const hasState = variant === "success" || variant === "warning" || variant === "danger";
const hasState = variant === "success" || variant === "warning" || variant === "danger";
return (
<span
className={clsx(
"inline-flex items-center gap-1.5 whitespace-nowrap rounded-sm border border-current px-1.5 py-0.5 font-mono text-[0.65rem] uppercase tracking-[0.08em]",
variantClasses[variant],
className
)}
>
{hasState && <span className="h-1.5 w-1.5 rounded-full bg-current" />}
{children}
</span>
);
return (
<span
className={clsx(
"inline-flex items-center gap-1.5 whitespace-nowrap rounded-sm border border-current px-1.5 py-0.5 font-mono text-[0.65rem] uppercase tracking-[0.08em]",
variantClasses[variant],
className,
)}
>
{hasState && <span className="h-1.5 w-1.5 rounded-full bg-current" />}
{children}
</span>
);
}
+73 -94
View File
@@ -4,7 +4,7 @@ import { HTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from "react";
/*
* Below sm the table stops being a table: the head is hidden, each row becomes
* a bordered card and each cell becomes a label/value pair. That lives here
* rather than in the six pages that render tables two markup trees per page
* rather than in the six pages that render tables two markup trees per page
* would drift apart on the first edit, and every one of those trees would mean
* the same thing.
*
@@ -14,115 +14,94 @@ import { HTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from "react";
*/
export function Table({ className, children, ...props }: HTMLAttributes<HTMLTableElement>) {
return (
<div className="overflow-x-auto">
<table
className={clsx("w-full border-collapse text-sm max-sm:block", className)}
{...props}
>
{children}
</table>
</div>
);
return (
<div className="overflow-x-auto">
<table className={clsx("w-full border-collapse text-sm max-sm:block", className)} {...props}>
{children}
</table>
</div>
);
}
export function Thead({ className, children, ...props }: HTMLAttributes<HTMLTableSectionElement>) {
return (
<thead className={clsx("border-b border-border max-sm:hidden", className)} {...props}>
{children}
</thead>
);
return (
<thead className={clsx("border-b border-border max-sm:hidden", className)} {...props}>
{children}
</thead>
);
}
export function Tbody({ className, children, ...props }: HTMLAttributes<HTMLTableSectionElement>) {
return (
<tbody
className={clsx(
"divide-y divide-border",
"max-sm:block max-sm:space-y-3 max-sm:divide-y-0 max-sm:p-3",
className
)}
{...props}
>
{children}
</tbody>
);
return (
<tbody className={clsx("divide-y divide-border", "max-sm:block max-sm:space-y-3 max-sm:divide-y-0 max-sm:p-3", className)} {...props}>
{children}
</tbody>
);
}
export function Tr({ className, children, ...props }: HTMLAttributes<HTMLTableRowElement>) {
return (
<tr
className={clsx(
"transition-colors hover:bg-surface-2/50",
"max-sm:block max-sm:rounded max-sm:border max-sm:border-border max-sm:bg-surface-2/40 max-sm:p-3",
className
)}
{...props}
>
{children}
</tr>
);
return (
<tr className={clsx("transition-colors hover:bg-surface-2/50", "max-sm:block max-sm:rounded max-sm:border max-sm:border-border max-sm:bg-surface-2/40 max-sm:p-3", className)} {...props}>
{children}
</tr>
);
}
export function Th({ className, children, ...props }: ThHTMLAttributes<HTMLTableCellElement>) {
return (
<th
className={clsx(
// site/'s keyed-label idiom: mono, small, widely tracked, dimmed.
// A column head is a key, not prose.
// text-secondary, not tertiary: a column head is how you navigate the
// table, and tertiary lands under 4.5:1 at this size.
"px-4 py-3 text-left font-mono text-[0.68rem] uppercase tracking-[0.13em] text-text-secondary",
className
)}
{...props}
>
{children}
</th>
);
return (
<th
className={clsx(
// site/'s keyed-label idiom: mono, small, widely tracked, dimmed.
// A column head is a key, not prose.
// text-secondary, not tertiary: a column head is how you navigate the
// table, and tertiary lands under 4.5:1 at this size.
"px-4 py-3 text-left font-mono text-[0.68rem] uppercase tracking-[0.13em] text-text-secondary",
className,
)}
{...props}
>
{children}
</th>
);
}
interface TdProps extends TdHTMLAttributes<HTMLTableCellElement> {
/**
* The column head this cell belongs to, shown beside the value below sm
* where the real head is hidden. Omit on a trailing action cell an action
* needs no key, and the button then sits alone on its own row in the card.
*/
label?: string;
/**
* The column head this cell belongs to, shown beside the value below sm
* where the real head is hidden. Omit on a trailing action cell an action
* needs no key, and the button then sits alone on its own row in the card.
*/
label?: string;
}
export function Td({ className, label, children, ...props }: TdProps) {
return (
<td
className={clsx(
"px-4 py-3 text-text-primary",
"max-sm:flex max-sm:items-start max-sm:gap-4 max-sm:px-0 max-sm:py-1.5",
// Exactly one justify class clsx picks it. Emitting both and relying
// on string order would not work: Tailwind's output order decides which
// of two same-property utilities wins, not the order in this array.
label ? "max-sm:justify-between" : "max-sm:justify-end max-sm:pt-2.5",
className
)}
{...props}
>
{label && (
<span className="hidden shrink-0 font-mono text-[0.68rem] uppercase leading-5 tracking-[0.13em] text-text-secondary max-sm:inline">
{label}
</span>
)}
{/*
* Below sm the cell is a flex row, and a flex item's default
* min-width:auto resolves to its min-content width. A fingerprint or a
* target URL is a single unbreakable token, so min-content was the whole
* string: the value refused to shrink and spilled out of the card.
* min-w-0 lets it shrink, break-words gives the token somewhere to break.
*
* Above sm the wrapper is display:contents, so the desktop table renders
* exactly as it did before. Every other class here is max-sm: scoped on
* purpose — display:contents generates no box, but inherited properties
* (overflow-wrap among them) would still reach the children.
*/}
<span className="max-sm:min-w-0 max-sm:break-words sm:contents">{children}</span>
</td>
);
return (
<td
className={clsx(
"px-4 py-3 text-text-primary",
"max-sm:flex max-sm:items-start max-sm:gap-4 max-sm:px-0 max-sm:py-1.5",
// Exactly one justify class clsx picks it. Emitting both and relying
// on string order would not work: Tailwind's output order decides which
// of two same-property utilities wins, not the order in this array.
label ? "max-sm:justify-between" : "max-sm:justify-end max-sm:pt-2.5",
className,
)}
{...props}
>
{label && <span className="hidden shrink-0 font-mono text-[0.68rem] uppercase leading-5 tracking-[0.13em] text-text-secondary max-sm:inline">{label}</span>}
{/*
* Below sm the cell is a flex row, and a flex item's default
* min-width:auto resolves to its min-content width. A fingerprint or a
* target URL is a single unbreakable token, so min-content was the whole
* string: the value refused to shrink and spilled out of the card.
* min-w-0 lets it shrink, break-words gives the token somewhere to break.
*
* Above sm the wrapper is display:contents, so the desktop table renders
* exactly as it did before. Every other class here is max-sm: scoped on
* purpose display:contents generates no box, but inherited properties
* (overflow-wrap among them) would still reach the children.
*/}
<span className="max-sm:min-w-0 max-sm:break-words sm:contents">{children}</span>
</td>
);
}