# `MishkaCmsCore.Builder.PaletteEntry`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/builder/palette_entry.ex#L5)

A component in the builder's palette — from the database, or contributed by an extension.

## The two paths, one shape

Most palette entries are `Component` rows: an extension that ships template-shaped components
(a post card, a hero) installs them as ordinary rows and everything downstream — compile,
resolution, palette, inspector, examples — already works with no editor changes at all.

Some components cannot be a stored HEEx template: "Latest posts", streaming live data, is a real
LiveView component in the extension's own code. Those register on the `builder:components` hook as
descriptors carrying a `render` ref, and are inserted as a module call rather than a
`<.component component_name=…>` wrapper.

Both arrive here as the same struct, so the palette, the search, the categories and the property
panel are written once.

## `render` is never a closure

A descriptor travels through a `MishkaInstaller` hook payload, which is plain-map only — the same
rule `MishkaCmsCore.Dashboard.Widget` follows. `render` is `{module, function}`, and the template
the editor emits for it is a fully-qualified module call.

## Serialization is whitelisted

A module call in a template is arbitrary code at compile time. `template/2` will only emit a call
for a module that a registered descriptor actually named, so the editor cannot be talked into
emitting a call to anything else — defence in depth, since templates are admin-authored anyway.

# `t`

```elixir
@type t() :: %MishkaCmsCore.Builder.PaletteEntry{
  attrs: list() | nil,
  description: String.t() | nil,
  doc_url: String.t() | nil,
  examples: list() | nil,
  features: map() | nil,
  form: map() | nil,
  furnishing: map() | nil,
  global?: boolean() | nil,
  helpers: list() | nil,
  icon: String.t() | nil,
  key: String.t(),
  label: String.t() | nil,
  name: String.t(),
  priority: integer() | nil,
  reads: any() | nil,
  render: any() | nil,
  slots: list() | nil,
  stateful?: boolean() | nil,
  template: String.t() | nil,
  type: String.t() | nil
}
```

# `build`

```elixir
@spec build(map()) :: {:ok, t()} | {:error, term()}
```

Builds a descriptor, returning `{:error, _}` instead of raising — for untrusted input.

# `builder`

# `concealed?`

```elixir
@spec concealed?(map()) :: boolean()
```

Whether this component renders its own content closed — a dialog, a drawer, a collapsible.

A DECLARATION FIRST, a guess only when there is none. `opens/1` reads what the component itself
says; the markup scan below is what answers for the hundred and thirty-six that were written
before there was anything to say it with. Same contract as `opts["editor"]` on an attribute and
`extra.furnishing` on a row: what the author declared wins, and inference is the fallback.

The guess is deliberately a generous one: the cost of a wrong yes is a glyph on a row that did not
need it, and the cost of a wrong no is content the author cannot see at all.

# `enforce_keys`

# `enforce_keys`

# `example`

A sample %MishkaCmsCore.Builder.PaletteEntry{} populated from defaults + type-based fallbacks.

# `keys`

# `keys`

# `module_component?`

```elixir
@spec module_component?(t() | map()) :: boolean()
```

Whether this entry renders through an extension module rather than a stored template.

# `new!`

```elixir
@spec new!(map()) :: t()
```

Builds a descriptor, raising on an invalid one.

Extensions declare these in module attributes, so a malformed descriptor fails the build rather
than the page — the `Dashboard.Widget` precedent.

# `opens`

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

The open/close commands this component declares on its own root element.

## Why a component has to say it

Because "open it" is not one thing. A dialog is `display:none` on the root AND `display:none` again
on the panel inside it, so revealing the root paints the backdrop over a page and shows no dialog.
A drawer is fully displayed and held off-screen by `-translate-x-full`. A dropdown is opened by a
JavaScript hook that removes a boolean attribute and adds three classes. A navbar wants one class
toggled on the very element the editor is already pointing at. One generic `JS.show` is right for
exactly one of those shapes and silently does nothing for the rest — which is what it did.

The component is the only thing that knows which of those it is, so it is the thing that says:

    <div id={@id} data-pb-open={show_modal(@id)} data-pb-close={hide_modal(@id)} class={[…]}>

The value is an ordinary `Phoenix.LiveView.JS` command — the same shape as the `data-cancel` the
kit's own dialogs already carry, and `chelekom-mega-menu` already ships this exact pattern under
its own name (`phx-open-mega`, run with `JS.exec/2` from its trigger). A button aimed at the
component compiles to `JS.exec("data-pb-open", to: ".pb-xxxxxxxx")`, so what runs is the
component's own sequence — every transition, the body scroll lock, the focus trap — rather than
the editor's guess at it.

