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

Copy Markdown View Source

The plain HTML an author reaches for constantly — a section, a column grid, a heading, a list.

Why this exists

The editor had two ways to put something on a page and neither of them was "an empty box". The palette lists a UI kit's components; MishkaCmsCore.Runtime.PageBuilder.Blocks lists whole finished sections. To get a <section> of your own you had to drop a Hero and delete everything inside it, or open the Code window and type. Every other builder — Elementor's Layout group, Gantry's structural particles — puts the primitives first, because they are what a page is actually made of; the finished sections are the shortcut, not the foundation.

What an element is

The same %{id, title, category, description, template} shape a block has, so one panel lists both and one search covers both. The difference is intent: a block arrives finished and is edited down, an element arrives empty and is built up.

Plain HTML and Tailwind utilities only, for the same reason blocks are: an element that reached for a kit component would fail on an install without that kit, and every node here has to be something the Inspector already knows how to style. <.form> and <.input> are inside that rule rather than outside it: they are Phoenix.Component.form/1 and MishkaCmsCore.CoreComponents.input/1, imported into every compiled page and resolved by the editing canvas, so they are there on every install — the standing <.link> already has.

What the Forms elements are

A form is a real Phoenix form: for={assigns[:form_form]} names the page assign that holds what the visitor typed, as={:form} scopes the params, and the two phx-* names are the handlers MishkaCmsCore.Runtime.Compilers.LiveViewCompiler generates from the markup. All five names come from one seed, minted and renumbered by MishkaCmsCore.Runtime.PageBuilder.PageForms.Naming, so a second form on the page cannot share the first one's assign. el-form therefore ships with a field and a button in it rather than empty: the five attributes are the point of it, and an empty one teaches none of them.

The bare controls — el-input, el-textarea, el-select, el-checkbox, el-radio-group — keep a literal name, because a palette card and a hover preview render them on their own, outside any form. MishkaCmsCore.Runtime.PageBuilder.PageForms.FormPlacement rewrites that name into a binding on the way in, and wraps the control in a form first when it is dropped where there is none.

Two rules that decide whether these are usable at all

Containers ship empty, and the canvas draws them anyway. An empty <section> is zero pixels tall, so without help it cannot be clicked, cannot be dropped into, and looks like the drop failed. MishkaCmsCoreWeb.Live.Admin.Builder.CanvasLive gives any childless container a dashed outline and a minimum height while editing — never in preview, and never for a void tag like <img>, which is childless on purpose.

Text elements ship with words in them. An empty <p> has the same problem and no good answer: a paragraph is its text. They arrive saying what they are, and the first thing an author does is type over it — which is exactly what the inline text editor is for.

Summary

Functions

Every shipped element, in the order the panel lists them.

The categories the section's elements fall into, in order.

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

One element by id, or nil.

Elements matching a search string, nil or "" meaning all of them.

Types

element()

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

Functions

all()

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

Every shipped element, in the order the panel lists them.

The whole catalogue, whatever section it belongs to — which is what a check over "every element" has to mean. Use for_section/1 to ask what a particular document may place.

categories(section \\ nil)

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

The categories the section's elements fall into, in order.

for_section(section)

@spec for_section(String.t()) :: [element()]

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

MOST ELEMENTS BELONG EVERYWHERE and say nothing; a <section> is a <section> in all four documents. The ones that declare sections are the exception, and they are exceptions for a reason a reader can check: {@inner_content} names an assign only a LAYOUT is handed, so offering it on a page would be offering a document that will not compile.

A section has to be NAMED. It used to be an optional argument defaulting to nil, and nil meant "only the ones that belong everywhere" — so the catalogue-wide checks, which pass no section, quietly stopped covering the three elements that declare one.

get(id)

@spec get(String.t()) :: element() | nil

One element by id, or nil.

Unfiltered on purpose: this answers "what is el-inner-content", which a drop that has already happened needs regardless of which panel offered it.

search(query, section \\ nil)

@spec search(String.t() | nil, String.t() | nil) :: [element()]

Elements matching a search string, nil or "" meaning all of them.

Title and description are both searched, so "column" finds the grids and "sidebar" finds aside — an author looking for a thing rarely knows what the tag is called.