//// gg_cn — a pure-Gleam `tailwind-merge`. //// //// Ported from [cnfast](https://github.com/aidenybai/cnfast)'s logic engine //// (itself a faithful reimplementation of `tailwind-merge` v4), this resolves //// conflicting Tailwind utility classes by keeping the last one per conflict //// group: `tw_merge("px-2 px-4") == "px-4"`. //// //// This is the engine behind `gg_base_ui/helpers/cn` (which `gg_ui` and its //// components use): a real `clsx + tailwind-merge`, for any markup that mixes //// raw Tailwind utilities and needs conflict resolution. It is pure Gleam (no //// FFI), so it compiles and behaves identically on JS and the BEAM — which is //// why it can back gg_ui's `cn` without bringing Elixir `tails` into the build. //// //// ## Usage //// //// ```gleam //// import gg_cn //// //// let merge = gg_cn.new() //// merge |> gg_cn.tw_merge("px-2 py-1 px-4") // "py-1 px-4" //// ``` //// //// `new()` builds the class trie once (it is moderately expensive); reuse the //// returned `Merger` across calls rather than rebuilding it per merge. For //// render-time callers, prefer [`default`](#default) — a process-global `Merger` //// built once and reused (what `gg_base_ui/helpers/cn` uses). import gleam/list import gleam/string import global_value import gg_cn/internal/cache import gg_cn/internal/config import gg_cn/internal/merge import gg_cn/internal/validators /// A prepared merge engine: the compiled regexes and built class trie. Build it /// once with [`new`](#new) and reuse it. pub opaque type Merger { Merger(engine: merge.Engine) } /// Build a `Merger` from the baked-in Tailwind v4 configuration. pub fn new() -> Merger { let regexes = validators.compile() let cfg = config.default_config(regexes) Merger(engine: merge.new(cfg)) } /// A process-global default `Merger`, built **once** on first use and reused /// forever after. Backed by `global_value` (persistent_term on the BEAM, a /// singleton object on JS), so the expensive trie + regex build happens a single /// time per runtime — the right thing for render-time callers (e.g. gg_ui's /// `cn`) that shouldn't thread a `Merger` around or rebuild it per call. /// /// This memoizes only the *engine* (the trie). It does not yet cache per-input /// merge *results*; that LRU is a separate, optional layer (see the package /// README / gg_ui follow-up). pub fn default() -> Merger { global_value.create_with_unique_name("gg_cn.default_merger", new) } /// A class value, mirroring `clsx`/`tailwind-merge`'s accepted inputs: a string, /// a conditional (a `Bool` gate paired with a class string), or a nested list. pub type ClassValue { Class(String) /// `When(condition, classes)` contributes `classes` only when `condition`. When(Bool, String) Group(List(ClassValue)) } // --- twJoin / clsx ------------------------------------------------------------ /// Join class values into one space-separated string, dropping falsy/empty /// parts — the `twJoin`/`clsx` step, without conflict resolution. pub fn tw_join(values: List(ClassValue)) -> String { values |> list.map(resolve_value) |> list.filter(fn(part) { part != "" }) |> string.join(" ") } fn resolve_value(value: ClassValue) -> String { case value { Class(class) -> class When(True, class) -> class When(False, _) -> "" Group(values) -> tw_join(values) } } // --- twMerge ------------------------------------------------------------------ /// Merge an already-joined, space-separated class string, resolving conflicts. /// /// The result is memoized by input string (JS only; the BEAM recomputes — see /// `internal/cache`). Output depends solely on the input (single baked config), /// so the cache never changes results, only speed. pub fn tw_merge(merger: Merger, class_list: String) -> String { cache.cached(class_list, fn() { merge.merge_class_list(merger.engine, class_list) }) } /// `clsx` + `twMerge` in one — the shadcn `cn` helper. Joins the class values, /// then resolves conflicts. pub fn cn(merger: Merger, values: List(ClassValue)) -> String { tw_merge(merger, tw_join(values)) }