"use client";
import { useEffect, useRef, useState, type ComponentProps, type ReactNode } from "react";
import { Field, FormField, IconButton, Radio, RadioGroup, Textarea, Tooltip, useToast } from "@unlocalhosted/metalui";
import { MorphIcon } from "@unlocalhosted/metalui/icons";
/*
* MetalUI's form anatomy for the settings, auth and new-repository forms: the label above, 6 apart,
* the control, then its description in meta ink3 (FormField), around MetalUI's regular Field and
* Textarea. Every control keeps its `name`, so the forms still post to their server actions.
*/
type Words = { label: ReactNode; description?: ReactNode; optional?: boolean; className?: string };
function FieldLabel({ label, optional }: { label: ReactNode; optional?: boolean }) {
return (
{label}
{optional && · optional}
);
}
/** A one-line text field: MetalUI's Field at the form size (32, radius 11) with its words. */
export function TextField({
label,
description,
optional,
className,
icon,
mono,
...input
}: Words & { icon?: ReactNode; mono?: boolean } & Omit, "ref" | "className">) {
return (
{icon && {icon}}
{description && {description}}
);
}
/** Several lines: MetalUI's Textarea (it grows with what is written) with its words. */
export function TextareaField({
label,
description,
optional,
className,
mono,
...textarea
}: Words & { mono?: boolean } & Omit, "ref" | "className">) {
return (
{description && {description}}
);
}
export type Choice = { value: V; title: string; description?: ReactNode; icon?: ReactNode };
/**
* One of a few choices that need explaining (visibility, a token's permission): MetalUI's RadioGroup,
* each option a pane on a sunk track; the chosen one rides the raised thumb, as on a switcher.
*/
export function ChoiceCards({
name,
choices,
value,
defaultValue,
onValueChange,
columns = 2,
...aria
}: {
name: string;
choices: Choice[];
value?: V;
defaultValue?: V;
onValueChange?: (value: V) => void;
columns?: 1 | 2;
"aria-label"?: string;
"aria-labelledby"?: string;
}) {
return (
onValueChange?.(v as V)}
className={`grid gap-1 rounded-plate recipe-well-track p-1 ${columns === 2 ? "sm:grid-cols-2" : ""}`}
{...aria}
>
{choices.map((c) => (
{c.icon && {c.icon}}
{c.title}
{c.description && {c.description}}
))}
);
}
/**
* Copies a value: MetalUI's ghost IconButton whose glyph morphs duplicate → check while it is copied,
* named by its tooltip. With `toast`, the copy is also confirmed in the toast deck.
*/
export function CopyIconButton({ text, label = "Copy", toast }: { text: string; label?: string; toast?: string }) {
const [copied, setCopied] = useState(false);
const timer = useRef | undefined>(undefined);
const toasts = useToast();
useEffect(() => () => clearTimeout(timer.current), []);
return (
}
onClick={async () => {
await navigator.clipboard.writeText(text);
setCopied(true);
if (toast) toasts.show({ title: toast, tone: "success" });
clearTimeout(timer.current);
timer.current = setTimeout(() => setCopied(false), 1500);
}}
/>
);
}