# `MishkaCmsCore.Runtime.PageBuilder.Blocks`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/page_builder/blocks.ex#L5)

Whole sections an author drops in one click — a hero, a feature row, a pricing table.

The palette lists 183 components and the Studio's Presets tab is a catalogue of whole-page themes
that nothing on disk supplies. Between the two there was nothing: to get a landing page you placed
a grid, then a heading, then a paragraph, then a button, then set the column count, and repeated it
for every band down the page. Elementor and Gantry both answer this with a section library, and it
is the single thing that decides whether someone who is not a developer finishes a page.

## Why these are plain HTML and Tailwind

A block that reached for `chelekom-card` would be a block that fails on an install without
Chelekom, and the whole point of the kit-agnostic work is that no part of the editor assumes one
kit. Every block here is `div`/`h2`/`p`/`a` with utility classes, so it renders on any install,
and every node inside it is an ordinary element the Inspector already knows how to style. An
author who does have a kit can replace any piece afterwards.

## What a block is

A `%{id, title, category, template}` map. `template` is HEEx that `Tree.parse/1` accepts, and it
may have several roots — `Tree.insert/4` splices them into the sibling list as ONE undoable op, so
a fourteen-node hero arrives and leaves in a single step.

Blocks are code, not content: they ship with the CMS, are the same for every tenant, and are never
edited in place. Anything an author saves for reuse is a `MishkaCmsCore.Runtime.Snippet`, which is
a different thing with a different lifetime.

## Why every block opens with a band tag

`MishkaCmsCore.Runtime.PageBuilder.Placement` reads band-ness off the head node's tag, and a
component head is not a band. A block whose template started with `<.form>` or a `<div>` would be
treated as a leaf and land inside whatever was clicked rather than beside the other slices of the
page, so every block here opens with a `<section>`, `<header>`, `<footer>` or `<main>`.

## What a form block is

A real form rather than a picture of one. `<.form>` is `Phoenix.Component.form/1` and `<.input>` is
`MishkaCmsCore.CoreComponents.input/1`; both are imported into every compiled page and both are
resolved by the editing canvas, so a form block renders the same on the palette card, on the canvas
and on the published page — on an install carrying no UI kit at all. An author who does have one
swaps a field for the kit's own through the Inspector, per site.

Five attributes carry everything a form needs and all five come from one seed the author types
once: `contact` gives `id="contact-form"`, `as={:contact}`, `for={assigns[:contact_form]}`,
`phx-change="contact_validate"` and `phx-submit="contact_submit"`.
`MishkaCmsCore.Runtime.PageBuilder.PageForms.Naming` mints and renumbers them, so dropping one
block twice gives `contact` and `contact_2` rather than two forms sharing one page assign, and
gives each form a token as it lands (`contact-k7m2q-validate`, `contact-k7m2q-submit`). The page
compiler reads the markup back through `MishkaCmsCore.Runtime.PageBuilder.PageForms` and generates
both handlers, so a dropped form works with no Elixir written anywhere.

What is checked is the HTML constraint attributes already on the field — `required`, `minlength`,
`maxlength`, `data-format` — declared once, enforced by the browser and re-checked on the server,
so the two cannot disagree. `data-success` on the form node is what the visitor is told afterwards.

# `block`

```elixir
@type block() :: %{
  id: String.t(),
  title: String.t(),
  category: String.t(),
  description: String.t(),
  template: String.t()
}
```

# `all`

```elixir
@spec all() :: [block()]
```

Every shipped block, in the order the panel lists them — the whole catalogue.

# `categories`

```elixir
@spec categories(String.t() | nil) :: [String.t()]
```

The categories present, in first-appearance order — the panel's filter chips.

# `fetch`

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

One block by id, or `nil`.

# `filter`

```elixir
@spec filter([block()], String.t() | nil, String.t() | nil) :: [block()]
```

Filters ANY list of block-shaped entries the way `search/2` filters the built-in ones.

The palette merges an extension's contributions in beside these, and a contributed block has to
answer the category chips and the search box exactly as a built-in one does — which it cannot if
the merge happens after the filtering.

# `for_section`

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

Every block the given section can use, in the order the panel lists them.

MOST BLOCKS BELONG EVERYWHERE and say nothing; a hero is a hero in all four documents. The ones
that declare `sections` are the exception, and they are exceptions for a reason a reader can
check: the three form blocks ship `phx-change` and `phx-submit` bound to handlers, and
`MishkaCmsCore.Runtime.Compilers.LiveViewCompiler` is the only compiler that emits a
`handle_event/3` — a Layout, a Component and an error page have no `events` column to hold one.
Dropping one of those anywhere else produced markup whose form could never be submitted.

The same rule `MishkaCmsCore.Runtime.PageBuilder.Elements.for_section/1` applies, for the same
reason and with the same requirement that the section be named rather than defaulted.

# `search`

```elixir
@spec search(String.t() | nil, String.t() | nil) :: [block()]
```

The blocks matching a category and a search string.

`"all"` and a blank query both mean "everything", so the panel can pass its controls straight
through without deciding what an empty filter means.

---

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