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 (
);
}
/** Where an account's uploaded avatar is served (organizations set one in their settings), if it has one. */
export const avatarUrl = (account: { username: string; avatarVersion?: number | null }) =>
account.avatarVersion ? `/organizations/${account.username}/avatar?v=${account.avatarVersion}` : undefined;
/**
* A person or organization: their uploaded avatar in a round cap, or else 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 = "", src }: { name: string; size?: number; className?: string; src?: string }) {
if (src) {
return (
{/* eslint-disable-next-line @next/next/no-img-element -- tiny, already-sized images */}
);
}
const muSize = size >= 40 ? "large" : size >= 30 ? "regular" : "small";
const base = muSize === "large" ? 44 : muSize === "regular" ? 32 : 24;
return (
);
}
export type RepoImageRef = {
owner: string;
name: string;
imageVersion: number | null;
/** The owning organization's avatar, shown when the repository has no image of its own. */
ownerAvatarVersion?: 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 its
* organization's avatar, 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) : avatarUrl({ username: repo.owner, avatarVersion: repo.ownerAvatarVersion }));
return (
{image ? (
// eslint-disable-next-line @next/next/no-img-element -- tiny, already-sized, auth-checked images
) : (
)}
);
}
/**
* 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;
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