The pure, inert design-token value for a Rendro document.
A %Rendro.Theme{} is a plain data value: color roles, typography, spacing,
rules, radius, density, and mode. It performs no I/O, touches no registry, and
never reaches the deterministic render pipeline directly — recipes read token
values from it (e.g. theme.colors.ink, theme.typography.scale.body).
Construct one of four core ways:
default/0— the built-in light theme.resolve/1— deep-merge a partialkeyword/map/%Theme{}onto the defaults, validating every color role.dark/1— swap the resolved light role tuples to their dark counterparts.from_brand/2— derive a full theme from a single brandaccent:seed.
Field shape vs. token values
The field shape (group names, keys, nesting, arities) is stable — treat it as a public contract. The token values (the default palette, the type scale numbers, leading) and the rendered bytes they produce may evolve across minor versions. Callers who need an exact frozen appearance should pin it in their own golden/snapshot tests rather than relying on literal values.
Flat elevation
There is no shadow, elevation, z-index, opacity, or gradient field,
by design — those do not map to deterministic print output. Express elevation
flatly via a surface tint plus a rule hairline; the page (background)
stays pure white.
on_accent derivation
from_brand/2 derives a readable on_accent by picking whichever of the
theme's own neutral poles (background vs ink) has the greater WCAG
contrast against the accent. This is a sensible readable default, not a
WCAG-AA/AAA or PDF-UA conformance claim — mid-tone accents may miss 4.5:1
either way; pass an explicit on_accent: to override.
Summary
Functions
Returns the dark counterpart of a theme.
Returns the built-in light theme — the shared-attribute defaults.
Derives a full theme from brand tokens carrying a single accent: seed.
Returns a fully resolved curated theme selection.
Resolves a partial input onto the defaults, returning a full %Theme{}.
Types
@type font_role() :: atom()
@type rgb() :: {0..255, 0..255, 0..255}
@type t() :: %Rendro.Theme{ colors: colors(), density: :comfortable | :compact, mode: :light | :dark, radius: radius(), rules: rules(), spacing: spacing(), typography: typography() }
@type type_step() :: number()
@type typography() :: %{ fonts: %{heading: font_role(), body: font_role(), mono: font_role()}, scale: %{ display: type_step(), title: type_step(), subtitle: type_step(), body: type_step(), small: type_step(), caption: type_step() }, leading: number(), widows: non_neg_integer(), orphans: non_neg_integer() }
Functions
Returns the dark counterpart of a theme.
Resolves the input, then swaps the pre-resolved integer role tuples to their
dark targets and sets mode: :dark. accent is unchanged and on_accent
stays white (R2) — no transcendental color math at draw time.
Dark is screen-oriented, not recommended for print: it carries no print, accessibility, PDF/UA, or WCAG contrast support claim.
Examples
iex> Rendro.Theme.dark(Rendro.Theme.default()).mode
:dark
iex> Rendro.Theme.dark(Rendro.Theme.default()).colors.background
{27, 23, 19}
@spec default() :: t()
Returns the built-in light theme — the shared-attribute defaults.
A bare %Rendro.Theme{} is equal to default/0; there is no half-nil trap.
Examples
iex> Rendro.Theme.default() == %Rendro.Theme{}
true
iex> Rendro.Theme.default().colors.background
{255, 255, 255}
Derives a full theme from brand tokens carrying a single accent: seed.
brand_tokens is a keyword list with a required accent: and optional color
roles plus an optional on_accent: override. opts carries mode:/density:.
When on_accent: is not supplied it is derived by WCAG max-contrast between
the accent and the theme's neutral poles (always an integer tuple, one of the
theme's own poles). Emits tokens only — registers no font or asset.
Examples
iex> Rendro.Theme.from_brand(accent: {44, 107, 237}).colors.accent
{44, 107, 237}
iex> Rendro.Theme.from_brand(accent: {44, 107, 237}, on_accent: {1, 2, 3}).colors.on_accent
{1, 2, 3}
Returns a fully resolved curated theme selection.
The first argument must be canonical and opts must include an :accent
color. The returned value is pure; register required curated fonts explicitly
on the document through the sibling module.
Resolves a partial input onto the defaults, returning a full %Theme{}.
Accepts a keyword, map, or existing %Theme{}. Partial input is
deep-merged onto the default groups (never raising KeyError), then every
color role is validated via Rendro.Color.validate/1; an invalid token raises
an instructive ArgumentError. resolve/1 is idempotent —
resolve(resolve(x)) == resolve(x).
Examples
iex> Rendro.Theme.resolve(mode: :light).colors.ink
{16, 24, 39}
iex> Rendro.Theme.resolve(colors: %{ink: {0, 0, 0}}).colors.ink
{0, 0, 0}