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