open Htmlit type variant = [ `Primary | `Secondary | `Outline | `Ghost | `Link ] type size = [ `Sm | `Default | `Lg | `Icon ] type state = [ `Default | `Loading | `Disabled ] type t = { variant: variant; size: size; state: state; icon: El.html option; icon_position: [`Left | `Right]; classes: Tailwind.t option; attributes: (string * string) list; children: El.html list; } let classes_attr tailwind_classes = At.class' (Tailwind.to_string tailwind_classes) let base_button_classes = Tailwind.Css.tw [ Tailwind.Display.inline_flex; Tailwind.Flexbox.(to_class (align_items `Center)); Tailwind.Flexbox.(to_class (justify `Center)); Tailwind.Effects.rounded_md; Tailwind.Typography.(to_class (font_size `Sm)); Tailwind.Typography.(to_class (font_weight `Medium)); Tailwind.Css.make "ring-offset-background"; Tailwind.Effects.transition `Colors; Tailwind.Css.make "focus-visible:outline-none"; Tailwind.Css.make "focus-visible:ring-2"; Tailwind.Css.make "focus-visible:ring-ring"; Tailwind.Css.make "focus-visible:ring-offset-2"; Tailwind.Css.make "disabled:pointer-events-none"; Tailwind.Css.make "disabled:opacity-50"; ] let variant_classes = function | `Primary -> Tailwind.Css.tw [ Tailwind.Color.bg (Tailwind.Color.make `Blue ~variant:`V600 ()); Tailwind.Color.text Tailwind.Color.white; Tailwind.Variants.hover (Tailwind.Color.bg (Tailwind.Color.make `Blue ~variant:`V700 ())); ] | `Secondary -> Tailwind.Css.tw [ Tailwind.Color.bg (Tailwind.Color.make `Gray ~variant:`V200 ()); Tailwind.Color.text (Tailwind.Color.make `Gray ~variant:`V900 ()); Tailwind.Variants.hover (Tailwind.Color.bg (Tailwind.Color.make `Gray ~variant:`V300 ())); ] | `Outline -> Tailwind.Css.tw [ Tailwind.Effects.border; Tailwind.Color.border (Tailwind.Color.make `Gray ~variant:`V300 ()); Tailwind.Color.bg Tailwind.Color.transparent; Tailwind.Variants.hover (Tailwind.Color.bg (Tailwind.Color.make `Gray ~variant:`V100 ())); ] | `Ghost -> Tailwind.Css.tw [ Tailwind.Color.bg Tailwind.Color.transparent; Tailwind.Variants.hover (Tailwind.Color.bg (Tailwind.Color.make `Gray ~variant:`V100 ())); ] | `Link -> Tailwind.Css.tw [ Tailwind.Color.bg Tailwind.Color.transparent; Tailwind.Color.text (Tailwind.Color.make `Blue ~variant:`V600 ()); Tailwind.Css.make "underline-offset-4"; Tailwind.Variants.hover (Tailwind.Css.make "underline"); ] let size_classes = function | `Default -> Tailwind.Css.tw [ Tailwind.Spacing.(to_class (px (Tailwind.Size.rem 1.0))); Tailwind.Spacing.(to_class (py (Tailwind.Size.rem 0.5))); ] | `Sm -> Tailwind.Css.tw [ Tailwind.Spacing.(to_class (px (Tailwind.Size.rem 0.75))); Tailwind.Spacing.(to_class (py (Tailwind.Size.rem 0.375))); ] | `Lg -> Tailwind.Css.tw [ Tailwind.Spacing.(to_class (px (Tailwind.Size.rem 2.0))); Tailwind.Spacing.(to_class (py (Tailwind.Size.rem 0.75))); ] | `Icon -> Tailwind.Css.tw [ Tailwind.Layout.(to_class (width (Tailwind.Size.rem 2.5))); Tailwind.Layout.(to_class (height (Tailwind.Size.rem 2.5))); ] let state_classes = function | `Default -> Tailwind.Css.empty | `Loading -> Tailwind.Css.tw [ Tailwind.Css.make "cursor-not-allowed"; Tailwind.Effects.(to_class (opacity 75)); ] | `Disabled -> Tailwind.Css.tw [ Tailwind.Css.make "cursor-not-allowed"; Tailwind.Effects.(to_class (opacity 50)); ] let make ?(variant=`Primary) ?(size=`Default) ?(state=`Default) ?icon ?(icon_position=`Left) ?classes ?attributes ~children () = { variant; size; state; icon; icon_position; classes; attributes = (match attributes with Some a -> a | None -> []); children; } let to_html button = let button_classes = Tailwind.Css.tw [ base_button_classes; variant_classes button.variant; size_classes button.size; state_classes button.state; (match button.classes with Some c -> c | None -> Tailwind.Css.empty); ] in let base_attrs = [classes_attr button_classes] in let state_attrs = match button.state with | `Disabled -> [At.disabled] | _ -> [] in let custom_attrs = List.map (fun (k, v) -> At.v k v) button.attributes in let all_attrs = base_attrs @ state_attrs @ custom_attrs in let loading_spinner = match button.state with | `Loading -> [El.span ~at:[classes_attr (Tailwind.Css.tw [ Tailwind.Css.make "animate-spin"; Tailwind.Spacing.(to_class (mr (Tailwind.Size.rem 0.5))); ])] [El.txt "⟳"]] | _ -> [] in let icon_element = match button.icon with | Some icon -> [icon] | None -> [] in let content = match button.icon_position with | `Left -> loading_spinner @ icon_element @ button.children | `Right -> loading_spinner @ button.children @ icon_element in El.button ~at:all_attrs content (* Shorthand functions *) let primary ?size ?state ?icon ?classes ~children () = let btn = make ~variant:`Primary ?size ?state ?icon ?classes ~children () in to_html btn let secondary ?size ?state ?icon ?classes ~children () = let btn = make ~variant:`Secondary ?size ?state ?icon ?classes ~children () in to_html btn let outline ?size ?state ?icon ?classes ~children () = let btn = make ~variant:`Outline ?size ?state ?icon ?classes ~children () in to_html btn let ghost ?size ?state ?icon ?classes ~children () = let btn = make ~variant:`Ghost ?size ?state ?icon ?classes ~children () in to_html btn let link ?size ?state ?classes ~children () = let btn = make ~variant:`Link ?size ?state ?classes ~children () in to_html btn