import { forwardRef, useId } from "react";
import { inputVariants } from "@cloudflare/kumo/components/input";
import { Label } from "@cloudflare/kumo/components/label";
import { Select as KumoSelect } from "@cloudflare/kumo/components/select";
import { cn } from "~/utils/cn";
const CONTROL = cn(inputVariants({ focusIndicator: true }), "w-full disabled:opacity-60");
const ERROR_CONTROL = cn(inputVariants({ variant: "error", focusIndicator: true }), "w-full disabled:opacity-60");
function controlClass(error: string | null | undefined, className?: string): string {
return cn(error ? ERROR_CONTROL : CONTROL, className);
}
type FieldShellProps = {
label: string;
hint?: string;
error?: string | null;
id: string;
children: React.ReactNode;
};
function FieldShell({ label, hint, error, id, children }: FieldShellProps) {
return (
{children}
{hint && !error && (
{hint}
)}
{error &&
{error}}
);
}
function describedBy(id: string, hint?: string, error?: string | null): string | undefined {
if (error) return `${id}-error`;
if (hint) return `${id}-hint`;
return undefined;
}
type InputProps = Omit, "id"> & {
label: string;
hint?: string;
error?: string | null;
};
export const TextField = forwardRef(function TextField(
{ label, hint, error, className, ...rest },
ref,
) {
const id = useId();
return (
);
});
TextField.displayName = "TextField";
type TextAreaProps = Omit, "id"> & {
label: string;
hint?: string;
error?: string | null;
};
export const TextAreaField = forwardRef(function TextAreaField(
{ label, hint, error, className, ...rest },
ref,
) {
const id = useId();
return (
);
});
TextAreaField.displayName = "TextAreaField";
type SelectOption = { value: string; label: string };
type SelectProps = {
label: string;
hint?: string;
options: ReadonlyArray;
/** Submitted with the surrounding form through Kumo's hidden input. */
name?: string;
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
required?: boolean;
disabled?: boolean;
className?: string;
};
/** Kumo `Select` with a visible label; uncontrolled selects start on the first option like a native select. */
export function SelectField({
label,
hint,
options,
value,
defaultValue,
onValueChange,
className,
...rest
}: SelectProps) {
const initial = value === undefined ? (defaultValue ?? options[0]?.value) : undefined;
return (
label={label}
hideLabel={false}
description={hint}
items={options}
value={value}
defaultValue={initial}
onValueChange={(next) => onValueChange?.(next ?? "")}
className={cn("w-full", className)}
{...rest}
>
{options.map((option) => (
{option.label}
))}
);
}
type InlineSelectProps = {
label: string;
options: ReadonlyArray;
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
name?: string;
placeholder?: string;
className?: string;
disabled?: boolean;
};
/** Compact Kumo `Select` for table cells, toolbars and filter rows, labelled for screen readers only. */
export function InlineSelect({
label,
options,
value,
defaultValue,
onValueChange,
className,
...rest
}: InlineSelectProps) {
return (
label={label}
size="sm"
items={options}
value={value}
defaultValue={defaultValue}
onValueChange={(next) => onValueChange?.(next ?? "")}
className={className}
{...rest}
>
{options.map((option) => (
{option.label}
))}
);
}
type CheckboxProps = Omit, "id" | "type"> & {
label: string;
hint?: string;
};
export const CheckboxField = forwardRef(function CheckboxField(
{ label, hint, ...rest },
ref,
) {
const id = useId();
return (
{hint && (
{hint}
)}
);
});
CheckboxField.displayName = "CheckboxField";
export const inputClass = CONTROL;
type FieldErrorProps = { id?: string; className?: string; children: React.ReactNode };
export function FieldError({ id, className, children }: FieldErrorProps) {
return (
{children}
);
}
/** Kumo control styling for bare inputs and selects that sit inline (toolbars, table cells). */
export function compactInputClass(className?: string): string {
return cn(inputVariants({ size: "sm", focusIndicator: true }), className);
}