Rendro.Theme (Rendro v1.3.4) (adapter)

Copy Markdown View Source

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 partial keyword/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 brand accent: 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

colors()

@type colors() :: %{
  ink: rgb(),
  muted: rgb(),
  accent: rgb(),
  on_accent: rgb(),
  background: rgb(),
  surface: rgb(),
  rule: rgb(),
  positive: rgb(),
  negative: rgb()
}

font_role()

@type font_role() :: atom()

radius()

@type radius() :: %{none: number(), sm: number(), md: number()}

rgb()

@type rgb() :: {0..255, 0..255, 0..255}

rules()

@type rules() :: %{hairline: number(), thin: number(), thick: number()}

spacing()

@type spacing() :: %{
  unit: number(),
  tight: number(),
  normal: number(),
  loose: number(),
  section: number()
}

t()

@type t() :: %Rendro.Theme{
  colors: colors(),
  density: :comfortable | :compact,
  mode: :light | :dark,
  radius: radius(),
  rules: rules(),
  spacing: spacing(),
  typography: typography()
}

type_step()

@type type_step() :: number()

typography()

@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

dark(theme)

@spec dark(t()) :: t()

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}

default()

@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}

from_brand(brand_tokens, opts \\ [])

@spec from_brand(keyword(), keyword()) :: t()

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}

preset(genre, opts)

@spec preset(
  atom(),
  keyword()
) :: t()

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.

resolve(input)

@spec resolve(t() | map() | keyword()) :: t()

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}