PhiaUi.ClassMerger.Groups (phia_ui v0.1.12)

Copy Markdown View Source

Maps Tailwind CSS utility class tokens to their conflict group.

The Conflict Group Concept

Two Tailwind classes "conflict" when they both set the same CSS property. For example, px-2 and px-4 both set padding-inline-start and padding-inline-end. Applying both classes to an element results in only one value being active (whichever appears last in the stylesheet), which makes the earlier class redundant and confusing.

cn/1 uses this module to detect and resolve such conflicts at merge time: only the last class in a conflict group is kept in the output.

Each conflict group is identified by an atom (e.g. :px, :bg, :text_size). Two tokens with the same group atom conflict; two tokens with different group atoms are independent and both kept.

Lookup Strategy

group_for/1 uses a four-step cascade to classify any token. Steps are ordered from most specific to least specific, and the first match wins:

  1. exact_group/1 — O(1) compile-time map lookup. Handles classes that must be matched precisely to avoid false positives (e.g. flex matches both the display group and the flex-* prefix rule). Also handles text-{align} classes that must be separated from the text-{color} catch-all in step 2.

  2. special_group/1 — Handles ambiguous text-* and font-* prefixes that cover multiple independent CSS properties:

    • text-sm / text-lg:text_size (font-size)
    • text-red-500 / text-primary:text_color (color)
    • font-bold / font-semibold:font_weight (font-weight)
    • font-sans / font-mono:font_family (font-family)
  3. prefix_group/1 — Iterates the @prefix_rules table (ordered longest-first) and returns the group for the first matching prefix. This covers spacing, sizing, layout, and decoration utilities.

  4. standalone_group/1 — Catches multi-word classes that share a prefix with an unrelated group (e.g. flex-row / flex-col for flex direction, which must not match the flex-* prefix in the display group).

Why MapSet for Membership Tests?

The compile-time constants (@text_size_classes, @font_weight_classes, @font_family_classes, @display_classes, etc.) are stored as MapSet values rather than plain lists. This gives O(1) average-case membership tests (class in @text_size_classes) instead of O(n) linear scans, which matters because group_for/1 is called for every token in every cn/1 call.

MapSets are built at compile time using module attributes, so there is no runtime allocation cost — the set is baked into the BEAM bytecode as a literal.

Why Prefix Rules Are Ordered Longest-First

The prefix-rule table is scanned linearly from top to bottom; the first matching prefix wins. Ordering by descending prefix length prevents shorter prefixes from matching classes that belong to a more specific group.

Example: gap-x-4 must match {"gap-x-", :gap_x} before {"gap-", :gap}. If "gap-" appeared first, gap-x-4 would be incorrectly assigned to the general :gap group instead of the axis-specific :gap_x group, causing gap-x-4 and gap-4 to be treated as conflicting (they are independent CSS properties: column-gap vs gap).

Similarly, px-4 must match {"px-", :px} before {"p-", :p} — they set different CSS properties (padding-inline vs all-sides padding).

Returns

Returns nil for classes that don't belong to any known group (arbitrary / custom classes), in which case exact-match deduplication applies instead.

Summary

Functions

Returns the conflict group atom for class, or nil if unknown.

Functions

group_for(class)

@spec group_for(String.t()) :: atom() | nil

Returns the conflict group atom for class, or nil if unknown.

The returned atom identifies the CSS property group that the class belongs to. Two classes with the same group atom conflict; only the last one is kept by cn/1. A nil return means the class is not in any known conflict group and will be deduplicated by exact string match instead.

Examples

Background colour group:

iex> PhiaUi.ClassMerger.Groups.group_for("bg-primary")
:bg

iex> PhiaUi.ClassMerger.Groups.group_for("bg-red-500")
:bg

Text size (font-size) group:

iex> PhiaUi.ClassMerger.Groups.group_for("text-sm")
:text_size

iex> PhiaUi.ClassMerger.Groups.group_for("text-2xl")
:text_size

Text colour — different group from text size:

iex> PhiaUi.ClassMerger.Groups.group_for("text-red-500")
:text_color

iex> PhiaUi.ClassMerger.Groups.group_for("text-primary")
:text_color

Text alignment — exact-matched before the text-* catch-all:

iex> PhiaUi.ClassMerger.Groups.group_for("text-center")
:text_align

Padding — axis-specific group:

iex> PhiaUi.ClassMerger.Groups.group_for("px-4")
:px

iex> PhiaUi.ClassMerger.Groups.group_for("p-4")
:p

Display — exact match:

iex> PhiaUi.ClassMerger.Groups.group_for("flex")
:display

iex> PhiaUi.ClassMerger.Groups.group_for("hidden")
:display

Unknown / custom class returns nil:

iex> PhiaUi.ClassMerger.Groups.group_for("phia-custom")
nil

iex> PhiaUi.ClassMerger.Groups.group_for("my-custom-class")
nil