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