MishkaCmsCore.Runtime.PageBuilder.Blocks (Mishka CMS Core v0.0.1)

Copy Markdown View Source

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

block()

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

Functions

all()

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

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

categories(section \\ nil)

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

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

fetch(id)

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

One block by id, or nil.

filter(entries, category, query)

@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(section)

@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(category, query)

@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.