import Link from "next/link";
import type { ComponentProps, ReactNode } from "react";
import {
Avatar as MuAvatar,
Button as MuButton,
EmptyState,
Kbd as MuKbd,
Label as MuLabel,
Led,
StatusBadge,
Surface,
type ButtonCap,
type LedKind,
} from "@unlocalhosted/metalui";
import { LockIcon } from "./icons";
/** Seeded 32-bit FNV-1a with a final avalanche, so similar names give unrelated patterns. */
function hash32(text: string, seed = 0) {
let h = (0x811c9dc5 ^ seed) >>> 0;
for (let i = 0; i < text.length; i++) {
h ^= text.charCodeAt(i);
h = Math.imul(h, 0x01000193);
}
h ^= h >>> 16;
h = Math.imul(h, 0x85ebca6b);
h ^= h >>> 13;
h = Math.imul(h, 0xc2b2ae35);
h ^= h >>> 16;
return h >>> 0;
}
/** A GitHub-style identicon: a horizontally mirrored 5×5 grid in one ink. */
function identicon(name: string) {
const key = name.trim().toLowerCase();
const bits = hash32(key);
const tone = hash32(key, 0x9e3779b9);
let path = "";
for (let row = 0; row < 5; row++) {
for (let col = 0; col < 3; col++) {
if (!(bits & (1 << (row * 3 + col)))) continue;
path += `M${col} ${row}h1v1h-1z`;
if (col < 2) path += `M${4 - col} ${row}h1v1h-1z`;
}
}
// MetalUI's feeling tints, so a repository's mark sits with the rest of the palette.
const tints = ["ember", "blush", "tide", "spark", "dusk", "iris", "graphite"];
return { path: path || "M2 0h1v5h-1z", color: `var(--mu-tint-${tints[tone % tints.length]})` };
}
function Identicon({ name, size }: { name: string; size: number }) {
const { path, color } = identicon(name);
return (
<svg viewBox="-1.2 -1.2 7.4 7.4" width={size} height={size} shapeRendering="crispEdges">
<path d={path} fill={color} opacity={0.85} />
</svg>
);
}
/**
* A person: MetalUI's raised disc with their initials. Its sizes are 24, 32 and 44; smaller avatars
* (inline beside a name) are the small disc scaled down.
*/
export function Avatar({ name, size = 20, className = "" }: { name: string; size?: number; className?: string }) {
const muSize = size >= 40 ? "large" : size >= 30 ? "regular" : "small";
const base = muSize === "large" ? 44 : muSize === "regular" ? 32 : 24;
return (
<span
className={`inline-flex shrink-0 rounded-full select-none ${className}`}
style={size === base ? undefined : { zoom: size / base }}
>
<MuAvatar name={name} size={muSize} />
</span>
);
}
export type RepoImageRef = { owner: string; name: string; imageVersion: number | null };
export const repoImageUrl = (repo: RepoImageRef) => `/${repo.owner}/${repo.name}/image?v=${repo.imageVersion}`;
/**
* A repository's "logo" on a raised key: the image uploaded in the repository's settings, or else an
* identicon engraved from the owner's name.
*/
export function RepoAvatar({
repo,
size = 40,
className = "",
src,
}: {
repo: RepoImageRef;
size?: number;
className?: string;
/** Show this image instead, e.g. a local preview before uploading. */
src?: string;
}) {
const image = src ?? (repo.imageVersion ? repoImageUrl(repo) : null);
return (
<span
className={`material-cap inline-flex shrink-0 overflow-hidden select-none ${className}`}
style={{ width: size, height: size, borderRadius: Math.max(4, Math.round(size * 0.3)) }}
aria-hidden
>
{image ? (
// eslint-disable-next-line @next/next/no-img-element -- tiny, already-sized, auth-checked images
<img src={image} alt="" width={size} height={size} className="h-full w-full object-cover" />
) : (
<Identicon name={repo.owner} size={size} />
)}
</span>
);
}
/**
* The app's button variants on MetalUI's caps: `default` is the soft-touch standard cap, `primary`
* (and `success`, the old green "do it") the dark cap, `danger` the red cap, `invisible` a quiet
* ghost key that only fills on hover. One primary or danger cap per group.
*/
const caps = {
default: "standard",
primary: "primary",
success: "primary",
danger: "destructive",
} satisfies Record<string, ButtonCap>;
export type ButtonVariant = keyof typeof caps | "invisible";
// MetalUI's Button recipe (button.tsx), for elements that must look like a cap but can't be one:
// a <label> around a file input, a <summary>.
const FRAME =
"mu-button mu-icon-trigger box-border inline-flex items-center justify-center m-0 border-0 whitespace-nowrap cursor-pointer select-none antialiased tap-highlight-none [&>svg]:flex-none focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-deep data-disabled:cursor-default data-disabled:opacity-button-disabled disabled:cursor-default disabled:opacity-button-disabled";
const PRESS = "not-data-disabled:active:translate-y-button-travel not-data-disabled:active:duration-button-press not-data-disabled:active:ease-linear";
const REGULAR = "gap-button-gap h-button-height px-button-pad rounded-pill type-ui transition-button [&>svg]:size-button-glyph";
const capClasses: Record<ButtonVariant, string> = {
default: `${REGULAR} text-ink recipe-button ${PRESS} not-data-disabled:active:recipe-button-pressed`,
primary: `${REGULAR} text-button-primary-ink recipe-button-primary ${PRESS} not-data-disabled:active:recipe-button-primary-pressed`,
success: `${REGULAR} text-button-primary-ink recipe-button-primary ${PRESS} not-data-disabled:active:recipe-button-primary-pressed`,
danger: `${REGULAR} text-button-destructive-ink recipe-button-destructive ${PRESS} not-data-disabled:active:recipe-button-destructive-pressed`,
invisible: `${REGULAR} bg-transparent text-ink2 hover:text-ink hover:recipe-icon-button-ghost-hover ${PRESS}`,
};
export function buttonClass(variant: ButtonVariant = "default", extra = "") {
return `${FRAME} ${capClasses[variant]} ${extra}`;
}
export function Button({
variant = "default",
className = "",
icon,
children,
...props
}: Omit<ComponentProps<"button">, "ref"> & { variant?: ButtonVariant; icon?: ReactNode }) {
if (variant === "invisible") {
return (
<button className={buttonClass("invisible", className)} {...props}>
{icon}
{children}
</button>
);
}
// Base UI's Button defaults to type="button"; keep the native default so buttons in forms submit them.
return (
<MuButton cap={caps[variant]} icon={icon} className={className} type="submit" {...props}>
{children}
</MuButton>
);
}
/** A MetalUI Button that navigates: the cap, rendered as a Next.js link. */
export function ButtonLink({
variant = "default",
className = "",
icon,
children,
href,
prefetch,
replace,
scroll,
...props
}: ComponentProps<typeof Link> & { variant?: ButtonVariant; icon?: ReactNode }) {
const link = <Link href={href} prefetch={prefetch} replace={replace} scroll={scroll} />;
if (variant === "invisible") {
return (
<Link href={href} prefetch={prefetch} replace={replace} scroll={scroll} className={buttonClass("invisible", className)} {...props}>
{icon}
{children}
</Link>
);
}
return (
<MuButton cap={caps[variant]} icon={icon} nativeButton={false} render={link} className={className} {...(props as object)}>
{children}
</MuButton>
);
}
/**
* MetalUI's regular form field (Field size="regular"): a sunk well, 32 tall at radius 11, the green
* caret, no ring on focus. For a plain <input>, <select> or <textarea>.
*/
export const inputClass =
"mu-field block w-full box-border min-h-field-regular-height px-3 py-[7px] rounded-field-regular-radius recipe-well-field border-0 outline-none type-ui text-field-field-ink caret-field-field-caret placeholder:text-field-field-hint aria-invalid:invalid-ring disabled:opacity-field-state-disabled disabled:cursor-default";
/** A control with its words: MetalUI's form field anatomy (label above, 6 apart; description below). */
export function Field({
label,
hint,
children,
optional,
}: {
label: string;
hint?: ReactNode;
children: ReactNode;
optional?: boolean;
}) {
return (
<label className="flex flex-col gap-1.5">
<span className="type-ui text-ink">
{label} {optional && <span className="text-ink3">· optional</span>}
</span>
{children}
{hint && <span className="type-meta text-ink3">{hint}</span>}
</label>
);
}
/**
* A notice in the colorway: a raised plate with an LED that names the state (red failed, green
* done), the words beside it. Errors are announced at once, the rest politely.
*/
export function Alert({ kind = "error", children }: { kind?: "error" | "success" | "info"; children: ReactNode }) {
const led: Record<typeof kind, LedKind> = { error: "failed", success: "live", info: "off" };
return (
<Surface
material="raise-lite"
radius="row"
role={kind === "error" ? "alert" : "status"}
className={`flex items-start gap-3 px-4 py-3 type-body ${kind === "error" ? "text-red" : "text-ink"}`}
>
<Led kind={led[kind]} className="mt-[6.5px]" />
<div className="min-w-0 flex-1">{children}</div>
</Surface>
);
}
/** The standard object surface: a raised plate (Surface material="raise-lite") at the card radius. */
export function Box({ children, className = "" }: { children: ReactNode; className?: string }) {
return (
<Surface material="raise-lite" radius="card" className={className}>
{children}
</Surface>
);
}
/** A count, in MetalUI's mono count role. */
export function Counter({ children, className = "" }: { children: ReactNode; className?: string }) {
return (
<MuLabel variant="count" className={`tabular-nums ${className}`}>
{children}
</MuLabel>
);
}
/**
* A short state or role as MetalUI's status badge: the cap pill with an LED and the engraved label.
* The LED carries the tone (green live, amber waiting, red failed, blue a kind); plain is unlit.
*/
export function Label({ children, tone = "default" }: { children: ReactNode; tone?: "default" | "accent" | "success" | "attention" | "danger" }) {
const leds: Record<typeof tone, LedKind> = { default: "off", accent: "link", success: "live", attention: "waiting", danger: "failed" };
return (
<StatusBadge led={leds[tone]} className="shrink-0 [&_svg]:h-3 [&_svg]:w-3">
{children}
</StatusBadge>
);
}
export function VisibilityLabel({ visibility }: { visibility: "public" | "private" }) {
return visibility === "private" ? (
<Label tone="attention">
<LockIcon /> Private
</Label>
) : (
<Label>Public</Label>
);
}
/** A place with nothing in it yet: MetalUI's EmptyState (a glyph in a sunk well, what would be here, how to start). */
export function Blankslate({ icon, title, children, action }: { icon?: ReactNode; title: string; children?: ReactNode; action?: ReactNode }) {
return <EmptyState title={title} description={children} icon={icon} action={action} className="px-6 py-14" />;
}
export function PageTitle({ title, description, actions }: { title: string; description?: ReactNode; actions?: ReactNode }) {
return (
<div className="mb-6 flex flex-wrap items-end gap-4">
<div className="min-w-0 flex-1">
<h1 className="text-[22px] leading-7 font-semibold tracking-[-0.025em] text-ink">{title}</h1>
{description && <p className="mt-1 type-lead text-ink2">{description}</p>}
</div>
{actions}
</div>
);
}
/**
* The switcher's track (a sunk pill) for options the caller renders as links or buttons, when they
* navigate and so can't be MetalUI's Switcher (a radio group). Pair with `segmentClass`.
*/
export function Segmented({ children, className = "" }: { children: ReactNode; className?: string }) {
return <div className={`inline-flex items-center gap-0.5 rounded-pill recipe-well-track p-[3px] ${className}`}>{children}</div>;
}
/** An option on the switcher track; the active one rides the raised thumb. */
export function segmentClass(active: boolean) {
return `inline-flex items-center gap-1.5 rounded-pill px-3 h-7 type-ui whitespace-nowrap transition-colors duration-150 [&_svg]:size-3.5 ${
active ? "material-thumb text-ink" : "text-ink2 hover:text-ink"
}`;
}
/** A branch or ref: an engraved mono tag in a sunk well. */
export function RefChip({ children, className = "" }: { children: ReactNode; className?: string }) {
return (
<code className={`well inline-flex items-center rounded-[7px] px-1.5 py-0.5 font-mono text-[11.5px] text-ink ${className}`}>{children}</code>
);
}
/** A key, on MetalUI's small raised keycap. */
export function Kbd({ children }: { children: ReactNode }) {
return <MuKbd>{children}</MuKbd>;
}