# `Expresso.Palette`
[🔗](https://github.com/rellen/expresso/blob/main/lib/expresso/palette.ex#L1)

Holds the colors of a theme, and makes sure that they meet the minimums of WCAG and APCA

A palette has the 16 colors of a base16 scheme, `base00` to `base0F`. The
scheme gives each color a purpose: `base00` is the background, `base05` the
text, `base03` the comments, and `base08` to `base0F` the accents. `new/4`
makes a palette, and it gives each **role** of the theme one of the colors.
A role is a color that the style sheet reads as a custom property, such as
`--text` or `--code-keyword`. `roles/0` lists them.

`problems/1` returns each role that does not meet a minimum. Each minimum has
two measures: the contrast ratio of WCAG 2.2, and the lightness contrast `Lc`
of APCA. The contrast ratio is too high for a color on a dark background,
and APCA corrects this. Thus a color must meet both. These are the minimums:

  * Each role of text, on its background: 4.5:1 and Lc 60. 4.5:1 is the
    minimum of WCAG 2.2 for normal text, criterion 1.4.3. Lc 60 is the
    minimum of APCA for the text of content. A slide shows large text, but a
    projector and the light of a room lower the contrast, so a theme does not
    use the smaller minimums of large text.
  * Each role of text with the dim opacity of the palette, on its
    background: 3:1 and Lc 30. 3:1 is the minimum of criterion 1.4.11 for a
    part that the reader must see. Lc 30 is the minimum of APCA for any
    text that the reader must be able to read, such as a text that the
    presenter does not talk about now. A dimmed element is less important, and it
    must stay readable. The dimming applies to each color of the element,
    such as a comment of a dimmed line of code, so the role with the lowest
    contrast sets the opacity. 3:1 is also the minimum of WCAG for large
    text, and not for normal text. The `dim_contrast` option of a deck raises
    it, such as to 4.5:1 for criterion 1.4.3. See `with_dim_contrast/2`.

`docs/architecture.md` gives the formulas, and it tells why the dimming of a
theme is small.

The accent also colors the progress bar and the frame of the selected page
in the overview, and 4.5:1 is more than the 3:1 that criterion 1.4.11 gives
for them. `adjust/1` changes only the lightness of each role that fails,
until it meets the minimum, and it never changes a background.
`Expresso.Palette.Builtin` adjusts each built-in theme, and
`Expresso.ThemeVerifier` gives a warning for a palette of a deck that fails.

# `slot`

```elixir
@type slot() :: atom()
```

A slot of a base16 scheme, from `:base00` to `:base0F`

# `t`

```elixir
@type t() :: %Expresso.Palette{
  change: float(),
  colors: %{required(slot()) =&gt; Expresso.Color.t()},
  dim_opacity: float(),
  dim_ratio: float(),
  dimmed: %{required(atom()) =&gt; {float(), Expresso.Color.t()}},
  name: String.t(),
  roles: %{required(atom()) =&gt; Expresso.Color.t()},
  variant: :dark | :light
}
```

A palette: its name, its variant, its 16 colors, and the color of each role

`change` is the largest change of lightness that `adjust/1` made, in OKLab,
from 0 to 1. `dimmed` holds, for each role of text, its dimmed color and the
opacity that gives it. `dim_opacity` is one opacity that dims each role of
text to its minimums. `dim_ratio` is the minimum contrast ratio of a dimmed
role: 3.0 by default, and the `dim_contrast` option of a deck can raise it.
See `with_dim_contrast/2`. The style sheet dims an image, an SVG file and an
embed with it, because their colors are not roles.

# `theme`

```elixir
@type theme() :: atom() | %{required(slot()) =&gt; Expresso.Color.t()} | keyword()
```

The value of the `theme` option of a deck

A name of a built-in theme, a map with a color for each slot, or a keyword
list. The keyword list has `colors` for one theme, or `dark` and `light` for
two variants, and `adjust` to adjust each map.

# `adjust`

```elixir
@spec adjust(t()) :: {:ok, t()} | :error
```

Change the lightness of each role that fails, until each role meets its minimum

The function keeps the hue of each color and each background. It returns
`:error` when no lightness gives a role its minimum.

# `chart_colors`

```elixir
@spec chart_colors(:light | :dark) :: [String.t()]
```

Return the colors of the series of a chart for a variant, in order

    iex> Expresso.Palette.chart_colors(:light) |> length()
    8

# `declarations`

```elixir
@spec declarations(t()) :: String.t()
```

Return the custom properties of the palette, as declarations for a rule

Each role gets a property, such as `--text`. Each role of text also gets
the property of its dimmed color, such as `--text-dim`. `--dim-opacity`
holds the dim opacity of the palette, and `--chart-1` to `--chart-8` hold
the colors of the series of a chart for the variant of the palette.

    iex> palette = Expresso.Palette.Builtin.fetch!(:default)
    iex> Expresso.Palette.declarations(palette) =~ "--text: #000000;"
    true
    iex> Expresso.Palette.declarations(palette) =~ "--text-dim: #"
    true

# `new`

```elixir
@spec new(String.t(), :dark | :light, %{required(slot()) =&gt; Expresso.Color.t()}) ::
  t()
```

Make a palette from the 16 colors of a base16 scheme

`colors` holds a `#rrggbb` color for each slot. Each role of text gets a
dimmed color: the role blended on its background at the smallest multiple
of 0.05 that keeps the role at 3:1 and at Lc 30. Each role dims as far as
its own contrast lets it.

# `of`

```elixir
@spec of(theme() | nil) :: t()
```

Return the palette of one theme of the `theme` option of a deck

The option is the name of a built-in theme, or a map with a `#rrggbb` color
for each slot of base16. A palette of a map is not adjusted. Its variant is
dark when its background is dark. A keyword list returns its first variant
from `variants/1`.

# `problems`

```elixir
@spec problems(t()) :: [{atom(), :wcag | :apca, float(), number()}]
```

Return each role that does not meet a minimum, with the measure, its value and the minimum

The measure is `:wcag` for the contrast ratio, or `:apca` for the lightness
contrast. A role can fail both. The role `:dimmed_text` stands for the role
of text with the lowest contrast at the dim opacity. A palette that meets
each minimum returns an empty list.

# `roles`

```elixir
@spec roles() :: [{atom(), {slot(), atom() | nil}}]
```

Return each role, the slot of its color, and the role of its background

A background has `nil` in place of a background.

# `slots`

```elixir
@spec slots() :: [slot()]
```

Return the slots of a base16 scheme, in order

# `suggestions`

```elixir
@spec suggestions(t()) :: %{required(slot()) =&gt; Expresso.Color.t()}
```

Return a color that passes for each slot with a role of text that fails

A slot can color more than one role, such as `base05` for `text` on
`base00` and for `code_text` on `base01`. The color of the slot changes
its lightness with `Expresso.Color.adjust/4` against each background in
turn, and it must then meet the minimums on each one. A slot with no such
color is not in the map. `Expresso.ThemeVerifier` puts the color into its
warning.

    iex> colors = Expresso.Palette.Builtin.fetch!(:default).colors
    iex> palette = Expresso.Palette.of(%{colors | base03: "#dddddd"})
    iex> %{base03: color} = Expresso.Palette.suggestions(palette)
    iex> problems = Expresso.Palette.problems(Expresso.Palette.of(%{colors | base03: color}))
    iex> Enum.filter(problems, &(elem(&1, 0) == :code_comment))
    []

# `validate`

```elixir
@spec validate(term()) :: {:ok, theme()} | {:error, String.t()}
```

Validate the value of the `theme` option, for the schema of the DSL

The value is the name of a built-in theme, or a map with exactly the 16
slots of base16 and a `#rrggbb` color for each one. The contrast is not part
of the validation: `Expresso.ThemeVerifier` gives a warning for it.

The value can also be a keyword list:

  * `colors` with a name or a map, for one theme, or
  * `dark` and `light`, each with a name or a map, for two variants,
  * and `adjust` with `true` or `false`, optional.

    iex> Expresso.Palette.validate(dark: :dracula, light: :default, adjust: true)
    {:ok, [dark: :dracula, light: :default, adjust: true]}

    iex> {:error, message} = Expresso.Palette.validate(dark: :dracula)
    iex> message =~ "needs colors, or both dark and light"
    true

    iex> Expresso.Palette.validate(:dracula)
    {:ok, :dracula}

    iex> {:error, message} = Expresso.Palette.validate(:plaid)
    iex> message =~ "is not a built-in theme"
    true

# `variants`

```elixir
@spec variants(theme() | nil) :: [{:light | :dark | nil, t()}]
```

Return the palette of each variant of the `theme` option of a deck

A theme with one variant returns `[{nil, palette}]`. A keyword list with
`dark` and `light` returns `[light: palette, dark: palette]`. With
`adjust: true`, the palette of each map gets `adjust/1`, as a built-in theme
does. A map that no lightness can adjust keeps its colors, and
`Expresso.ThemeVerifier` then gives a warning for each role that fails.

    iex> [{nil, palette}] = Expresso.Palette.variants(:dracula)
    iex> palette.name
    "Dracula"

    iex> Expresso.Palette.variants(dark: :dracula, light: :default) |> Keyword.keys()
    [:light, :dark]

# `with_dim_contrast`

```elixir
@spec with_dim_contrast(t(), number() | nil) :: t()
```

Dim the palette to a higher minimum contrast ratio

Each role of text then dims only as far as the role keeps `ratio` on its
background, and the dim opacity follows. The minimum of APCA stays at Lc 30.
A ratio of 4.5 is the minimum of WCAG 2.2 for normal text, criterion 1.4.3.
`nil` keeps the palette as it is.

    iex> palette = Expresso.Palette.Builtin.fetch!(:default)
    iex> Expresso.Palette.with_dim_contrast(palette, 4.5).dim_opacity >= palette.dim_opacity
    true

---

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