# `Color.Palette.Theme`

A complete **theme** — a coordinated set of five tonal scales —
generated from a single seed colour.

Inspired by **Material Design 3** and Material You's dynamic
theming. From one seed, this module produces five related
`Color.Palette.Tonal` palettes:

* `:primary` — the seed's hue at full chroma. The main brand
  colour and the accent for interactive elements.

* `:secondary` — the seed's hue at reduced chroma (default ⅓).
  A quieter accent for secondary actions.

* `:tertiary` — the seed's hue rotated by a fixed angle
  (default +60°) at full chroma. A complementary accent.

* `:neutral` — the seed's hue at very low chroma (default 0.02).
  For surfaces, backgrounds, and text.

* `:neutral_variant` — the seed's hue at slightly higher chroma
  (default 0.04). For outlines and dividers.

Each of the five palettes has its own 13-stop tonal scale (or
whatever stops were configured), so one seed yields ~65 colours
covering every role a typical component library needs.

## Material roles

`role/2` maps a symbolic role name to a specific stop in one of
the five palettes, following Material 3's role tokens:

    iex> theme = Color.Palette.Theme.new("#3b82f6")
    iex> {:ok, primary} = Color.Palette.Theme.role(theme, :primary)
    iex> {:ok, on_primary} = Color.Palette.Theme.role(theme, :on_primary)
    iex> match?(%Color.SRGB{}, primary) and match?(%Color.SRGB{}, on_primary)
    true

# `t`

```elixir
@type t() :: %Color.Palette.Theme{
  name: binary() | nil,
  neutral: Color.Palette.Tonal.t(),
  neutral_variant: Color.Palette.Tonal.t(),
  options: keyword(),
  primary: Color.Palette.Tonal.t(),
  secondary: Color.Palette.Tonal.t(),
  seed: Color.SRGB.t(),
  tertiary: Color.Palette.Tonal.t()
}
```

# `gamut_report`

```elixir
@spec gamut_report(t(), Color.Types.working_space()) :: map()
```

Returns a detailed gamut report broken down by sub-palette.

### Arguments

* `theme` is a `Color.Palette.Theme` struct.

* `working_space` defaults to `:SRGB`.

### Returns

* A map with:

  * `:working_space` — the space checked against.

  * `:in_gamut?` — `true` if every stop in every sub-palette
    is inside.

  * `:sub_palettes` — map of sub-palette key →
    `Color.Palette.Tonal.gamut_report/2` result.

  * `:out_of_gamut` — flat list of `%{sub_palette, label,
    color}` for stops that failed, across every sub-palette.

### Examples

    iex> theme = Color.Palette.Theme.new("#3b82f6")
    iex> report = Color.Palette.Theme.gamut_report(theme, :SRGB)
    iex> report.in_gamut?
    true
    iex> Map.keys(report.sub_palettes) |> Enum.sort()
    [:neutral, :neutral_variant, :primary, :secondary, :tertiary]

# `in_gamut?`

```elixir
@spec in_gamut?(t(), Color.Types.working_space()) :: boolean()
```

Returns `true` when every stop across all five sub-palettes
(primary, secondary, tertiary, neutral, neutral-variant) is
inside the given RGB working space.

### Arguments

* `theme` is a `Color.Palette.Theme` struct.

* `working_space` is an RGB working-space atom. Defaults to
  `:SRGB`.

### Returns

* A boolean.

### Examples

    iex> theme = Color.Palette.Theme.new("#3b82f6")
    iex> Color.Palette.Theme.in_gamut?(theme, :SRGB)
    true

# `new`

```elixir
@spec new(
  Color.input(),
  keyword()
) :: t()
```

Generates a complete theme from a seed colour.

### Arguments

* `seed` is anything accepted by `Color.new/1`.

### Options

