# `PhiaUi.ClassMerger.Groups`
[🔗](https://github.com/charlenopires/PhiaUI/blob/v0.1.17/lib/phia_ui/class_merger/groups.ex#L1)

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.

# `group_for`

```elixir
@spec group_for(String.t()) :: {String.t() | nil, atom()} | nil
```

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

Returns a `{prefix, group}` tuple where `prefix` is the variant prefix
string (e.g. `"sm:"`, `"hover:sm:"`) or `nil` for unprefixed classes.
Two classes with the same `{prefix, group}` tuple conflict; only the last
one is kept by `cn/1`.

This makes conflict resolution responsive-prefix aware: `sm:flex-col` and
`sm:flex-row` conflict (both `{"sm:", :flex_direction}`), but `sm:flex-col`
and `md:flex-row` do not conflict (different prefixes).

Returns `nil` when the class has no known conflict group — it will be
deduplicated by exact string match instead.

## Examples

Background colour group:

    iex> PhiaUi.ClassMerger.Groups.group_for("bg-primary")
    {nil, :bg}

    iex> PhiaUi.ClassMerger.Groups.group_for("bg-red-500")
    {nil, :bg}

Responsive prefix awareness:

    iex> PhiaUi.ClassMerger.Groups.group_for("sm:bg-primary")
    {"sm:", :bg}

    iex> PhiaUi.ClassMerger.Groups.group_for("md:bg-red-500")
    {"md:", :bg}

Text size (font-size) group:

    iex> PhiaUi.ClassMerger.Groups.group_for("text-sm")
    {nil, :text_size}

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

Text colour — different group from text size:

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

    iex> PhiaUi.ClassMerger.Groups.group_for("text-primary")
    {nil, :text_color}

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

    iex> PhiaUi.ClassMerger.Groups.group_for("text-center")
    {nil, :text_align}

Padding — axis-specific group:

    iex> PhiaUi.ClassMerger.Groups.group_for("px-4")
    {nil, :px}

    iex> PhiaUi.ClassMerger.Groups.group_for("p-4")
    {nil, :p}

Display — exact match:

    iex> PhiaUi.ClassMerger.Groups.group_for("flex")
    {nil, :display}

    iex> PhiaUi.ClassMerger.Groups.group_for("hidden")
    {nil, :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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
