Tailwind classes in OCaml
1open Htmlit
2
3type variant = [ `Primary | `Secondary | `Outline | `Ghost | `Link ]
4type size = [ `Sm | `Default | `Lg | `Icon ]
5type state = [ `Default | `Loading | `Disabled ]
6
7type t = {
8 variant: variant;
9 size: size;
10 state: state;
11 icon: El.html option;
12 icon_position: [`Left | `Right];
13 classes: Tailwind.t option;
14 attributes: (string * string) list;
15 children: El.html list;
16}
17
18let classes_attr tailwind_classes =
19 At.class' (Tailwind.to_string tailwind_classes)
20
21let base_button_classes = Tailwind.Css.tw [
22 Tailwind.Display.inline_flex;
23 Tailwind.Flexbox.(to_class (align_items `Center));
24 Tailwind.Flexbox.(to_class (justify `Center));
25 Tailwind.Effects.rounded_md;
26 Tailwind.Typography.(to_class (font_size `Sm));
27 Tailwind.Typography.(to_class (font_weight `Medium));
28 Tailwind.Css.make "ring-offset-background";
29 Tailwind.Effects.transition `Colors;
30 Tailwind.Css.make "focus-visible:outline-none";
31 Tailwind.Css.make "focus-visible:ring-2";
32 Tailwind.Css.make "focus-visible:ring-ring";
33 Tailwind.Css.make "focus-visible:ring-offset-2";
34 Tailwind.Css.make "disabled:pointer-events-none";
35 Tailwind.Css.make "disabled:opacity-50";
36]
37
38let variant_classes = function
39 | `Primary -> Tailwind.Css.tw [
40 Tailwind.Color.bg (Tailwind.Color.make `Blue ~variant:`V600 ());
41 Tailwind.Color.text Tailwind.Color.white;
42 Tailwind.Variants.hover (Tailwind.Color.bg (Tailwind.Color.make `Blue ~variant:`V700 ()));
43 ]
44 | `Secondary -> Tailwind.Css.tw [
45 Tailwind.Color.bg (Tailwind.Color.make `Gray ~variant:`V200 ());
46 Tailwind.Color.text (Tailwind.Color.make `Gray ~variant:`V900 ());
47 Tailwind.Variants.hover (Tailwind.Color.bg (Tailwind.Color.make `Gray ~variant:`V300 ()));
48 ]
49 | `Outline -> Tailwind.Css.tw [
50 Tailwind.Effects.border;
51 Tailwind.Color.border (Tailwind.Color.make `Gray ~variant:`V300 ());
52 Tailwind.Color.bg Tailwind.Color.transparent;
53 Tailwind.Variants.hover (Tailwind.Color.bg (Tailwind.Color.make `Gray ~variant:`V100 ()));
54 ]
55 | `Ghost -> Tailwind.Css.tw [
56 Tailwind.Color.bg Tailwind.Color.transparent;
57 Tailwind.Variants.hover (Tailwind.Color.bg (Tailwind.Color.make `Gray ~variant:`V100 ()));
58 ]
59 | `Link -> Tailwind.Css.tw [
60 Tailwind.Color.bg Tailwind.Color.transparent;
61 Tailwind.Color.text (Tailwind.Color.make `Blue ~variant:`V600 ());
62 Tailwind.Css.make "underline-offset-4";
63 Tailwind.Variants.hover (Tailwind.Css.make "underline");
64 ]
65
66let size_classes = function
67 | `Default -> Tailwind.Css.tw [
68 Tailwind.Spacing.(to_class (px (Tailwind.Size.rem 1.0)));
69 Tailwind.Spacing.(to_class (py (Tailwind.Size.rem 0.5)));
70 ]
71 | `Sm -> Tailwind.Css.tw [
72 Tailwind.Spacing.(to_class (px (Tailwind.Size.rem 0.75)));
73 Tailwind.Spacing.(to_class (py (Tailwind.Size.rem 0.375)));
74 ]
75 | `Lg -> Tailwind.Css.tw [
76 Tailwind.Spacing.(to_class (px (Tailwind.Size.rem 2.0)));
77 Tailwind.Spacing.(to_class (py (Tailwind.Size.rem 0.75)));
78 ]
79 | `Icon -> Tailwind.Css.tw [
80 Tailwind.Layout.(to_class (width (Tailwind.Size.rem 2.5)));
81 Tailwind.Layout.(to_class (height (Tailwind.Size.rem 2.5)));
82 ]
83
84let state_classes = function
85 | `Default -> Tailwind.Css.empty
86 | `Loading -> Tailwind.Css.tw [
87 Tailwind.Css.make "cursor-not-allowed";
88 Tailwind.Effects.(to_class (opacity 75));
89 ]
90 | `Disabled -> Tailwind.Css.tw [
91 Tailwind.Css.make "cursor-not-allowed";
92 Tailwind.Effects.(to_class (opacity 50));
93 ]
94
95let make ?(variant=`Primary) ?(size=`Default) ?(state=`Default) ?icon ?(icon_position=`Left) ?classes ?attributes ~children () = {
96 variant;
97 size;
98 state;
99 icon;
100 icon_position;
101 classes;
102 attributes = (match attributes with Some a -> a | None -> []);
103 children;
104}
105
106let to_html button =
107 let button_classes = Tailwind.Css.tw [
108 base_button_classes;
109 variant_classes button.variant;
110 size_classes button.size;
111 state_classes button.state;
112 (match button.classes with Some c -> c | None -> Tailwind.Css.empty);
113 ] in
114
115 let base_attrs = [classes_attr button_classes] in
116 let state_attrs = match button.state with
117 | `Disabled -> [At.disabled]
118 | _ -> []
119 in
120 let custom_attrs = List.map (fun (k, v) -> At.v k v) button.attributes in
121 let all_attrs = base_attrs @ state_attrs @ custom_attrs in
122
123 let loading_spinner = match button.state with
124 | `Loading -> [El.span ~at:[classes_attr (Tailwind.Css.tw [
125 Tailwind.Css.make "animate-spin";
126 Tailwind.Spacing.(to_class (mr (Tailwind.Size.rem 0.5)));
127 ])] [El.txt "⟳"]]
128 | _ -> []
129 in
130
131 let icon_element = match button.icon with
132 | Some icon -> [icon]
133 | None -> []
134 in
135
136 let content = match button.icon_position with
137 | `Left -> loading_spinner @ icon_element @ button.children
138 | `Right -> loading_spinner @ button.children @ icon_element
139 in
140
141 El.button ~at:all_attrs content
142
143(* Shorthand functions *)
144let primary ?size ?state ?icon ?classes ~children () =
145 let btn = make ~variant:`Primary ?size ?state ?icon ?classes ~children () in
146 to_html btn
147
148let secondary ?size ?state ?icon ?classes ~children () =
149 let btn = make ~variant:`Secondary ?size ?state ?icon ?classes ~children () in
150 to_html btn
151
152let outline ?size ?state ?icon ?classes ~children () =
153 let btn = make ~variant:`Outline ?size ?state ?icon ?classes ~children () in
154 to_html btn
155
156let ghost ?size ?state ?icon ?classes ~children () =
157 let btn = make ~variant:`Ghost ?size ?state ?icon ?classes ~children () in
158 to_html btn
159
160let link ?size ?state ?classes ~children () =
161 let btn = make ~variant:`Link ?size ?state ?classes ~children () in
162 to_html btn