open Htmlit type input_type = | Text | Email | Password | Number | Tel | Url | Search | Date | Time | Datetime_local type validation_state = | Valid | Invalid | Warning type field_type = | Input of input_type | Textarea | Select of (string * string) list (* value, label pairs *) | Checkbox | Radio of string (* value *) | Switch type t = { field_type: field_type; label: string option; placeholder: string option; value: string option; name: string option; id: string option; rows: int option; (* for textarea *) required: bool; disabled: bool; readonly: bool; checked: bool; (* for checkbox/radio/switch *) validation: validation_state option; helper_text: string option; error_text: string option; classes: Tailwind.t option; attributes: (string * string) list; } let classes_attr tailwind_classes = At.class' (Tailwind.to_string tailwind_classes) let base_input_classes = Tailwind.Css.tw [ Tailwind.Display.flex; Tailwind.Layout.(to_class (height (Tailwind.Size.rem 2.5))); Tailwind.Layout.w_full; Tailwind.Effects.rounded_md; Tailwind.Effects.border; Tailwind.Color.border (Tailwind.Color.make `Gray ~variant:`V300 ()); Tailwind.Color.bg Tailwind.Color.white; Tailwind.Spacing.(to_class (px (Tailwind.Size.rem 0.75))); Tailwind.Spacing.(to_class (py (Tailwind.Size.rem 0.5))); Tailwind.Typography.(to_class (font_size `Sm)); ] let validation_classes = function | Some Valid -> Tailwind.Css.tw [ Tailwind.Color.border (Tailwind.Color.make `Green ~variant:`V500 ()); Tailwind.Variants.focus (Tailwind.Color.border (Tailwind.Color.make `Green ~variant:`V600 ())); ] | Some Invalid -> Tailwind.Css.tw [ Tailwind.Color.border (Tailwind.Color.make `Red ~variant:`V500 ()); Tailwind.Variants.focus (Tailwind.Color.border (Tailwind.Color.make `Red ~variant:`V600 ())); ] | Some Warning -> Tailwind.Css.tw [ Tailwind.Color.border (Tailwind.Color.make `Yellow ~variant:`V500 ()); Tailwind.Variants.focus (Tailwind.Color.border (Tailwind.Color.make `Yellow ~variant:`V600 ())); ] | None -> Tailwind.Css.empty let input ?input_type ?label ?placeholder ?value ?name ?id ?required ?disabled ?readonly ?validation ?helper_text ?error_text ?classes ?attributes () = { field_type = Input (match input_type with Some t -> t | None -> Text); label; placeholder; value; name; id; rows = None; required = (match required with Some r -> r | None -> false); disabled = (match disabled with Some d -> d | None -> false); readonly = (match readonly with Some r -> r | None -> false); checked = false; validation; helper_text; error_text; classes; attributes = (match attributes with Some a -> a | None -> []); } let textarea ?label ?placeholder ?value ?name ?id ?rows ?required ?disabled ?readonly ?validation ?helper_text ?error_text ?classes ?attributes () = { field_type = Textarea; label; placeholder; value; name; id; rows; required = (match required with Some r -> r | None -> false); disabled = (match disabled with Some d -> d | None -> false); readonly = (match readonly with Some r -> r | None -> false); checked = false; validation; helper_text; error_text; classes; attributes = (match attributes with Some a -> a | None -> []); } let select ?label ?name ?id ?required ?disabled ?validation ?helper_text ?error_text ?classes ?attributes ~options () = { field_type = Select options; label; placeholder = None; value = None; name; id; rows = None; required = (match required with Some r -> r | None -> false); disabled = (match disabled with Some d -> d | None -> false); readonly = false; checked = false; validation; helper_text; error_text; classes; attributes = (match attributes with Some a -> a | None -> []); } let checkbox ?label ?name ?id ?checked ?disabled ?classes ?attributes () = { field_type = Checkbox; label; placeholder = None; value = None; name; id; rows = None; required = false; disabled = (match disabled with Some d -> d | None -> false); readonly = false; checked = (match checked with Some c -> c | None -> false); validation = None; helper_text = None; error_text = None; classes; attributes = (match attributes with Some a -> a | None -> []); } let radio ?label ?name ?id ?value ?checked ?disabled ?classes ?attributes () = { field_type = Radio (match value with Some v -> v | None -> ""); label; placeholder = None; value; name; id; rows = None; required = false; disabled = (match disabled with Some d -> d | None -> false); readonly = false; checked = (match checked with Some c -> c | None -> false); validation = None; helper_text = None; error_text = None; classes; attributes = (match attributes with Some a -> a | None -> []); } let switch ?label ?name ?id ?checked ?disabled ?classes ?attributes () = { field_type = Switch; label; placeholder = None; value = None; name; id; rows = None; required = false; disabled = (match disabled with Some d -> d | None -> false); readonly = false; checked = (match checked with Some c -> c | None -> false); validation = None; helper_text = None; error_text = None; classes; attributes = (match attributes with Some a -> a | None -> []); } let input_type_to_string = function | Text -> "text" | Email -> "email" | Password -> "password" | Number -> "number" | Tel -> "tel" | Url -> "url" | Search -> "search" | Date -> "date" | Time -> "time" | Datetime_local -> "datetime-local" let to_html field = let field_classes = Tailwind.Css.tw [ base_input_classes; validation_classes field.validation; (match field.classes with Some c -> c | None -> Tailwind.Css.empty); ] in let base_attrs = [classes_attr field_classes] in let optional_attrs = List.filter_map (fun x -> x) [ Option.map At.placeholder field.placeholder; Option.map At.value field.value; Option.map At.name field.name; Option.map At.id field.id; (if field.required then Some At.required else None); (if field.disabled then Some At.disabled else None); (if field.readonly then Some (At.v "readonly" "readonly") else None); (if field.checked then Some At.checked else None); ] in let custom_attrs = List.map (fun (k, v) -> At.v k v) field.attributes in let all_attrs = base_attrs @ optional_attrs @ custom_attrs in let input_element = match field.field_type with | Input input_type -> El.input ~at:(At.type' (input_type_to_string input_type) :: all_attrs) () | Textarea -> let textarea_attrs = match field.rows with | Some r -> At.rows r :: all_attrs | None -> all_attrs in El.textarea ~at:textarea_attrs [] | Select options -> let option_elements = List.map (fun (value, label) -> El.option ~at:[At.value value] [El.txt label] ) options in El.select ~at:all_attrs option_elements | Checkbox -> El.input ~at:(At.type' "checkbox" :: all_attrs) () | Radio value -> El.input ~at:(At.type' "radio" :: At.value value :: all_attrs) () | Switch -> (* Switch is implemented as a styled checkbox *) El.input ~at:(At.type' "checkbox" :: all_attrs) () in let label_element = match field.label with | Some label_text -> El.label [ El.txt label_text; input_element; ] | None -> input_element in (* Add helper and error text if provided *) let help_elements = List.filter_map (fun x -> x) [ Option.map (fun text -> El.p ~at:[classes_attr (Tailwind.Css.tw [ Tailwind.Typography.(to_class (font_size `Sm)); Tailwind.Color.text (Tailwind.Color.make `Gray ~variant:`V600 ()); Tailwind.Spacing.(to_class (mt (Tailwind.Size.rem 0.25))); ])] [El.txt text] ) field.helper_text; Option.map (fun text -> El.p ~at:[classes_attr (Tailwind.Css.tw [ Tailwind.Typography.(to_class (font_size `Sm)); Tailwind.Color.text (Tailwind.Color.make `Red ~variant:`V600 ()); Tailwind.Spacing.(to_class (mt (Tailwind.Size.rem 0.25))); ])] [El.txt text] ) field.error_text; ] in (* Wrap everything in a container *) match help_elements with | [] -> label_element | _ -> El.div ([label_element] @ help_elements) let group ?classes ~fields () = let group_classes = Tailwind.Css.tw [ Tailwind.Display.grid; Tailwind.Layout.w_full; Tailwind.Spacing.(to_class (gap `All (Tailwind.Size.rem 1.0))); (match classes with Some c -> c | None -> Tailwind.Css.empty); ] in let field_elements = List.map to_html fields in El.div ~at:[classes_attr group_classes] field_elements let form ?action ?method_ ?classes ?attributes ~fields ?submit () = let form_classes = Tailwind.Css.tw [ Tailwind.Display.grid; Tailwind.Layout.w_full; Tailwind.Spacing.(to_class (gap `All (Tailwind.Size.rem 1.5))); (match classes with Some c -> c | None -> Tailwind.Css.empty); ] in let base_attrs = [classes_attr form_classes] in let optional_attrs = List.filter_map (fun x -> x) [ Option.map At.action action; (match method_ with | Some `Get -> Some (At.method' "get") | Some `Post -> Some (At.method' "post") | None -> None); ] in let custom_attrs = match attributes with | Some attrs -> List.map (fun (k, v) -> At.v k v) attrs | None -> [] in let all_attrs = base_attrs @ optional_attrs @ custom_attrs in let field_elements = List.map to_html fields in let submit_element = match submit with | Some btn -> [Button.to_html btn] | None -> [] in El.form ~at:all_attrs (field_elements @ submit_element)