# `PhoenixKit.Notifications.Types`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.14.2/lib/phoenix_kit/notifications/types.ex#L1)

Registry of notification types (and optional sub-types) for the per-user
preferences UI.

A **type** is a named group of related activity actions a user can toggle as
one unit (`"account"`, `"posts"`, `"comments"`, …). Core types ship with
PhoenixKit; external modules contribute more via the optional
`notification_types/0` callback on `PhoenixKit.Module`.

A type may declare **sub-types** for finer control — e.g. `"comments"` with
`"replies"` and `"reactions"`. Sub-type preference keys are the base key and
the sub key joined by a dot (`"comments.replies"`), mirroring the permission
system's dotted sub-keys.

## Master-switch semantics (NOT the permission cascade)

The base type is a **master switch evaluated at resolution time only**:

  * base OFF ⇒ every sub-type under it is muted, regardless of the sub's own value;
  * base ON  ⇒ each sub-type follows its own toggle (defaulting to the sub's `default`).

This is deliberately the OPPOSITE of `PhoenixKit.Users.Permissions`, where a
sub *implies* its base and grants cascade/normalize the stored set. Here nothing
cascades and nothing is normalized in storage — a user's per-sub choices are
preserved when the master is toggled off, so flipping it back on restores them.
See `PhoenixKit.Notifications.Prefs` for the resolution.

## Shape

    %{
      key: "comments",
      label: "Comments",
      description: "Replies and reactions to your comments",
      actions: [],                       # base-owned actions (often [] when fully split)
      default: true,
      sub_types: [
        %{key: "comments.replies",   label: "Replies",   actions: ["comment.replied"], default: true},
        %{key: "comments.reactions", label: "Reactions", actions: ["comment.liked", "comment.disliked"], default: true}
      ]
    }

Module authors declare sub keys **bare** (`"replies"`); `normalize/1` composes
them to `"<type>.<sub>"`. Declared keys must not contain `.` and nesting is one
level only — offending entries are dropped with a warning. Unknown top-level
map fields are preserved (a seam for future per-channel / digest metadata).
Sub-type labels/descriptions are runtime data — `mix gettext.extract` won't see
them (same caveat as permission labels).

# `sub_type`

```elixir
@type sub_type() :: %{
  key: String.t(),
  label: String.t(),
  description: String.t(),
  actions: [String.t()],
  default: boolean()
}
```

# `t`

```elixir
@type t() :: %{
  key: String.t(),
  label: String.t(),
  description: String.t(),
  actions: [String.t()],
  default: boolean(),
  sub_types: [sub_type()]
}
```

# `all_pref_keys`

```elixir
@spec all_pref_keys() :: [String.t()]
```

Every valid preference key — each base key followed by its sub keys, de-duped,
stable order. Used to sanitize saves and to build the pause-all set.

# `base_keys`

```elixir
@spec base_keys() :: [String.t()]
```

The base type keys only (no sub keys) — the master switches.

# `default_for`

```elixir
@spec default_for(String.t()) :: boolean()
```

Default-enabled flag for a base OR dotted key.

TOTAL by contract: an unknown key returns `true` (the fail-open backstop —
a `false`/`nil` here would let `master AND sub` go silently falsy).

# `find`

```elixir
@spec find(String.t()) :: t() | nil
```

Look up a base type by its key. Returns `nil` when not registered (base keys only).

# `key_for_action`

```elixir
@spec key_for_action(String.t()) :: String.t() | nil
```

Resolves an action to the **most-specific** preference key that owns it — a
sub-type's dotted key when a sub-type claims it, otherwise the base type key,
otherwise `nil`.

Backed by a deterministic action→key index (`action_index/0`): sub-types are
indexed before their base (so a sub wins over its base), and the FIRST claim of
an action wins globally (a later duplicate claim is ignored with a warning).
`nil` for unclaimed actions preserves the caller's fail-open behaviour.

# `list`

```elixir
@spec list() :: [t()]
```

Full list of types — core plus module-contributed, normalized, stable order.

# `parent_type_key`

```elixir
@spec parent_type_key(String.t()) :: String.t() | nil
```

Base type key for a dotted sub key (`"comments.replies"` → `"comments"`).

Returns `nil` for a base key or an unkeyed value. Safe because declared keys
are `.`-free and nesting is one level (enforced in `normalize/1`).

# `type_for_action`

```elixir
@spec type_for_action(String.t()) :: String.t() | nil
```

Resolves an action to the key of its owning **base** type (back-compat).

Used by `Prefs.user_wants_type?/2`. Returns the base key even when a sub-type
owns the action.

---

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