123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324(** Theme scheme configuration for customizing CSS output.
A scheme defines theme overrides that affect how utilities generate CSS.
This allows matching Tailwind's test expectations which use custom [@theme]
definitions with hex colors and explicit spacing variables. *)moduleCss=Cascade.Css(** Color value - either hex string or oklch components *)typecolor_value=|Hexofstring(** e.g., "#ef4444" *)|Oklchof{l:float;c:float;h:float}(** e.g., oklch(63.7% 0.237 25.331) *)typecustom_variant={values:(string*string)list;template:string}(** A [matchVariant]-registered variant: a value map (including a [DEFAULT]
entry under the key [""]) and a selector template containing [{}] where the
resolved value is substituted. *)typet={colors:(string*color_value)list;(** Color overrides. Key is color name like "red-500". When a color is
Hex, opacity modifiers use hex+alpha fallback. When Oklch, opacity
modifiers use color-mix fallback. *)spacing:(int*Css.length)list;(** Explicit spacing variables. Key is the multiplier (e.g., 4 for
--spacing-4). When defined, utilities use var(--spacing-N) instead of
calc(var(--spacing) * N). *)radius:(string*Css.length)list;(** Explicit radius variables. Key is the radius name (e.g., "none",
"full", "sm"). When defined, utilities use var(--radius-NAME) instead
of raw values. *)default_ring_width:int;(** Default ring width in pixels for bare [ring] utility. Corresponds to
Tailwind's [@theme \{ --default-ring-width: Npx \}]. Default: 1. *)default_border_width:int;(** Default border width in pixels for bare [border] utility. Corresponds
to Tailwind's [@theme \{ --default-border-width: Npx \}]. Default: 1.
*)default_outline_width:int;(** Default outline width in pixels for bare [outline] utility.
Corresponds to Tailwind's [@theme \{ --default-outline-width: Npx \}].
Default: 1. *)breakpoints:(string*float)list;(** Explicit breakpoint values in px. Key is breakpoint name (e.g., "sm").
When defined, responsive media queries use [@media (min-width: Xpx)]
instead of rem-based values. *)token_overrides:(string*string)list;inline_tokens:stringlist;reference_tokens:stringlist;reference_theme:bool;static_tokens:stringlist;static_theme:bool;important:bool;prefix:stringoption;(** Per-render theme token overrides (from a [@theme] block). Key is the
variable name without the leading [--] (e.g. "text-shadow-2xs"), value
is the CSS string. Threaded replacement for the global
[Var.theme_value_overrides]. *)custom_variants:(string*custom_variant)list;(** The [@custom-variant]s this [@theme] declared as a value map plus a
selector template. *)container_variants:(string*Css.Container.t)list;(** The [@custom-variant]s this [@theme] declared with a container-query
body. Kept apart from {!custom_variants} because the condition is
structural, so the [not-] prefix can negate it soundly. *)}(** Theme scheme configuration *)moduleTokens=Map.Make(String)(** Lock-free snapshot of the static v4.3.1 theme-token catalog. Utilities
publish their baseline entries once at module initialisation; per-render
values remain in {!t.token_overrides}. *)letdefault_tokens=Atomic.makeTokens.emptyletrecregister_default_tokennamecss=letcurrent=Atomic.getdefault_tokensinletnext=Tokens.addnamecsscurrentinifnot(Atomic.compare_and_setdefault_tokenscurrentnext)thenregister_default_tokennamecsslettoken_defaultname=Tokens.find_optname(Atomic.getdefault_tokens)letall_default_tokens()=Tokens.bindings(Atomic.getdefault_tokens)(** Default scheme - uses oklch colors and calc-based spacing (matches Tailwind
v4 default) *)letdefault:t={colors=[];spacing=[];radius=[];default_ring_width=1;default_border_width=1;default_outline_width=1;breakpoints=[];token_overrides=[];inline_tokens=[];reference_tokens=[];reference_theme=false;static_tokens=[];static_theme=false;important=false;prefix=None;custom_variants=[];container_variants=[];}letppt=Pp.str["{colors=";Pp.int(List.lengtht.colors);"; spacing=";Pp.int(List.lengtht.spacing);"; radius=";Pp.int(List.lengtht.radius);"; ring=";Pp.intt.default_ring_width;"; border=";Pp.intt.default_border_width;"; outline=";Pp.intt.default_outline_width;"; breakpoints=";Pp.int(List.lengtht.breakpoints);"}";](** Lookup a color in the scheme *)letcolorschemename=List.assoc_optnamescheme.colors(** Lookup a spacing value in the scheme *)letspacingschemen=List.assoc_optnscheme.spacing(** Check if a color is defined as hex in the scheme *)letis_hex_colorschemename=matchcolorschemenamewithSome(Hex_)->true|_->false(** Get hex value for a color if defined as hex *)lethex_colorschemename=matchcolorschemenamewithSome(Hexh)->Someh|_->None(** Check if spacing has an explicit variable *)lethas_explicit_spacingschemen=Option.is_some(spacingschemen)(** Lookup a radius value in the scheme *)letradiusschemename=List.assoc_optnamescheme.radius(** Check if radius has an explicit variable *)lethas_explicit_radiusschemename=Option.is_some(radiusschemename)(** Lookup a breakpoint px value in the scheme *)letbreakpointschemename=List.assoc_optnamescheme.breakpoints(* Tailwind reads [--name: initial] in a [@theme] block as "remove this token",
and [--namespace-*: initial] as "remove the whole namespace", so a candidate
that needed the token stops resolving. *)letremoved_value="initial"(* Scales whose names begin with another scale's name and are not part of it:
[--font-*: initial] resets the font families, not the weights or the sizes.
Mirrors Tailwind's own [ignoredThemeKeyMap]. *)letnested_scales=[("font",["font-weight";"font-size"]);("inset",["inset-shadow";"inset-ring"]);("text",["text-color";"text-decoration-color";"text-decoration-thickness";"text-indent";"text-shadow";"text-underline-offset";]);("grid-column",["grid-column-start";"grid-column-end"]);("grid-row",["grid-row-start";"grid-row-end"]);]letin_nested_scalenamespacename=List.exists(funnested->String.equalnamenested||String.starts_with~prefix:(nested^"-")name)(Option.value~default:[](List.assoc_optnamespacenested_scales))letclears_one_namespacenamekey=String.ends_with~suffix:"-*"key&&String.lengthkey>2&&letnamespace=String.subkey0(String.lengthkey-2)inString.starts_with~prefix:namespacename&¬(in_nested_scalenamespacename)(* [--ns-*: initial] resets one namespace; the bare [--*: initial] names no
namespace at all and resets the whole theme. *)letclears_namespacename(key,value)=String.equalvalueremoved_value&&(String.equalkey"*"||clears_one_namespacenamekey)(** Whether a [@theme] block removed [name], either outright or by resetting the
namespace it belongs to. A token the block goes on to declare survives its
own namespace reset. *)letis_removedschemename=matchList.assoc_optnamescheme.token_overrideswith|Somevalue->String.equalvalueremoved_value|None->List.exists(clears_namespacename)scheme.token_overrides(** Lookup a per-render theme token override (from a [@theme] block). *)lettoken_overrideschemename=ifis_removedschemenamethenNoneelseList.assoc_optnamescheme.token_overrides(** [theme_value theme name] looks up a per-render token override from the
optionally-threaded [theme] ([None] when no theme is threaded). Threaded
replacement for the global [Var.theme_value]. *)letor_defaulttheme=Option.value~defaultthemelettheme_valuethemename=Option.bindtheme(funs->token_overridesname)(** Resolve a theme token: override (if any) else the registered default. A
token the [@theme] block removed resolves to nothing, default or not. *)lettokenschemename=matchtoken_overrideschemenamewith|Some_asv->v|None->ifis_removedschemenamethenNoneelsetoken_defaultnameletremoves_tokensscheme=List.exists(fun(_,value)->String.equalvalueremoved_value)scheme.token_overrides(* A name that was a token before the block took it away: [--tw-shadow] is
nobody's token, so the bare [--*: initial] does not reach it. *)letis_removed_tokenschemename=is_removedschemename&&(Option.is_some(token_defaultname)||List.mem_assocnamescheme.token_overrides)(** Lookup the exact CSS length of a breakpoint. Entrypoint [@theme] tokens take
precedence over the legacy px-only record field. A breakpoint the block
removed has none: [initial] reads as a length, and a query built from it is
one no browser honours. *)letbreakpoint_lengthschemename=letkey="breakpoint-"^nameinifis_removedschemekeythenNoneelsematchList.assoc_optkeyscheme.token_overrideswith|Somevalue->Css.parse_lengthvalue|None->Option.map(funpx->(Css.Pxpx:Css.length))(breakpointschemename)(** Every breakpoint the theme defines, keyed by name: the registered defaults,
the [--breakpoint-*] tokens a [@theme] block set, and the legacy px-only
field. *)letall_breakpointsscheme=letprefix="breakpoint-"inletsuffix(key,_)=ifString.starts_with~prefixkeythenSome(String.subkey(String.lengthprefix)(String.lengthkey-String.lengthprefix))elseNoneinletnames=List.filter_mapsuffix(all_default_tokens())@List.filter_mapsuffixscheme.token_overrides@List.mapfstscheme.breakpointsinList.sort_uniqString.comparenames|>List.filter_map(funname->ifis_removedscheme(prefix^name)thenNoneelseletlength=matchbreakpoint_lengthschemenamewith|Some_aslength->length|None->Option.bind(token_default(prefix^name))Css.parse_lengthinOption.map(funlength->(name,length))length)(** [has_breakpoint scheme name] is whether [scheme] still defines the
breakpoint [name], reading the same set as {!all_breakpoints} so a variant
and a container agree on what the theme has. *)lethas_breakpointschemename=List.mem_assocname(all_breakpointsscheme)(* The names a variant may spell, read off the same set: a breakpoint the
[@theme] block removed is not among them, whatever value the removal
wrote. *)letbreakpoint_namesscheme=List.mapfst(all_breakpointsscheme)(** [with_overrides scheme overrides] returns [scheme] with [overrides] applied
on top of any existing token overrides (new entries win). *)letwith_overrides?(inline=[])?(reference=[])?(static=[])schemeoverrides=letbreakpoints=List.fold_left(funbreakpoints(name,value)->letprefix="breakpoint-"inifString.starts_with~prefixnamethenletname=String.subname(String.lengthprefix)(String.lengthname-String.lengthprefix)inmatchCss.parse_lengthvaluewith|Some(Css.Pxpx)->(name,px)::List.remove_assocnamebreakpoints|_->breakpointselsebreakpoints)scheme.breakpointsoverridesin{schemewithbreakpoints;token_overrides=overrides@scheme.token_overrides;inline_tokens=inline@scheme.inline_tokens;reference_tokens=reference@scheme.reference_tokens;static_tokens=static@scheme.static_tokens;}letis_inline_tokenschemename=List.memnamescheme.inline_tokensletis_reference_tokenschemename=List.memnamescheme.reference_tokens||(scheme.reference_theme&¬(List.mem_assocnamescheme.token_overrides))letis_static_tokenschemename=List.memnamescheme.static_tokens