* `:stops` — the stop list for each of the five tonal scales.
  Defaults to Material 3's `[0, 10, 20, 30, 40, 50, 60, 70, 80,
  90, 95, 99, 100]`, which is what `role/2` expects. Override
  only if you understand that role lookups may then fail.

* `:secondary_chroma_factor` — how much to multiply the seed's
  chroma by for the secondary palette. Default `0.33`.

* `:tertiary_hue_rotation` — how many degrees to rotate the hue
  by for the tertiary palette. Default `60.0`.

* `:neutral_chroma` — absolute Oklch chroma for the neutral
  scale. Default `0.02`.

* `:neutral_variant_chroma` — absolute Oklch chroma for the
  neutral-variant scale. Default `0.04`.

* `:light_anchor`, `:dark_anchor`, `:hue_drift`, `:gamut` —
  passed through to each tonal scale. See
  `Color.Palette.Tonal`.

* `:name` — optional string label stored on the struct.

### Returns

* A `Color.Palette.Theme` struct.

### Examples

    iex> theme = Color.Palette.Theme.new("#3b82f6", name: "brand")
    iex> theme.name
    "brand"
    iex> match?(%Color.Palette.Tonal{}, theme.primary)
    true
    iex> match?(%Color.Palette.Tonal{}, theme.neutral)
    true

# `role`

```elixir
@spec role(t(), atom(), keyword()) :: {:ok, Color.SRGB.t()} | :error
```

Looks up a Material 3 role name in the theme.

Roles are the symbolic tokens used by Material components —
`:primary`, `:on_primary`, `:surface`, `:outline`, etc. Each
role maps to a specific stop in one of the five palettes.

### Arguments

* `theme` is a `Color.Palette.Theme` struct.

* `role` is a role atom (see
  [Material 3 tokens](https://m3.material.io/styles/color/roles)).

### Options

* `:scheme` is `:light` (default) or `:dark`. The dark scheme
  uses different stops to maintain contrast against a dark
  background.

### Returns

* `{:ok, %Color.SRGB{}}` for known roles, `:error` for unknown
  roles.

### Examples

    iex> theme = Color.Palette.Theme.new("#3b82f6")
    iex> {:ok, %Color.SRGB{}} = Color.Palette.Theme.role(theme, :primary)
    iex> {:ok, %Color.SRGB{}} = Color.Palette.Theme.role(theme, :primary, scheme: :dark)
    iex> Color.Palette.Theme.role(theme, :nonsense)
    :error

# `roles`

```elixir
@spec roles() :: [atom()]
```

Returns the list of role names supported by `role/3`.

### Returns

* A sorted list of role atoms.

### Examples

    iex> :primary in Color.Palette.Theme.roles()
    true
    iex> :surface in Color.Palette.Theme.roles()
    true

# `to_tokens`

```elixir
@spec to_tokens(
  t(),
  keyword()
) :: map()
```

Emits the theme as a W3C [Design Tokens Community Group](https://www.designtokens.org/)
token file.

Produces two top-level groups:

* `"palette"` — the five tonal scales (primary, secondary,
  tertiary, neutral, neutral-variant), each as a stop-keyed
  group of color tokens.

* `"role"` — Material 3 role tokens (`primary`, `on_primary`,
  `surface`, etc.) emitted as **DTCG alias tokens** that
  reference the corresponding stop in `"palette"`. Tools that
  resolve aliases will see both the raw palette and the
  semantic vocabulary.

### Arguments

* `theme` is a `Color.Palette.Theme` struct.

### Options

* `:space` is the colour space for emitted stop values. Any
  module accepted by `Color.convert/2`. Default `Color.Oklch`.

* `:scheme` is `:light` (default) or `:dark`. Controls which
  tone each role aliases to.

### Returns

* A map with `"palette"` and `"role"` keys.

### Examples

    iex> theme = Color.Palette.Theme.new("#3b82f6")
    iex> tokens = Color.Palette.Theme.to_tokens(theme)
    iex> tokens["palette"]["primary"]["40"]["$type"]
    "color"
    iex> tokens["role"]["primary"]["$value"]
    "{palette.primary.40}"

---

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