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.
Summary
Functions
Every shipped block, in the order the panel lists them — the whole catalogue.
The categories present, in first-appearance order — the panel's filter chips.
One block by id, or nil.
Filters ANY list of block-shaped entries the way search/2 filters the built-in ones.
Every block the given section can use, in the order the panel lists them.
The blocks matching a category and a search string.
Types
Functions
@spec all() :: [block()]
Every shipped block, in the order the panel lists them — the whole catalogue.
The categories present, in first-appearance order — the panel's filter chips.
One block by id, or nil.
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.
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.
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.