import { forwardRef, useId, type ButtonHTMLAttributes, type InputHTMLAttributes, type SelectHTMLAttributes, type TextareaHTMLAttributes, type ReactNode, } from "react"; import { Dialog as HeadlessDialog, DialogPanel, DialogTitle, Menu, MenuButton, MenuItems, MenuItem, } from "@headlessui/react"; export interface ButtonProps extends ButtonHTMLAttributes { variant?: "primary" | "secondary" | "ghost" | "danger"; loading?: boolean; } export const Button = forwardRef( function Button( { variant = "primary", loading = false, disabled, children, className = "", type = "button", ...props }, ref, ) { return ( {loading && } {children} ); }, ); export interface LinkAction { label: string; href: string; } export function ActionLink({ label, href, variant = "primary", }: LinkAction & { variant?: ButtonProps["variant"] }) { return ( {label} ); } export type Tone = "neutral" | "success" | "warning" | "danger"; export function Badge({ children, tone = "neutral", }: { children: ReactNode; tone?: Tone; }) { return {children}; } interface FieldDescription { label: string; hint?: string; error?: string; } export interface InputProps extends InputHTMLAttributes, FieldDescription { prefix?: string; suffix?: string; } export const Input = forwardRef(function Input( { label, hint, error, id, prefix, suffix, className = "", ...props }, ref, ) { const generated = useId(), fieldId = id || generated, messageId = fieldId + "-message"; return ( {label} {prefix && {prefix}} {suffix && {suffix}} {(error || hint) && ( {error || hint} )} ); }); export function Select({ label, options, id, hint, error, ...props }: SelectHTMLAttributes & FieldDescription & { options: { value: string; label: string }[] }) { const generated = useId(), fieldId = id || generated, listbox = !!props.multiple || (props.size || 0) > 1; return ( {label} {options.map((o) => ( {o.label} ))} {!listbox && ( )} {(hint || error) && ( {error || hint} )} ); } export function TextArea({ label, id, hint, error, ...props }: TextareaHTMLAttributes & FieldDescription) { const generated = useId(), fieldId = id || generated; return ( {label} {(hint || error) && ( {error || hint} )} ); } export function Checkbox({ label, ...props }: Omit, "type"> & { label: string }) { return ( {label} ); } export function Switch({ label, ...props }: Omit, "type" | "role"> & { label: string; }) { return ( {label} ); } export function RadioGroup({ label, name, value, onChange, options, }: { label: string; name: string; value: string; onChange: (value: string) => void; options: { value: string; label: string; description?: string }[]; }) { return ( {label} {options.map((o) => ( onChange(o.value)} /> {o.label} {o.description && {o.description}} ))} ); } export function Tabs({ label, value, onChange, items, }: { label: string; value: string; onChange: (value: string) => void; items: { id: string; label: string; content: ReactNode }[]; }) { const base = useId(); return ( { const i = items.findIndex((item) => item.id === value); let next = i; if (e.key === "ArrowRight") next = (i + 1) % items.length; else if (e.key === "ArrowLeft") next = (i - 1 + items.length) % items.length; else if (e.key === "Home") next = 0; else if (e.key === "End") next = items.length - 1; else return; e.preventDefault(); onChange(items[next].id); document.getElementById(`${base}-${items[next].id}-tab`)?.focus(); }} > {items.map((item) => ( onChange(item.id)} > {item.label} ))} {items.map((item) => ( {item.content} ))} ); } export function Modal({ open, onClose, title, children, actions, }: { open: boolean; onClose: () => void; title: string; children: ReactNode; actions?: ReactNode; }) { return ( {title} Close {children} {actions && {actions}} ); } export function Dropdown({ label, items, }: { label: string; items: { label: string; onSelect: () => void; danger?: boolean; disabled?: boolean; }[]; }) { return ( {label} ⌄ {items.map((item) => ( {item.label} ))} ); } export function Notice({ title, children, tone = "neutral", announce = false, }: { title: string; children?: ReactNode; tone?: Tone; announce?: boolean; }) { return ( {title} {children && {children}} ); } export function EmptyState({ title, description, action, }: { title: string; description: string; action?: ReactNode; }) { return ( {title} {description} {action} ); }
{error || hint}
{description}