"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 (
<FormField.Label>
{label}
{optional && <span className="text-ink3"> · optional</span>}
</FormField.Label>
);
}
/** 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<ComponentProps<"input">, "ref" | "className">) {
return (
<FormField className={className} disabled={input.disabled}>
<FieldLabel label={label} optional={optional} />
<Field size="regular" disabled={input.disabled}>
{icon && <Field.Icon>{icon}</Field.Icon>}
<Field.Input {...input} className={mono ? "font-mono!" : undefined} />
</Field>
{description && <FormField.Description>{description}</FormField.Description>}
</FormField>
);
}
/** 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<ComponentProps<typeof Textarea>, "ref" | "className">) {
return (
<FormField className={className}>
<FieldLabel label={label} optional={optional} />
<Textarea
{...textarea}
className={
mono
? "[&_.mu-textarea-input]:font-mono [&_.mu-textarea-input]:text-[12px] [&_.mu-textarea-input]:leading-[18px] [&_.mu-textarea-mirror]:font-mono [&_.mu-textarea-mirror]:text-[12px] [&_.mu-textarea-mirror]:leading-[18px] [&_.mu-textarea-mirror]:break-all [&_.mu-textarea-input]:break-all"
: undefined
}
/>
{description && <FormField.Description>{description}</FormField.Description>}
</FormField>
);
}
export type Choice<V extends string> = { 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<V extends string>({
name,
choices,
value,
defaultValue,
onValueChange,
columns = 2,
...aria
}: {
name: string;
choices: Choice<V>[];
value?: V;
defaultValue?: V;
onValueChange?: (value: V) => void;
columns?: 1 | 2;
"aria-label"?: string;
"aria-labelledby"?: string;
}) {
return (
<RadioGroup
name={name}
value={value}
defaultValue={defaultValue}
onValueChange={(v) => 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) => (
<Radio
key={c.value}
value={c.value}
className="w-full items-start! rounded-[14px] px-3.5 py-3 transition-[background,box-shadow] duration-150 has-data-checked:material-thumb [&>.mu-radio-well]:mt-px"
>
<span className="flex min-w-0 gap-3">
{c.icon && <span className="mt-px shrink-0 text-ink2 [&_svg]:size-4">{c.icon}</span>}
<span className="flex min-w-0 flex-col gap-0.5">
<span className="type-title text-ink">{c.title}</span>
{c.description && <span className="type-body text-ink2">{c.description}</span>}
</span>
</span>
</Radio>
))}
</RadioGroup>
);
}
/**
* 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<ReturnType<typeof setTimeout> | undefined>(undefined);
const toasts = useToast();
useEffect(() => () => clearTimeout(timer.current), []);
return (
<Tooltip label={copied ? "Copied" : label}>
<IconButton
label={label}
icon={<MorphIcon name={copied ? "check" : "duplicate"} />}
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);
}}
/>
</Tooltip>
);
}