Tailwind classes in OCaml
1open Htmlit
2
3(** Card variants *)
4type variant =
5 | Default
6 | Outlined
7 | Elevated
8 | Flat
9
10(** Card configuration *)
11type t = {
12 variant: variant;
13 header: El.html option;
14 footer: El.html option;
15 image: El.html option;
16 padding: bool;
17 hoverable: bool;
18 clickable: bool;
19 classes: Tailwind.t option;
20 attributes: (string * string) list;
21 children: El.html list;
22}
23
24let classes_attr tailwind_classes =
25 At.class' (Tailwind.to_string tailwind_classes)
26
27let base_card_classes = Tailwind.tw [
28 Tailwind.Effects.rounded_lg;
29 Tailwind.Color.bg Tailwind.Color.white;
30 Tailwind.Color.text (Tailwind.Color.make `Gray ~variant:`V900 ());
31]
32
33let variant_classes = function
34 | Default -> Tailwind.tw [
35 Tailwind.Effects.border;
36 Tailwind.Color.border (Tailwind.Color.make `Gray ~variant:`V200 ());
37 Tailwind.Effects.shadow_sm;
38 ]
39 | Outlined -> Tailwind.tw [
40 Tailwind.Effects.border_2;
41 Tailwind.Color.border (Tailwind.Color.make `Gray ~variant:`V300 ());
42 ]
43 | Elevated -> Tailwind.tw [
44 Tailwind.Effects.shadow_lg;
45 Tailwind.Effects.border;
46 Tailwind.Color.border (Tailwind.Color.make `Gray ~variant:`V200 ());
47 ]
48 | Flat -> Tailwind.tw [
49 Tailwind.Color.bg (Tailwind.Color.make `Gray ~variant:`V50 ());
50 ]
51
52let make ?variant ?header ?footer ?image ?(padding=true) ?(hoverable=false) ?(clickable=false) ?classes ?attributes ~children () = {
53 variant = (match variant with Some v -> v | None -> Default);
54 header;
55 footer;
56 image;
57 padding;
58 hoverable;
59 clickable;
60 classes;
61 attributes = (match attributes with Some a -> a | None -> []);
62 children;
63}
64
65let to_html card =
66 let card_classes = Tailwind.tw [
67 base_card_classes;
68 variant_classes card.variant;
69 (if card.padding then Tailwind.Spacing.(to_class (p (Tailwind.Size.rem 1.5))) else Tailwind.Css.empty);
70 (if card.hoverable then Tailwind.Variants.hover (Tailwind.Effects.shadow_md) else Tailwind.Css.empty);
71 (if card.clickable then Tailwind.tw [
72 Tailwind.Css.make "cursor-pointer";
73 Tailwind.Variants.hover (Tailwind.Effects.shadow_md);
74 ] else Tailwind.Css.empty);
75 (match card.classes with Some c -> c | None -> Tailwind.Css.empty);
76 ] in
77
78 let base_attrs = [classes_attr card_classes] in
79 let custom_attrs = List.map (fun (k, v) -> At.v k v) card.attributes in
80 let all_attrs = base_attrs @ custom_attrs in
81
82 let content = List.filter_map (fun x -> x) [
83 card.image;
84 card.header;
85 Some (El.div card.children);
86 card.footer;
87 ] in
88
89 El.div ~at:all_attrs content
90
91(** Card header section *)
92let header ?classes ?title ?subtitle ?actions ~children () =
93 let header_classes = Tailwind.tw [
94 Tailwind.Display.flex;
95 Tailwind.Flexbox.(to_class (direction `Col));
96 Tailwind.Spacing.(to_class (p (Tailwind.Size.rem 1.5)));
97 (match classes with Some c -> c | None -> Tailwind.Css.empty);
98 ] in
99
100 let title_element = match title with
101 | Some t -> Some (El.h3 ~at:[classes_attr (Tailwind.tw [
102 Tailwind.Typography.(to_class (font_size `Lg));
103 Tailwind.Typography.(to_class (font_weight `Semibold));
104 Tailwind.Spacing.(to_class (mb (Tailwind.Size.rem 0.5)));
105 ])] [El.txt t])
106 | None -> None
107 in
108
109 let subtitle_element = match subtitle with
110 | Some s -> Some (El.p ~at:[classes_attr (Tailwind.tw [
111 Tailwind.Typography.(to_class (font_size `Sm));
112 Tailwind.Color.text (Tailwind.Color.make `Gray ~variant:`V600 ());
113 ])] [El.txt s])
114 | None -> None
115 in
116
117 let content = List.filter_map (fun x -> x) [
118 title_element;
119 subtitle_element;
120 (if children = [] then None else Some (El.div children));
121 actions;
122 ] in
123
124 El.div ~at:[classes_attr header_classes] content
125
126(** Card body section *)
127let body ?classes ~children () =
128 let body_classes = Tailwind.tw [
129 Tailwind.Spacing.(to_class (p (Tailwind.Size.rem 1.5)));
130 Tailwind.Spacing.(to_class (pt (Tailwind.Size.zero)));
131 (match classes with Some c -> c | None -> Tailwind.Css.empty);
132 ] in
133
134 El.div ~at:[classes_attr body_classes] children
135
136(** Card footer section *)
137let footer ?classes ?actions ~children () =
138 let footer_classes = Tailwind.tw [
139 Tailwind.Display.flex;
140 Tailwind.Flexbox.(to_class (align_items `Center));
141 Tailwind.Spacing.(to_class (p (Tailwind.Size.rem 1.5)));
142 Tailwind.Spacing.(to_class (pt (Tailwind.Size.zero)));
143 (match classes with Some c -> c | None -> Tailwind.Css.empty);
144 ] in
145
146 let content = children @ (match actions with Some a -> a | None -> []) in
147 El.div ~at:[classes_attr footer_classes] content
148
149(** Card image section *)
150let image ?classes ?alt ?(cover=true) ~src () =
151 let img_classes = Tailwind.tw [
152 Tailwind.Layout.w_full;
153 Tailwind.Layout.(to_class (height (Tailwind.Size.rem 12.0)));
154 (if cover then Tailwind.Layout.(to_class (object_fit `Cover)) else Tailwind.Css.empty);
155 Tailwind.Effects.(to_class (rounded `Top `Lg));
156 (match classes with Some c -> c | None -> Tailwind.Css.empty);
157 ] in
158
159 El.img ~at:[
160 classes_attr img_classes;
161 At.src src;
162 At.alt (match alt with Some a -> a | None -> "");
163 ] ()
164
165(** Create a card grid *)
166let grid ?cols ?gap ?classes ~cards () =
167 let base_classes = [Tailwind.Display.grid] in
168
169 let col_classes = match cols with
170 | Some col_list -> List.fold_left (fun acc col ->
171 match col with
172 | `Xs n -> acc @ [Tailwind.Grid.(to_class (template_cols (`Cols n)))]
173 | `Sm n -> acc @ [Tailwind.Responsive.(to_class (at_breakpoint `Sm (Tailwind.Grid.(to_class (template_cols (`Cols n))))))]
174 | `Md n -> acc @ [Tailwind.Responsive.(to_class (at_breakpoint `Md (Tailwind.Grid.(to_class (template_cols (`Cols n))))))]
175 | `Lg n -> acc @ [Tailwind.Responsive.(to_class (at_breakpoint `Lg (Tailwind.Grid.(to_class (template_cols (`Cols n))))))]
176 | `Xl n -> acc @ [Tailwind.Responsive.(to_class (at_breakpoint `Xl (Tailwind.Grid.(to_class (template_cols (`Cols n))))))]
177 ) [] col_list
178 | None -> [Tailwind.Grid.(to_class (template_cols (`Cols 1)))]
179 in
180
181 let gap_classes = match gap with
182 | Some g -> [Tailwind.Spacing.(to_class (gap `All g))]
183 | None -> []
184 in
185
186 let grid_classes = Tailwind.tw (base_classes @ col_classes @ gap_classes @
187 (match classes with Some c -> [c] | None -> [])) in
188
189 let card_elements = List.map to_html cards in
190 El.div ~at:[classes_attr grid_classes] card_elements