Declaring nothing still works: a component that renders closed with no declaration keeps the
layers glyph on inference, and a click aimed at it keeps compiling to the plain `JS` verb.

Returns `%{verb => attribute}` for the verbs actually declared, `%{}` for none.

# `optional`

```elixir
@spec optional(t() | map(), atom()) :: term()
```

Reads an optional field off an entry, whichever shape it arrived in.

THE PALETTE HOLDS TWO SHAPES AND ALWAYS WILL. The core builds a plain map with everything it
learned in the read that filled the palette; a plugin contributes a `%PaletteEntry{}`, which is
the documented way to add one and carries only the fields this struct declares. Asking a struct
for `entry[:has_examples?]` raises — structs have no `Access` — so a single contributed component
took the whole palette down, and with it the editor.

Answers `nil` for a field the entry does not carry, which for every optional field is the honest
answer: a contributed component has no examples to browse until it says it does.

# `permitted?`

```elixir
@spec permitted?(String.t(), MapSet.t(module())) :: boolean()
```

Whether `template` only calls modules in `whitelist`. A template naming an unregistered module is
refused rather than compiled.

# `reads`

```elixir
@spec reads(map()) :: MapSet.t()
```

Every assign a component's own markup and helpers read.

Public because the palette builds its entries as plain maps rather than through `new!/1`, and this
is what lets it carry the answer instead of the source: `helpers` is 87% of a palette's weight and
the only runtime questions asked of it are this scan and `Inspector.features/1`, both of which are
computed once when the palette is read.

# `seeded`

```elixir
@spec seeded(map(), [String.t()], :blank | :preview, [map()]) :: String.t()
```

The required attributes a component cannot render without, written as HEEx attribute text.

A REQUIRED attribute with no default is an assign the component's own template reads and nothing
supplies — `@source` on `chelekom-video`, `@id` on `chelekom-banner` — and reading a missing assign
raises `KeyError`, which takes the whole canvas LiveView down with it the instant the component is
dropped. Seeding those attributes is what makes dropping one survivable; the value is a placeholder
the author then edits in the inspector, and every other attribute is left alone so the component's
own defaults keep applying.

`style` is `:blank` for PLACEMENT and `:preview` for the wizard. Placement leaves a required value
empty on purpose — the inspector marks it "Required" and the save refuses until the author fills
it, which is the guard against publishing an incomplete page. A preview has no author to wait for,
so it uses a real placeholder: an empty `src` renders as "needs src" rather than as an image.

Public because the palette is not the only place that has to produce a RENDERABLE component:
`MishkaCmsCore.Runtime.PageBuilder.Wizard` composes one too, and without the same seeding its
preview showed "needs id" instead of the component, which made every option in the wizard look
broken.

`skip` names attributes the caller is writing itself, so the two cannot both emit one.
`components` is the palette, which decides whether a component calling another owns a form.

# `seeded_slots`

```elixir
@spec seeded_slots(map(), :blank | :preview) :: String.t()
```

The required slots a component cannot render without, written as HEEx slot text.

A REQUIRED SLOT is the same trap wearing different clothes — see `seeded/2`. Public for the same
reason.

# `shapes_children?`

```elixir
@spec shapes_children?(map() | String.t() | nil) :: boolean()
```

Whether this component styles the children an author puts inside it.

A component that writes `[&>*]:rounded-none` means the element immediately inside it, and the
editor's own wrapper around each of those children is what the rule lands on instead. That is why
a `chelekom-button-group` drew three separately rounded buttons on the canvas and one joined
control on the page: the CSS was never reaching the buttons at all.

Read off the TEMPLATE AND THE HELPER BODIES, once, where the source is still in hand — see the
note on `reads/1` in `palette_entry/1`'s caller for why it cannot be asked again later.

A bare string is asked the same question: `<div class="[&>*]:rounded-none">` is an author making
the same promise by hand, and the canvas has to keep it the same way.

# `template`

```elixir
@spec template(t() | map(), String.t() | nil, [map()]) :: String.t()
```

The HEEx this entry inserts.

A database component becomes the wrapper form every stored template already uses; a descriptor
with a `render` ref becomes a module call.

A TENANT component is bound to the page's own site; a GLOBAL one is bound to `"Global"` whatever
page it lands on. That distinction is not cosmetic: the compiled module's name is hashed from this
attribute, so binding a global component to the page's site named a module nobody ever built and
the canvas showed the runtime's red "failed to render" the moment it was dropped.

`components` is the palette, which decides whether a component calling another owns a form.

# `whitelist`

```elixir
@spec whitelist([t() | map()]) :: MapSet.t(module())
```

The set of modules the editor is permitted to emit a call to — every module a registered
descriptor named, and nothing else.

---

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