# `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(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 all text sizes by this
    space_scale: 1.0  # multiply all spacing tokens by this

### 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(),
  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

---

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