import { type JSX, For, Show, createSignal } from 'solid-js';
import { cn } from '../utils/cn';
import { Textarea } from '../ui/textarea';
import { Star } from 'lucide-solid';
import type { FormField } from './form';
/** The shared prop shape every leaf widget receives from FieldRow. */
export interface WidgetProps {
id: string;
value: unknown;
field: FormField;
disabled: boolean;
placeholder?: string;
required: boolean;
invalid: boolean;
describedBy?: string;
label: string;
onInput: (value: unknown) => void;
onBlur: () => void;
}
const inputBase =
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:pointer-events-none';
function ariaProps(p: WidgetProps) {
return {
'aria-required': p.required || undefined,
'aria-invalid': p.invalid || undefined,
'aria-describedby': p.describedBy,
};
}
/** text / email / url / date / datetime / time / password — all variants. */
export function TextWidget(
props: WidgetProps & { variant: 'text' | 'email' | 'url' | 'date' | 'datetime' | 'time' | 'password' },
): JSX.Element {
const inputType = () => {
switch (props.variant) {
case 'email':
return 'email';
case 'url':
return 'url';
case 'date':
return 'date';
case 'datetime':
return 'datetime-local';
case 'time':
return 'time';
case 'password':
return 'password';
default:
return 'text';
}
};
return (
props.onInput(e.currentTarget.value)}
onBlur={props.onBlur}
/>
);
}
export function TextareaWidget(props: WidgetProps): JSX.Element {
const len = () => ((props.value as string) ?? '').length;
return (
);
}
export function NumberWidget(props: WidgetProps): JSX.Element {
const step = () => props.field['x-kc-step'] ?? (props.field.type === 'integer' ? 1 : undefined);
return (
props.onInput(e.currentTarget.value)}
onBlur={props.onBlur}
/>
);
}
export function SliderWidget(props: WidgetProps): JSX.Element {
const min = () => props.field.minimum ?? 0;
const max = () => props.field.maximum ?? 100;
const step = () => props.field['x-kc-step'] ?? (props.field.type === 'integer' ? 1 : undefined);
const current = () => (props.value === undefined || props.value === null ? min() : Number(props.value));
const fill = (): string => {
const lo = min();
const hi = max();
return hi > lo ? `${((current() - lo) / (hi - lo)) * 100}%` : '0%';
};
return (
props.onInput(Number(e.currentTarget.value))}
onBlur={props.onBlur}
/>
{current()}
);
}
export function RatingWidget(props: WidgetProps): JSX.Element {
const max = () => props.field.maximum ?? 5;
const current = () => Number(props.value ?? 0);
const stars = () => Array.from({ length: max() }, (_, i) => i + 1);
const onKey = (e: KeyboardEvent): void => {
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
e.preventDefault();
props.onInput(Math.min(max(), current() + 1));
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
e.preventDefault();
props.onInput(Math.max(props.field.minimum ?? 1, current() - 1));
}
};
return (
{(n) => (
)}
);
}
export function SwitchWidget(props: WidgetProps): JSX.Element {
const on = () => props.value === true;
const toggle = (): void => {
if (props.disabled) return;
props.onInput(!on());
props.onBlur();
};
return (
);
}
export function CheckboxWidget(props: WidgetProps): JSX.Element {
return (
);
}
export function RadioGroupWidget(props: WidgetProps): JSX.Element {
const options = () => (props.field.enum ?? []) as unknown[];
return (
{(opt) => (
)}
);
}
export function SelectWidget(props: WidgetProps): JSX.Element {
const options = () => (props.field.enum ?? []) as unknown[];
return (
);
}
function itemEnum(field: FormField): unknown[] {
const items = field.items;
if (items && 'enum' in items && Array.isArray(items.enum)) return items.enum;
return [];
}
export function CheckboxGroupWidget(props: WidgetProps): JSX.Element {
const selected = () => (Array.isArray(props.value) ? (props.value as unknown[]) : []);
const toggle = (opt: unknown): void => {
const set = selected();
const next = set.includes(opt) ? set.filter((v) => v !== opt) : [...set, opt];
props.onInput(next);
props.onBlur();
};
return (
{(opt) => (
)}
);
}
export function MultiSelectWidget(props: WidgetProps): JSX.Element {
const selected = () => (Array.isArray(props.value) ? (props.value as unknown[]) : []);
return (
);
}
export function TagListWidget(props: WidgetProps): JSX.Element {
const tags = () => (Array.isArray(props.value) ? (props.value as string[]) : []);
const [draft, setDraft] = createSignal('');
const add = (): void => {
const v = draft().trim();
if (!v) return;
props.onInput([...tags(), v]);
setDraft('');
props.onBlur();
};
const remove = (i: number): void => {
props.onInput(tags().filter((_, idx) => idx !== i));
props.onBlur();
};
return (
);
}