(** Parse Tailwind CSS class strings into native style records. Supports ~50 common utility classes covering layout, spacing, sizing, typography, colors, borders, and effects. *) open Sx_native_types (* -- Color palette (Tailwind stone + violet) -- *) let white = { r = 1.0; g = 1.0; b = 1.0; a = 1.0 } let black = { r = 0.0; g = 0.0; b = 0.0; a = 1.0 } let stone_50 = { r = 0.980; g = 0.976; b = 0.973; a = 1.0 } let stone_100 = { r = 0.961; g = 0.953; b = 0.945; a = 1.0 } let stone_200 = { r = 0.906; g = 0.890; b = 0.875; a = 1.0 } let stone_300 = { r = 0.839; g = 0.812; b = 0.788; a = 1.0 } let stone_400 = { r = 0.659; g = 0.616; b = 0.576; a = 1.0 } let stone_500 = { r = 0.471; g = 0.431; b = 0.396; a = 1.0 } let stone_600 = { r = 0.341; g = 0.306; b = 0.275; a = 1.0 } let stone_700 = { r = 0.267; g = 0.231; b = 0.208; a = 1.0 } (* stone_800 is already in sx_native_types *) let stone_900 = { r = 0.106; g = 0.098; b = 0.090; a = 1.0 } let violet_50 = { r = 0.961; g = 0.953; b = 1.0; a = 1.0 } let violet_100 = { r = 0.929; g = 0.906; b = 0.996; a = 1.0 } let violet_200 = { r = 0.867; g = 0.820; b = 0.992; a = 1.0 } let violet_300 = { r = 0.769; g = 0.686; b = 0.984; a = 1.0 } let violet_400 = { r = 0.655; g = 0.525; b = 0.969; a = 1.0 } let violet_500 = { r = 0.545; g = 0.361; b = 0.945; a = 1.0 } let violet_600 = { r = 0.486; g = 0.227; b = 0.929; a = 1.0 } let violet_700 = { r = 0.427; g = 0.176; b = 0.831; a = 1.0 } let violet_800 = { r = 0.357; g = 0.153; b = 0.694; a = 1.0 } let violet_900 = { r = 0.298; g = 0.133; b = 0.576; a = 1.0 } let red_500 = { r = 0.937; g = 0.267; b = 0.267; a = 1.0 } let red_600 = { r = 0.863; g = 0.145; b = 0.145; a = 1.0 } let blue_500 = { r = 0.231; g = 0.510; b = 0.965; a = 1.0 } let blue_600 = { r = 0.145; g = 0.388; b = 0.922; a = 1.0 } let green_500 = { r = 0.133; g = 0.773; b = 0.369; a = 1.0 } let green_600 = { r = 0.086; g = 0.635; b = 0.290; a = 1.0 } let amber_500 = { r = 0.961; g = 0.718; b = 0.078; a = 1.0 } (* -- Spacing scale (Tailwind: 1 unit = 4px) -- *) let spacing n = float_of_int n *. 4.0 (* -- Font sizes (Tailwind) -- *) let font_size_of = function | "text-xs" -> 12. | "text-sm" -> 14. | "text-base" -> 16. | "text-lg" -> 18. | "text-xl" -> 20. | "text-2xl" -> 24. | "text-3xl" -> 30. | "text-4xl" -> 36. | "text-5xl" -> 48. | _ -> 16. (* -- Parse a single Tailwind class, mutating a style -- *) let parse_spacing_value s = (* Extract numeric value from strings like "p-4", "gap-2" *) match int_of_string_opt s with | Some n -> spacing n | None -> 0. let bg_color_of cls = match cls with | "bg-white" -> Some white | "bg-black" -> Some black | "bg-stone-50" -> Some stone_50 | "bg-stone-100" -> Some stone_100 | "bg-stone-200" -> Some stone_200 | "bg-stone-300" -> Some stone_300 | "bg-stone-400" -> Some stone_400 | "bg-stone-500" -> Some stone_500 | "bg-stone-600" -> Some stone_600 | "bg-stone-700" -> Some stone_700 | "bg-stone-800" -> Some stone_800 | "bg-stone-900" -> Some stone_900 | "bg-violet-50" -> Some violet_50 | "bg-violet-100" -> Some violet_100 | "bg-violet-200" -> Some violet_200 | "bg-violet-300" -> Some violet_300 | "bg-violet-400" -> Some violet_400 | "bg-violet-500" -> Some violet_500 | "bg-violet-600" -> Some violet_600 | "bg-violet-700" -> Some violet_700 | "bg-violet-800" -> Some violet_800 | "bg-violet-900" -> Some violet_900 | "bg-red-500" -> Some red_500 | "bg-red-600" -> Some red_600 | "bg-blue-500" -> Some blue_500 | "bg-blue-600" -> Some blue_600 | "bg-green-500" -> Some green_500 | "bg-green-600" -> Some green_600 | "bg-amber-500" -> Some amber_500 | _ -> None let text_color_of cls = match cls with | "text-white" -> Some white | "text-black" -> Some black | "text-stone-50" -> Some stone_50 | "text-stone-100" -> Some stone_100 | "text-stone-200" -> Some stone_200 | "text-stone-300" -> Some stone_300 | "text-stone-400" -> Some stone_400 | "text-stone-500" -> Some stone_500 | "text-stone-600" -> Some stone_600 | "text-stone-700" -> Some stone_700 | "text-stone-800" -> Some stone_800 | "text-stone-900" -> Some stone_900 | "text-violet-50" -> Some violet_50 | "text-violet-100" -> Some violet_100 | "text-violet-200" -> Some violet_200 | "text-violet-300" -> Some violet_300 | "text-violet-400" -> Some violet_400 | "text-violet-500" -> Some violet_500 | "text-violet-600" -> Some violet_600 | "text-violet-700" -> Some violet_700 | "text-violet-800" -> Some violet_800 | "text-violet-900" -> Some violet_900 | "text-red-500" -> Some red_500 | "text-red-600" -> Some red_600 | "text-blue-500" -> Some blue_500 | "text-blue-600" -> Some blue_600 | "text-green-500" -> Some green_500 | "text-green-600" -> Some green_600 | "text-amber-500" -> Some amber_500 | _ -> None let border_color_of cls = match cls with | "border-stone-100" -> Some stone_100 | "border-stone-200" -> Some stone_200 | "border-stone-300" -> Some stone_300 | "border-violet-200" -> Some violet_200 | "border-violet-300" -> Some violet_300 | "border-white" -> Some white | _ -> None (** Apply a single Tailwind class to a style, returning the updated style. *) let apply_class (s : style) (cls : string) : style = (* Layout *) if cls = "flex" then { s with display = `Flex; flex_direction = `Row } else if cls = "flex-col" then { s with display = `Flex; flex_direction = `Column } else if cls = "flex-row" then { s with display = `Flex; flex_direction = `Row } else if cls = "block" then { s with display = `Block } else if cls = "hidden" then { s with display = `None } else if cls = "items-center" then { s with align_items = `Center } else if cls = "items-start" then { s with align_items = `Start } else if cls = "items-end" then { s with align_items = `End } else if cls = "items-stretch" then { s with align_items = `Stretch } else if cls = "justify-center" then { s with justify_content = `Center } else if cls = "justify-between" then { s with justify_content = `Between } else if cls = "justify-start" then { s with justify_content = `Start } else if cls = "justify-end" then { s with justify_content = `End } else if cls = "flex-grow" || cls = "grow" then { s with flex_grow = 1. } (* Gap *) else if String.length cls > 4 && String.sub cls 0 4 = "gap-" then let n = String.sub cls 4 (String.length cls - 4) in { s with gap = parse_spacing_value n } (* Padding *) else if String.length cls > 2 && String.sub cls 0 2 = "p-" && cls.[2] <> 'x' && cls.[2] <> 'y' && cls.[2] <> 'l' && cls.[2] <> 'r' && cls.[2] <> 't' && cls.[2] <> 'b' then let n = String.sub cls 2 (String.length cls - 2) in let v = parse_spacing_value n in { s with padding = { top = v; right = v; bottom = v; left = v } } else if String.length cls > 3 && String.sub cls 0 3 = "px-" then let n = String.sub cls 3 (String.length cls - 3) in let v = parse_spacing_value n in { s with padding = { s.padding with left = v; right = v } } else if String.length cls > 3 && String.sub cls 0 3 = "py-" then let n = String.sub cls 3 (String.length cls - 3) in let v = parse_spacing_value n in { s with padding = { s.padding with top = v; bottom = v } } else if String.length cls > 3 && String.sub cls 0 3 = "pt-" then let n = String.sub cls 3 (String.length cls - 3) in { s with padding = { s.padding with top = parse_spacing_value n } } else if String.length cls > 3 && String.sub cls 0 3 = "pb-" then let n = String.sub cls 3 (String.length cls - 3) in { s with padding = { s.padding with bottom = parse_spacing_value n } } else if String.length cls > 3 && String.sub cls 0 3 = "pl-" then let n = String.sub cls 3 (String.length cls - 3) in { s with padding = { s.padding with left = parse_spacing_value n } } else if String.length cls > 3 && String.sub cls 0 3 = "pr-" then let n = String.sub cls 3 (String.length cls - 3) in { s with padding = { s.padding with right = parse_spacing_value n } } (* Margin *) else if String.length cls > 2 && String.sub cls 0 2 = "m-" && cls.[2] <> 'x' && cls.[2] <> 'y' && cls.[2] <> 'l' && cls.[2] <> 'r' && cls.[2] <> 't' && cls.[2] <> 'b' then let n = String.sub cls 2 (String.length cls - 2) in let v = parse_spacing_value n in { s with margin = { top = v; right = v; bottom = v; left = v } } else if String.length cls > 3 && String.sub cls 0 3 = "mx-" then let n = String.sub cls 3 (String.length cls - 3) in let v = parse_spacing_value n in { s with margin = { s.margin with left = v; right = v } } else if String.length cls > 3 && String.sub cls 0 3 = "my-" then let n = String.sub cls 3 (String.length cls - 3) in let v = parse_spacing_value n in { s with margin = { s.margin with top = v; bottom = v } } else if String.length cls > 3 && String.sub cls 0 3 = "mt-" then let n = String.sub cls 3 (String.length cls - 3) in { s with margin = { s.margin with top = parse_spacing_value n } } else if String.length cls > 3 && String.sub cls 0 3 = "mb-" then let n = String.sub cls 3 (String.length cls - 3) in { s with margin = { s.margin with bottom = parse_spacing_value n } } (* Sizing *) else if cls = "w-full" then { s with width = `Full } else if cls = "h-full" then { s with height = `Full } else if String.length cls > 2 && String.sub cls 0 2 = "w-" then let n = String.sub cls 2 (String.length cls - 2) in (match int_of_string_opt n with | Some v -> { s with width = `Px (float_of_int v *. 4.) } | None -> s) else if String.length cls > 2 && String.sub cls 0 2 = "h-" then let n = String.sub cls 2 (String.length cls - 2) in (match int_of_string_opt n with | Some v -> { s with height = `Px (float_of_int v *. 4.) } | None -> s) (* Typography *) else if cls = "font-bold" then { s with font_weight = `Bold } else if cls = "font-semibold" then { s with font_weight = `Bold } else if cls = "font-normal" then { s with font_weight = `Normal } else if cls = "italic" then { s with font_style = `Italic } else if cls = "font-mono" then { s with font_family = `Mono } else if String.length cls >= 5 && String.sub cls 0 5 = "text-" then (* Could be text color or text size *) let rest = String.sub cls 5 (String.length cls - 5) in if rest = "xs" || rest = "sm" || rest = "base" || rest = "lg" || rest = "xl" || rest = "2xl" || rest = "3xl" || rest = "4xl" || rest = "5xl" then { s with font_size = font_size_of cls } else (match text_color_of cls with | Some c -> { s with text_color = c } | None -> s) (* Background *) else if String.length cls >= 3 && String.sub cls 0 3 = "bg-" then (match bg_color_of cls with | Some c -> { s with bg_color = Some c } | None -> s) (* Borders *) else if cls = "rounded" then { s with border_radius = 4. } else if cls = "rounded-md" then { s with border_radius = 6. } else if cls = "rounded-lg" then { s with border_radius = 8. } else if cls = "rounded-xl" then { s with border_radius = 12. } else if cls = "rounded-2xl" then { s with border_radius = 16. } else if cls = "rounded-full" then { s with border_radius = 9999. } else if cls = "border" then { s with border_width = 1.; border_color = (if s.border_color = None then Some stone_200 else s.border_color) } else if cls = "border-2" then { s with border_width = 2.; border_color = (if s.border_color = None then Some stone_200 else s.border_color) } else if String.length cls >= 7 && String.sub cls 0 7 = "border-" then (match border_color_of cls with | Some c -> { s with border_color = Some c; border_width = (if s.border_width = 0. then 1. else s.border_width) } | None -> s) (* Shadow *) else if cls = "shadow" then { s with shadow = `Sm } else if cls = "shadow-md" then { s with shadow = `Md } else if cls = "shadow-lg" then { s with shadow = `Md } (* Overflow *) else if cls = "overflow-hidden" then { s with overflow_hidden = true } else s (* unknown class: ignore *) (** Parse a space-separated Tailwind class string into a [style]. *) let parse_classes ?(base = default_style) (classes : string) : style = let parts = String.split_on_char ' ' classes in List.fold_left (fun s cls -> let cls = String.trim cls in if cls = "" then s else apply_class s cls ) base parts