Vectrum
Sign In
328 lines · 12.4 KB · TypeScript
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>;
}