README.md

# TM - Tailwind Merge for Elixir

Pure Elixir utility for merging Tailwind CSS classes with conditional support. Zero dependencies, fast native BEAM performance.

Based on the JavaScript libraries [tailwind-merge](https://github.com/dcastil/tailwind-merge) and [clsx](https://github.com/lukeed/clsx).

## Features

- **Pure Elixir**: No Node.js, no external dependencies
- **Fast**: Native BEAM performance (~0.02ms per call)
- **Conflict Resolution**: Later classes override earlier ones for the same CSS property
- **Conditional Classes**: clsx-style syntax with maps and keyword lists
- **Modifier Support**: Handles `hover:`, `dark:`, `sm:`, etc. as separate scopes
- **Arbitrary Values**: Full support for `bg-[#fff]`, `p-[13px]`, etc.

## Installation

Add `tm` to your dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:tm, path: "../tm_tailwind_merge"}
  ]
end
```

## Usage

### Basic Merge

```elixir
# Later classes win when they conflict
TM.merge("text-red-500 text-blue-500")
#=> "text-blue-500"

TM.merge("p-4 p-2")
#=> "p-2"

# Non-conflicting classes are preserved
TM.merge("p-4 px-2")
#=> "p-4 px-2"

# Modifiers create separate scopes
TM.merge("hover:bg-red-500 hover:bg-blue-500")
#=> "hover:bg-blue-500"

TM.merge("bg-red-500 hover:bg-red-500")
#=> "bg-red-500 hover:bg-red-500"
```

### With Conditionals (tc = tailwind conditionals)

```elixir
# Map syntax
TM.tc(["p-4", %{"bg-red-500" => is_error, "bg-green-500" => is_success}])
#=> "p-4 bg-red-500" (when is_error is true)

# Keyword syntax
TM.tc(["flex", hidden: should_hide])
#=> "flex" or "hidden" depending on should_hide

# Handles nil/false from if() expressions
TM.tc(["base", if(false, do: "hidden")])
#=> "base"

# Multiple conditions
TM.tc([
  "px-4 py-2",
  %{
    "bg-blue-500" => variant == :primary,
    "bg-red-500" => variant == :danger
  }
])
```

### In Phoenix Components

```elixir
def button(assigns) do
  ~H"""
  <button class={TM.tc([
    "inline-flex items-center justify-center font-medium transition-all",
    "px-3 py-1.5 text-sm": @size == :sm,
    "px-4 py-2 text-base": @size == :md,
    "px-6 py-3 text-lg": @size == :lg,
    "bg-blue-500 text-white hover:bg-blue-600": @variant == :primary,
    "bg-red-500 text-white hover:bg-red-600": @variant == :danger,
    "opacity-50 cursor-not-allowed": @disabled
  ])}>
    <%= render_slot(@inner_block) %>
  </button>
  """
end
```

### Just clsx (no merge)

If you only need conditional class building without conflict resolution:

```elixir
TM.clsx(["foo", %{"bar" => true, "baz" => false}])
#=> "foo bar"

# Note: clsx doesn't merge conflicts
TM.clsx(["p-4", "p-2"])
#=> "p-4 p-2"
```

## API

| Function | Description |
|----------|-------------|
| `TM.tc/1` | Conditionals + merge (main function) |
| `TM.merge/1` | Merge only (string or list input) |
| `TM.clsx/1` | Conditionals only (no merge) |

## How It Works

TM is a pure Elixir port of the tailwind-merge and clsx algorithms:

1. **clsx** - Recursively processes inputs (strings, lists, maps, keyword tuples), filters falsy values, and joins with spaces
2. **tailwind-merge** - Parses classes into components (modifiers + base class), identifies class groups, processes right-to-left keeping only the last class per group

### Class Groups

Classes are organized into groups based on the CSS property they modify:
- `p-4`, `p-2`, `p-8` → `:p` (padding all)
- `px-4`, `px-2` → `:px` (padding x)
- `bg-red-500`, `bg-blue-500` → `:bg_color`
- `text-sm`, `text-lg` → `:font_size`
- `text-red-500`, `text-blue-500` → `:text_color`

Groups also define conflicts (e.g., `p-*` overrides `px-*`, `py-*`, etc.)

## Performance

Pure Elixir implementation with no external process calls:

- **~0.02ms per merge** (vs ~50-100ms with Node.js bridge)
- **Zero memory overhead** (runs in existing BEAM)
- **No startup cost** (no process to spawn)

## Requirements

- Elixir ~> 1.14

## License

MIT