theme.ml1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169(** Shared theme variables for consistent ordering and avoiding conflicts *) module Css = Cascade.Css (** Main ordering scheme: 1. Font families (1) - basic font families 2. Colors (2) - color variables 3. Spacing (3) - spacing variables 4. Breakpoints (4) \- breakpoint variables 5. Containers (5) - container variables 6. Typography (6) - text sizes and other typography 7. Border radius (7) - border radius variables 8. Animation/timing (8) - animation variables 9. Default fonts (9) - default font family variables *) (* Publish a theme variable's built-in default through the token registry. The text comes from the binding the utility itself emits, so [theme(--x)] in a class, [theme()] in a project's CSS and [theme(static)] all read exactly what the theme layer would declare for the token. A registry entry is a token stream rather than a typed value, and the printer leaves its numbers as written, so the shortest spelling is the one to store: it is what the sheet carries in either output mode. *) let register_default var value = let decl = Var.set var value in Scheme.register_default_token (Var.name var) (Css.declaration_value ~minify:true decl) (** {1 Spacing Variables} *) (* Resolve the optionally-threaded theme, defaulting to the base scheme. *) (* Shared spacing variable used across padding, margin, positioning, etc. Tailwind v4 uses a single --spacing: 0.25rem variable and calc() for values. *) let spacing_var = Var.theme Css.Length "spacing" ~runtime:true ~order:(3, 0) (* The base spacing value: 0.25rem *) let spacing_base : Css.length = Rem 0.25 (* Publish the spacing step through the theme-token registry, the way rule.ml publishes the breakpoints, so [theme()] in a project's CSS resolves against the same value the utilities use. *) let () = register_default spacing_var spacing_base (* The spacing step times [n], rendered. Tailwind's v3 [spacing] and [lineHeight] scales are both that product, and v4 keeps no token per step, so a [theme(spacing.4)] has to be computed rather than looked up. *) let spacing_times n = match spacing_base with | Css.Rem v -> Some (Css.Pp.to_string Css.pp_length (Css.Rem (v *. n))) | Css.Px v -> Some (Css.Pp.to_string Css.pp_length (Css.Px (v *. n))) | Css.Em v -> Some (Css.Pp.to_string Css.pp_length (Css.Em (v *. n))) | _ -> None (* Create a spacing variable for explicit spacing values (e.g., --spacing-4) *) let spacing_n_var n = Var.theme Css.Length ("spacing-" ^ Pp.int n) ~order:(3, n) (* The length the theme binds to step [n] outright, as [--spacing-<n>]. Tailwind reads a bare step off that binding first and off the [--spacing] multiplier only when there is none. *) let explicit_spacing scheme n = match Scheme.spacing scheme n with | Some _ as length -> length | None -> Option.bind (Scheme.token scheme ("spacing-" ^ Pp.int n)) Css.parse_length (* Whether the bare step [n] of the spacing scale still resolves: [@theme { --spacing: initial }] removes the multiplier, and every step that relied on it stops being a utility. *) let has_spacing_step ?theme n = let scheme = Scheme.or_default theme in Float.is_integer n && explicit_spacing scheme (int_of_float (Float.abs n)) <> None || Scheme.token scheme (Var.name spacing_var) <> None (* The step the scheme binds outright, as var(--spacing-|n|) with a negative multiplier folded into calc(... * -1). None when the scheme binds no such step, or when the step is not a whole one: only whole steps get a token. *) let explicit_spacing_length ?theme (n : float) = let abs_n = Float.abs n in if not (Float.is_integer abs_n) then None else let abs_n = int_of_float abs_n in match explicit_spacing (Scheme.or_default theme) abs_n with | None -> None | Some explicit_length -> let spacing_n = spacing_n_var abs_n in let decl, spacing_ref = Var.binding spacing_n explicit_length in if n < 0.0 then (* Negative: wrap in calc(... * -1) *) let neg_len : Css.length = Css.Calc (Css.Calc.mul (Css.Calc.length (Css.Var spacing_ref)) (Css.Calc.float (-1.0))) in Some (decl, neg_len) else Some (decl, (Css.Var spacing_ref : Css.length)) (* The step itself: the [var(--spacing)] reference, or the value the project gave the token when it declared it in an [@theme inline] block, which has no declaration of its own for a reference to read. The declaration carried beside it is the dependency carrier the utility attaches either way; an unreferenced runtime one is dropped at the sheet. *) let spacing_step ?theme () : Css.declaration * Css.length = let decl, spacing_ref = Var.binding spacing_var spacing_base in let inline = match theme with | Some t when Scheme.is_inline_token t (Var.name spacing_var) -> Option.bind (Scheme.theme_value theme (Var.name spacing_var)) Css.parse_length | Some _ | None -> None in match inline with | Some length -> (decl, length) | None -> (decl, (Css.Var spacing_ref : Css.length)) (* The spacing step times [n], written out as calc(var(--spacing) * n), or calc(.25rem * n) over an inline token. The product is what a utility emits when the scheme binds no token for the step and the multiplier is not one Tailwind folds. *) let spacing_product ?theme (n : float) : Css.declaration * Css.length = match explicit_spacing_length ?theme n with | Some result -> result | None -> let decl, step = spacing_step ?theme () in let len : Css.length = Css.Calc (Css.Calc.mul (Css.Calc.length step) (Css.Calc.float n)) in (decl, len) (* Create a spacing length value. When scheme has explicit spacing for |n|, returns var(--spacing-|n|) or calc(var(--spacing-|n|) * -1) for negatives. Otherwise returns calc(var(--spacing) * n), with the zero and unit multipliers folded the way Tailwind folds them. *) let spacing_calc ?theme n : Css.declaration * Css.length = match explicit_spacing_length ?theme (float_of_int n) with | Some result -> result | None -> (* Default: calc(var(--spacing) * n). For the unit multiplier we emit a bare var(--spacing) rather than calc(var(--spacing) * 1). This shortcut exists only to match Tailwind core byte-for-byte: the fixture has p-1 producing "padding: var(--spacing)" next to p-4 producing "calc(var(--spacing) * 4)". Without it our output diverges and the examples/parity comparisons flag the difference. Runtime expectation: --spacing must resolve to a single length. That is the spacing-scale contract (the default 0.25rem and any single-value @theme override). The shortcut is NOT sound under a multi-term runtime redefinition such as ".dense { --spacing: 1px + 3px }": bare var(--spacing) then expands to invalid bare math and falls back to the initial value, whereas calc(var(--spacing) * 1) would still compute (4px). We inherit this fragility from Tailwind. cascade must not perform the equivalent calc(var(--spacing)) -> var(--spacing) rewrite, because it optimises arbitrary CSS and cannot assume that contract. The zero step is a plain [0px], as Tailwind emits it: the scale factor makes [calc(var(--spacing) * 0)] zero for any spacing, and only the optimiser can see that once [--spacing] is a literal. *) if n <> 0 && n <> 1 then spacing_product ?theme (float_of_int n) else let decl, step = spacing_step ?theme () in if n = 0 then (decl, (Px 0. : Css.length)) else (decl, step) (* Create a spacing length value for float multipliers like 2.5. For integer values, checks scheme for explicit spacing. Otherwise uses calc. This handles cases like my-2.5 which need calc(var(--spacing) * 2.5). *) let spacing_calc_float ?theme (n : float) : Css.declaration * Css.length = (* Only an integer step can be bound outright by the scheme, and only an integer step folds. *) if Float.is_integer n then spacing_calc ?theme (int_of_float n) else spacing_product ?theme n