# `Macaw.Layers.Theme`
[🔗](https://github.com/dcoai/macaw_layers/blob/v0.2.2/lib/macaw/layers/theme.ex#L1)

The colors, fonts and sizes a page draws with (spec/04 §2, §3).

A theme is a struct of named **slots**: `:series_1` … `:series_n` for the categorical
colors; the furniture `:axis`, `:grid`, `:text`, `:background` and `:surface`; the five
**roles** `:good`, `:warning`, `:bad`, `:info` and `:neutral`; and `:font_family`,
`:font_size`, `:label_size` and `:title_size`. Its colors are one **color table** (§2.2):
the theme's own `colors` over the default theme's, its extensions' and the CSS named
colors, with the furniture and the series as names too, so `Macaw.Layers.Color.color(:red)`
is the theme's red. Everything reads a theme through `resolve/3`, in the mode its output
needs:

  * `:literal` - the stored value, which a canvas needs;
  * `:css` - a custom-property reference with the literal as its fallback,
    `var(--mw-axis, #666666)`, so a stylesheet can override it from any ancestor.

## Meaning: ramps, roles and cues

A series color promises *distinct*, never a meaning. A theme also carries four **ramps**
whose meaning the library fixes (§2.3): `:status`, `:thermal`, `:diverging` and
`:sequential`, each a list of anchors interpolated in CIE Lab, and every role and ramp is
held to its contract when a theme is built by `new/1` or `merge/2`.

## Whole styles: the style palette

`styles` carries the design over to whole styles (§2.4): style series, the roles as whole
looks, view states, furniture styles and modifiers, the names the library's and its
registered extensions' (§3.4). `style/2` reads one, the default theme's where the theme
lacks it.

## Examples

    iex> Macaw.Layers.Theme.resolve(Macaw.Layers.Theme.default(), :axis, :css)
    "var(--mw-axis, #666666)"
    iex> Macaw.Layers.Theme.resolve(Macaw.Layers.Theme.dark(), {:series, 11}, :literal)
    "#7fb3e6"
    iex> Macaw.Layers.Theme.resolve(Macaw.Layers.Theme.default(), :warning, :literal)
    "#bd7407"
    iex> Macaw.Layers.Theme.resolve(Macaw.Layers.Theme.default(), {:ramp, :status, 0.5}, :css)
    "var(--mw-warning, #bd7407)"

Ported from visualize's `Visualize.Theme`.

# `mode`

```elixir
@type mode() :: :css | :literal
```

How a slot renders: a CSS reference with its fallback, or the stored value.

# `ramp_name`

```elixir
@type ramp_name() :: :status | :thermal | :diverging | :sequential
```

One of the four ramps whose meaning the library fixes (§2.3).

# `ramp_position`

```elixir
@type ramp_position() :: {:ramp, ramp_name(), number()}
```

A position along a ramp: `t` in `0..1`, clamped.

# `slot`

```elixir
@type slot() :: atom() | String.t() | {:series, pos_integer()}
```

A slot name, or `{:series, i}` for the i-th series color, cycling.

# `style_name`

```elixir
@type style_name() :: atom() | String.t() | {:style, pos_integer()}
```

A name of the style palette, or `{:style, i}` for the i-th style series entry.

# `t`

```elixir
@type t() :: %Macaw.Layers.Theme{
  axis: String.t(),
  background: String.t(),
  colors: %{optional(atom() | String.t()) =&gt; term()},
  dark: atom() | String.t() | nil,
  defaults: map(),
  extensions: [module()],
  font_family: String.t(),
  font_size: number(),
  grid: String.t(),
  label_size: number(),
  light: atom() | String.t() | nil,
  name: atom() | String.t(),
  ramps: %{optional(ramp_name()) =&gt; list()},
  series: [String.t(), ...],
  styles: %{optional(atom()) =&gt; map() | list()},
  surface: String.t(),
  text: String.t(),
  title_size: number()
}
```

# `builtin`

```elixir
@spec builtin(atom() | String.t()) :: t() | nil
```

The built-in theme of a name, `:default`, `:dark`, `:color_safe` or `:print`, by either
spelling, or `nil`.

    iex> Macaw.Layers.Theme.builtin("color_safe").name
    :color_safe
    iex> Macaw.Layers.Theme.builtin(:house)
    nil

# `check`

```elixir
@spec check(t()) ::
  :ok | {:error, {:ramp | :color | :style, atom() | String.t()}, String.t()}
```

`:ok`, or `{:error, subject, message}` for the first ramp, color or palette entry of the
theme that breaks its contract, `subject` being `{:ramp, name}`, `{:color, name}` or
`{:style, name}` and `message` naming the term broken with the values measured.

    iex> Macaw.Layers.Theme.check(Macaw.Layers.Theme.dark())
    :ok
    iex> theme = %{Macaw.Layers.Theme.default() | ramps: %{status: ~w(#c23238 #bd7407 #4ca34b)}}
    iex> {:error, {:ramp, :status}, message} = Macaw.Layers.Theme.check(theme)
    iex> message =~ "good (0) must be green or blue"
    true

# `check_color`

```elixir
@spec check_color(term(), :roles | :open) :: :ok | {:error, {:literal_color, term()}}
```

A color value held to a profile (§4): under `:roles` a value with a literal string among
its leaves is `{:error, {:literal_color, value}}`; under `:open` every color passes.

# `color_name?`

```elixir
@spec color_name?(t(), term()) :: boolean()
```

Whether `name` is of the theme's color table, answered without building the list.

    iex> theme = Macaw.Layers.Theme.default()
    iex> Enum.map([:red, :bad, :axis, :series_3, :steelblue, :series_99, :thick], &Macaw.Layers.Theme.color_name?(theme, &1))
    [true, true, true, true, true, false, false]

# `color_names`

```elixir
@spec color_names(t()) :: [atom() | String.t(), ...]
```

Every name of the theme's color table, sorted: its color slots, its own and the default
theme's colors, its extensions' and the CSS named colors.

    iex> names = Macaw.Layers.Theme.color_names(Macaw.Layers.Theme.default())
    iex> Enum.all?([:red, :amber, :bad, :axis, :series_3, :steelblue], &(&1 in names))
    true

# `color_safe`

```elixir
@spec color_safe() :: t()
```

The color-vision-safe theme: `default/0` with its `status` ramp blue → orange → red, which
survives the commonest color-vision deficiencies, in place of green → amber → red.

# `color_slots`

```elixir
@spec color_slots(t()) :: [atom(), ...]
```

The color slots: the series, then the furniture, then the five roles.

# `css_name`

```elixir
@spec css_name(atom() | String.t()) :: String.t()
```

The custom-property name of a slot or a color name: `--mw-` and the name with `_` as `-`.
A name is lower-case letters, digits and `_`, starting with a letter; any other raises
`ArgumentError`, so a stylesheet can never be broken out of through a name.

    iex> Macaw.Layers.Theme.css_name(:series_1)
    "--mw-series-1"

# `cue`

```elixir
@spec cue(atom()) :: %{glyph: atom(), dash: String.t() | nil}
```

A role's cue besides its hue: its glyph and its dash (`nil` for solid), so a status reads
without its hue. Raises `ArgumentError` for anything but a role.

    iex> Macaw.Layers.Theme.cue(:warning)
    %{glyph: :triangle, dash: "6 3"}

# `dark`

```elixir
@spec dark() :: t()
```

The dark theme: light text and axes on `#1b1e24`.

# `data_color`

```elixir
@spec data_color(t(), term()) ::
  {:ok, String.t()} | {:error, {:contrast, float()} | :unreadable}
```

A data color under the contrast guard (§4): `{:ok, literal}` where it reaches 3:1 against
the theme's background, `{:error, {:contrast, ratio}}` where it does not, and `{:error,
:unreadable}` for a value that does not resolve.

# `default`

```elixir
@spec default() :: t()
```

The light theme: dark text and axes on white.

# `floors`

```elixir
@spec floors(t()) :: :ok | {:error, [{atom(), term()}, ...]}
```

The contrast floors a theme offered to people holds against its own background: `text`
at least 4.5:1, `axis` and every series color at least 3:1, `grid` at least 1.2:1, and a
`surface` different from the `grid`. `:ok`, or `{:error, faults}`, each `{slot,
{:contrast, ratio, floor}}` or `{:surface, {:equal, :grid}}`.

# `ink`

```elixir
@spec ink(t(), String.t() | Macaw.Layers.Color.t() | nil) :: String.t()
```

The contrast ink for a text drawn on `under`: the literal of the theme's `text` or
`background`, whichever contrasts more (`text` on a tie), or `#000000`/`#ffffff`,
whichever contrasts more, when neither reaches 4.5:1. `under` is a color string, a
`%Macaw.Layers.Color{}`, or `nil` for the theme's background; an unreadable string is
`nil`, and a slot whose own value does not parse is skipped.

    iex> theme = Macaw.Layers.Theme.default()
    iex> {Macaw.Layers.Theme.ink(theme, "#3b6fa8"), Macaw.Layers.Theme.ink(theme, nil), Macaw.Layers.Theme.ink(theme, "#777777")}
    {"#ffffff", "#333333", "#000000"}

# `merge`

```elixir
@spec merge(t(), keyword() | map()) :: t()
```

The theme with the given fields replaced, `ramps`, `colors`, `styles` and `defaults` merged
key by key over its own; the checks of `new/1` apply.

    iex> theme = Macaw.Layers.Theme.new(name: :house, styles: %{muted: %{opacity: 0.4}})
    iex> Macaw.Layers.Theme.style(theme, :muted)
    %{opacity: 0.4}
    iex> Macaw.Layers.Theme.new(name: :house, colors: %{warning: "#f5c242"})
    ** (ArgumentError) theme "house": style :warning breaks its contract: fill {:color, :warning} is 1.65:1 against #ffffff; a role style's paint must reach 3:1 (WCAG 1.4.11, non-text contrast)

# `new`

```elixir
@spec new(keyword() | map()) :: t()
```

`default/0` with the given fields replaced; `ramps`, `colors`, `styles` and `defaults`
merged key by key. An unknown field raises `KeyError`; an empty `series`, or a ramp,
color or palette entry that breaks its contract, raises `ArgumentError` (§2.6).

# `print`

```elixir
@spec print() :: t()
```

The print theme: paper. A white background and surface, near-black text and axes, a grid
dark enough to survive a photocopy, the color-safe `status` ramp, and role styles whose
fills are hatches of the role's color, so a grayscale copy keeps each role apart.

# `ramp`

```elixir
@spec ramp(t(), ramp_name(), number()) :: String.t()
```

The color at `t` along the theme's ramp `name`, as a literal: the anchors spread evenly
over `0..1` and interpolated in CIE Lab, a position on an anchor the anchor as written,
`t` clamped. A theme that lacks the ramp takes the default theme's, never a series color.

    iex> Macaw.Layers.Theme.ramp(Macaw.Layers.Theme.default(), :thermal, 0.25)
    "#8da5cb"

# `ramp_names`

```elixir
@spec ramp_names() :: [ramp_name(), ...]
```

The four ramps whose meaning the library fixes.

# `resolve`

```elixir
@spec resolve(t(), slot() | ramp_position(), mode()) :: String.t() | number()
```

The slot's value: the stored literal, or a CSS custom-property reference that falls back
to it. Sizes are numbers under `:literal` and gain `px` under `:css`. A ramp position is
its literal in both modes, except where a status role stands, `{:ramp, :status, 0 | 0.5 |
1}`, which resolves as the role while the table holds the role there. An unknown name, or
a series slot beyond the palette, raises `ArgumentError`.

# `role_names`

```elixir
@spec role_names() :: [atom(), ...]
```

The five roles: the status ramp's three fixed points, then `:info` and `:neutral`.

# `series_slot`

```elixir
@spec series_slot(t(), pos_integer()) :: atom()
```

The `:series_k` slot the positive index `i` cycles to over the theme's series.

# `slots`

```elixir
@spec slots(t()) :: [atom()]
```

The theme's slot names: `:series_1` to `:series_n`, then the fixed slots.

# `style`

```elixir
@spec style(t(), style_name()) :: map()
```

The palette entry `name` as one style node in its parts: the theme's own, the default
theme's entry whole where the theme lacks it, or an extension's default. `{:style, i}`
cycles over the style series; in a role's entry `:cue` becomes the role's dash and glyph,
a solid dash leaving the key out. Any other name raises `ArgumentError`.

    iex> Macaw.Layers.Theme.style(Macaw.Layers.Theme.default(), :warning)
    %{fill: %{type: :solid, color: {:color, :warning}}, stroke: %{type: :solid, color: {:color, :warning}, dash: "6 3"}, marker: %{shape: :triangle}}
    iex> Macaw.Layers.Theme.style(Macaw.Layers.Theme.default(), :muted)
    %{opacity: 0.25}

# `style_names`

```elixir
@spec style_names(t()) :: [atom(), ...]
```

The theme's palette names: its style series, the default's ten when it has none of its
own, then the roles, the states, the furniture and the modifiers, then its extensions'.

# `violations`

```elixir
@spec violations(t()) :: [
  {{:ramp | :color | :style, atom() | String.t()}, String.t()}
]
```

Every ramp, color and palette entry of the theme that breaks its contract, as
`{subject, message}` in `check/1`'s order, each contract run alone.

---

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