# `Mob.Theme`
[🔗](https://github.com/genericjam/mob/blob/master/lib/mob/theme.ex#L1)

Design token system for Mob apps.

A theme is a compiled `%Mob.Theme{}` struct — a flat map of semantic tokens
for colors, spacing, radii, and scale factors. The renderer resolves these
tokens at render time so every component picks up the active theme
automatically.

## Using a named theme

Named themes are plain modules that export `theme/0`. Pass the module to
`use Mob.App`:

    use Mob.App, theme: MobThemes.Obsidian   # (the mob_themes style package)

Override individual tokens without leaving the theme:

    use Mob.App, theme: {MobThemes.Obsidian, primary: :rose_500}

Anyone can publish a theme as a Hex package — any module with `theme/0`
returning a `Mob.Theme.t()` works:

    use Mob.App, theme: AcmeCorp.BrandTheme

## Building a theme from scratch

Pass a keyword list of overrides against the neutral base:

    use Mob.App, theme: [primary: :emerald_500, type_scale: 1.1]

Or change the theme at runtime (e.g. for accessibility or user preference):

    Mob.Theme.set(MobThemes.Obsidian)
    Mob.Theme.set({MobThemes.Obsidian, type_scale: 1.2})
    Mob.Theme.set({MobThemes.Obsidian, text_scale: 1.3})   # the user's text size
    Mob.Theme.set(primary: :pink_500)

## Base theme

When no theme is set the renderer uses the neutral base — plain dark grays
with a standard blue primary. Functional, not opinionated. Good enough for
hello world; swap in a named theme when you want personality.

## Token reference

### Semantic color tokens

    :primary        — main action colour          (default :blue_500)
    :on_primary     — text/icons on primary        (default :white)
    :secondary      — secondary action colour      (default :gray_600)
    :on_secondary   — text/icons on secondary      (default :white)
    :background     — page/screen background       (default :gray_900)
    :on_background  — text on background           (default :gray_100)
    :surface        — card / sheet background      (default :gray_800)
    :surface_raised — elevated card background     (default :gray_700)
    :on_surface     — text/icons on surface        (default :gray_100)
    :muted          — secondary/placeholder text   (default :gray_500)
    :error          — error state colour           (default :red_500)
    :on_error       — text/icons on error          (default :white)
    :border         — dividers and outlines        (default :gray_700)

### Spacing tokens (scaled by `space_scale`)

    :space_xs  →  4 × scale
    :space_sm  →  8 × scale
    :space_md  → 16 × scale
    :space_lg  → 24 × scale
    :space_xl  → 32 × scale

### Radius tokens

    :radius_sm   → theme.radius_sm   (default  6)
    :radius_md   → theme.radius_md   (default 10)
    :radius_lg   → theme.radius_lg   (default 16)
    :radius_pill → theme.radius_pill (default 100)

### Scale factors

    type_scale:  1.0  # multiply the text size tokens (:sm, :base, :xl…) by this
    text_scale:  1.0  # multiply every text_size / font_size prop, number or token
    space_scale: 1.0  # multiply all spacing tokens by this

`type_scale` belongs to the theme's design: it scales the token sizes and
leaves a numeric `text_size` (a size pinned to a design) alone.
`text_scale` is the user's text-size preference (a Settings choice such as
Small … Largest): the renderer multiplies it into every node's `text_size`
and `font_size` prop, numeric or token, so an app written in exact numbers
still grows. Only sizes on the node scale: a `:text` with no `text_size`
keeps the platform default (give it `:base` to follow), and a sized
`:icon`'s glyph grows with it. Not scaled: a Canvas `text` op's `size`
(it's in the drawing's coordinates) and sizes nested in a plugin's own
props (a style map). It multiplies the OS font scale where native follows
one: Android text sizes are `sp`; on iOS, custom-font text (`Font.custom(_:size:)`)
follows Dynamic Type, while system-font text and the UIKit-backed text
field are fixed points. At 1.0 (the default) every value reaches native
exactly as written.
Set it with the rest of the theme, and keep it when you switch themes:

    Mob.Theme.set({MyApp.Theme.Dark, text_scale: 1.3})

### Font tokens

