Files
vantage-app/adminsite/components/Button.tsx
T
2026-08-11 10:32:59 +01:00

69 lines
2.6 KiB
TypeScript

import clsx from "clsx";
import Link from "next/link";
type Variant = "solid" | "line";
/*
* Matches site/'s .btn--solid and .btn--line exactly, including the neutral
* border on the secondary variant. site/ does not have an accent-outlined
* button and this app should not invent one.
*/
/*
* The height every form control resolves to, buttons included.
*
* Padding alone cannot align them: a select is mono at 0.84rem and a button is
* sans at 0.94rem, so identical padding still leaves them ~7px apart and a
* filter row looks assembled from two different kits. It is the height the
* button's own padding already computed to, so buttons do not move — everything
* else comes up to meet them.
*/
export const CONTROL_HEIGHT = "h-11";
/*
* An input or select that sits on a form row with a button. Mono, because in
* this product the values typed into these are addresses, UUIDs and price IDs.
*/
export function controlClass(className?: string) {
return clsx(
CONTROL_HEIGHT,
"w-full rounded border border-rule bg-panel-2 px-2.5 font-mono text-[0.88rem] text-ink",
"focus:border-accent focus:outline-none",
className,
);
}
export function buttonClass(variant: Variant = "solid", disabled = false, className?: string) {
return clsx(
"inline-flex items-center gap-2 rounded border px-4 text-[0.94rem] font-semibold",
CONTROL_HEIGHT,
"transition-[filter,border-color] duration-150 hover:brightness-110",
variant === "solid" ? "border-accent bg-accent text-accent-ink" : "border-rule bg-panel text-ink hover:border-ink-3",
disabled && "cursor-not-allowed border-rule bg-panel text-ink-3 hover:brightness-100",
className,
);
}
type Props = React.ButtonHTMLAttributes<HTMLButtonElement> & { variant?: Variant };
export function Button({ variant = "solid", className, ...rest }: Props) {
return <button {...rest} className={buttonClass(variant, rest.disabled, className)} />;
}
/*
* A link that looks like a button. It exists so a navigation action never has to
* be an <a> wrapped around a <button> invalid markup, and it gives screen
* readers two nested controls where the page means one.
*/
export function LinkButton({ href, variant = "solid", external, className, children }: { href: string; variant?: Variant; external?: boolean; className?: string; children: React.ReactNode }) {
const cls = buttonClass(variant, false, className);
return external ? (
<a href={href} className={cls}>
{children}
</a>
) : (
<Link href={href} className={cls}>
{children}
</Link>
);
}