# `MishkaCmsCore.Install.UiKitCatalog`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/install/ui_kit_catalog.ex#L5)

Pure view of a `mishka.ui_kit.bundle.v3` for the installer's component picker: groups the
components by `type`, keeps only the light fields the picker needs (name / label / deps — never
the heavy body/template/examples), and computes the transitive COMPONENT-dependency closure of a
selection so required components can be locked.

The bundle JSON has no "required" flag and `UiKitHandler` does not resolve dependencies itself, so
the picker must derive "required" = the closure of the user's picks. Picking `chelekom-card` pulls
in `chelekom-icon` (depended on by ~80 components), which the user then cannot unmark. `*.js` hook
deps are ignored here — js_hooks and the stylesheet install in full regardless of the component
selection — and the `chelekom-scroll_area` (underscore) vs `chelekom-scroll-area` (hyphen) naming
mismatch is normalized.

Per-component narrowing is offered along the axes variant/color/size/padding/space/rounded/border
(in that display order). An axis is shown for a component only when the kit enumerates string values
for it AND declares a string default — components whose axis (e.g. `size`) is numeric simply skip it.
The axis name is used verbatim as the `:selection` per-component key, which `UiKitHandler` accepts;
the picker pre-checks and locks the kit-declared default. Components install minimal by default
(their own default value), and `config_extras` widens chosen axes to `[default | extras]`.

See `MishkaCmsCore.Runtime.Handlers.UiKitHandler` and `MishkaCmsWeb.InstallLive`.

# `axis_config`

```elixir
@type axis_config() :: %{values: [String.t()], default: String.t() | nil}
```

# `entry`

```elixir
@type entry() :: %{
  name: String.t(),
  label: String.t(),
  kit: String.t() | nil,
  type: String.t(),
  deps: [String.t()],
  required?: boolean(),
  config: %{optional(String.t()) =&gt; axis_config()}
}
```

# `group`

```elixir
@type group() :: %{key: String.t(), label: String.t(), entries: [entry()]}
```

# `t`

```elixir
@type t() :: %{
  groups: [group()],
  index: %{optional(String.t()) =&gt; entry()},
  names: MapSet.t()
}
```

# `axis_order`

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

The order the installer shows configurable axes in, most-recognised first.

Kept as a SORT KEY rather than a filter — see `config_features/1`. An axis this list has never
heard of sorts after the ones it has, which is the only thing the order was ever for. Public so
the installer reads it here instead of holding a second copy.

# `build`

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

Builds the picker catalog (groups + name index + dependency graph) from a parsed v3 bundle.

# `install_selection`

```elixir
@spec install_selection(t(), Enum.t(), %{optional(String.t()) =&gt; map()}) :: map()
```

Builds the `:selection` map for `UiKitHandler.install_chelekom!/1` from the picked components and the
per-component variant config.

Every component installs **minimal by default** — narrowed to its OWN default variant/color (the
kit's defaults are not uniform: `color` defaults to `natural` for ~half the components, `base` for
others — so a global "base only" would render those bare). `config_extras` (`%{component_name =>
%{axis => [extra_values]}}`) widens individual components: e.g. adding `danger` to `button`'s color
installs that component with `[natural, danger]`. Axes without a usable default, or with no extras
on components that have no enumerable values, simply install full.

# `install_selection_by_kit`

```elixir
@spec install_selection_by_kit(t(), Enum.t(), %{optional(String.t()) =&gt; map()}) ::
  map()
```

The same selection, split per kit — what `UiKitHandler.install_kits!/2` takes.

With one kit installed this is the single-kit map under one key, and behaves exactly as before.
With two, each kit gets only its own components: the labels a selection is keyed by (`alert`,
`tree`) are unique per kit but not across kits, so a flat map would install one kit's narrowing
onto another kit's component of the same name.

# `install_set`

```elixir
@spec install_set(t(), Enum.t()) :: MapSet.t()
```

The full set of components to install: the user's picks plus their required closure.

# `kit_label`

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

A kit's own name, as a person would read it: `chelekom_headless` -> "Chelekom Headless".

Used by the installer's kit chips and the badge beside a component, both of which only appear once
a second kit is on disk.

# `label`

```elixir
@spec label(String.t()) :: String.t()
```

Strips the kit prefix for display (`chelekom-card` -> `card`).

# `label`

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

The same, told which kit shipped the component.

The kit's own name is the prefix, and `Component` SLUGIFIES what it stores — a bundle named
`chelekom_headless` installs `chelekom-headless-tree`, so stripping at the first hyphen would
leave "headless-tree". Given the kit, the whole prefix comes off and the label is "tree".

# `mandatory`

```elixir
@spec mandatory(t()) :: MapSet.t()
```

The components the kit declares the CMS cannot run without, whatever the operator picked.

`deps` answers "this component needs that one"; `required` answers "this install needs it at all".
An icon is drawn by half of Chelekom's other components and nothing in the markup says so, so
deselecting it left the rest unable to render — and the picker had no way to know.

Seeded into the dependency closure rather than checked separately, so it is locked in the UI and
installed by the same path a dependency is.

# `merge`

```elixir
@spec merge([map()]) :: t()
```

One catalogue over several bundles, for an installer that offers more than one kit.

Entries keep their FULL component name, which is unique across kits, so the picker's selection set
and dependency closure keep working unchanged; only the labels and the install selection need to
know which kit an entry came from.

# `required_by`

```elixir
@spec required_by(t(), Enum.t(), String.t()) :: [String.t()]
```

The selected components that force `name` into the required set (for a 'required by …' hint).

# `required_closure`

```elixir
@spec required_closure(t(), Enum.t()) :: MapSet.t()
```

The transitive component-dependency closure of `selected` that the user did NOT explicitly pick —
i.e. the components that must be installed and locked in the UI. `install_set/2` adds the picks back.

# `sort_axes`

```elixir
@spec sort_axes(Enum.t()) :: [{String.t(), term()}]
```

Sorts axes by `axis_order/0`, with anything unlisted after, then alphabetically.

---

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