`fonts` is a name → value map, resolved by the renderer exactly like
colors (`font: :heading` walks this map, same two-step shape as
`text_color: :primary`). `:default` is special — set it and the renderer
injects it automatically onto any node that doesn't specify its own
`font:`, so the whole app picks up a custom font without repeating it
everywhere:

    Mob.Theme.set(
      fonts: %{
        default: Mob.Theme.font("Inter-Regular", from_file: "priv/fonts/Inter-Regular.ttf"),
        heading: Mob.Theme.font("Inter-Bold", from_file: "priv/fonts/Inter-Bold.ttf")
      }
    )

    # in render/1:
    %{type: :text, props: %{text: "Section", font: :heading}, children: []}
    %{type: :text, props: %{text: "Body copy"}, children: []}  # gets :default automatically

`font_fallback` is an ordered list of font names tried, in order, if the
resolved font can't be loaded on-device. Empty by default (the platform's
own system-font fallback still applies) — set it when you want an
explicit intermediate fallback before that.

A font value is either built with `Mob.Theme.font/2` (recommended — it
computes the Android name from the actual bundled file) or a bare string
used as-is on both platforms.

# `color_value`

```elixir
@type color_value() :: atom() | non_neg_integer()
```

# `font_spec`

```elixir
@type font_spec() :: %{optional(:ios) =&gt; String.t(), optional(:android) =&gt; String.t()}
```

# `font_value`

```elixir
@type font_value() :: font_spec() | String.t()
```

# `t`

```elixir
@type t() :: %Mob.Theme{
  background: term(),
  border: term(),
  error: term(),
  font_fallback: term(),
  fonts: term(),
  glass: term(),
  muted: term(),
  on_background: term(),
  on_error: term(),
  on_primary: term(),
  on_secondary: term(),
  on_surface: term(),
  primary: term(),
  radius_lg: term(),
  radius_md: term(),
  radius_pill: term(),
  radius_sm: term(),
  secondary: term(),
  space_scale: term(),
  surface: term(),
  surface_raised: term(),
  text_scale: term(),
  type_scale: term()
}
```

# `build`

```elixir
@spec build(keyword()) :: t()
```

Build a theme from a keyword list of overrides against the neutral base.

    Mob.Theme.build(primary: :emerald_500, type_scale: 1.1)

# `color_scheme`

```elixir
@spec color_scheme() :: :light | :dark
```

Returns the current OS appearance: `:light` or `:dark`.

Reads from the platform NIF (`UITraitCollection.userInterfaceStyle` on
iOS, `Configuration.uiMode & UI_MODE_NIGHT_MASK` on Android). Falls back
to `:light` when running on the host BEAM (no NIF loaded), on platforms
that don't expose appearance, or on legacy Android apps that haven't
added `MobBridge.getColorScheme()` yet.

# `current`

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

Return the currently active theme (or the neutral base if none is set).

# `default`

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

Return the neutral base theme.

# `font`

```elixir
@spec font(String.t(), [{:from_file, Path.t()}]) :: font_spec()
```

Builds a font token value: the iOS PostScript name as given, paired with
the Android resource name computed from the bundled file via
`Mob.Font.android_resource_name/1` — the same function `mob_dev`'s asset
planner uses when it copies the file into `res/font/`. One computation,
used on both the build side and the theme side, so the two can't drift
apart the way a hand-typed Android name could.

    fonts: %{
      heading: Mob.Theme.font("Inter-Bold", from_file: "priv/fonts/Inter-Bold.ttf")
    }

# `resolved_palette`

```elixir
@spec resolved_palette(t()) :: %{required(atom()) =&gt; non_neg_integer()}
```

Returns the active theme's palette resolved to ARGB integers — semantic
tokens (`:primary`, `:on_surface`, …) walked through the theme's color
map and then through `Mob.Renderer.colors/0`. Used to push concrete
values to the native side (`Mob.Theme.set/1` does this automatically;
callers usually don't need to invoke this directly).

# `set`

```elixir
@spec set(t() | module() | {module(), keyword()} | keyword()) :: :ok
```

Set the active theme. Accepts:

- A compiled `%Mob.Theme{}` struct
- A theme module (any module exporting `theme/0`, e.g. `MobThemes.Obsidian`)
- A `{module, overrides}` tuple
- A keyword list of overrides against the neutral base

The module and `{module, overrides}` forms are remembered (see
`Mob.Theme.AdaptiveWatcher`): when the module is the registered
follow-OS theme, an OS light/dark flip resolves it again, keeping the
overrides. A struct or keyword list is a fixed theme and is never
re-resolved, even if it equals what the adaptive module returns.

---

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