Decides which control a component attribute gets in the property panel, and groups the attributes into sections — all as pure functions, so the no-code surface is testable without a browser.
Where the option lists come from
A Chelekom attribute almost never declares its own opts.values: it is "string" with a doc
string, and the real list of colours, sizes and variants lives in the component's
helpers[].discriminators — the axis clauses extracted from the kit's .eex source. That is why
the React editor could never have shown a variant dropdown: the data it needed was in a field the
editor was never sent.
features/1 folds both sources the way MishkaCmsCore.Runtime.Handlers.UiKitHandler does
(helper-derived values win over attr-declared ones) so widget/2 can offer a real select for the
~130 of 136 components that carry discriminators.
Widget resolution
boolean → :toggle
a known value list, ≤ 5 short entries → {:segmented, values}
a known value list → {:select, values}
integer / float → {:number, min, max, step}
string named like a colour, or editor hint → :color
a long doc, or editor hint → :textarea
type "global" (the `@rest` catch-all) → :global
type "struct" → {:struct, name}
anything else → :textThe contract a kit can write instead — opts.editor
Everything above is a GUESS, and a guess reads one thing: how this kit happens to spell its
attributes. icon gets the picker because the word is on a list. A kit that calls the same thing
glyph gets a text box, and no amount of adding names fixes that — it moves the failure to the
next kit.
So a kit can say it outright. opts["editor"] on an attribute is read FIRST and is authoritative:
opts.editor | control | for |
|---|---|---|
"color" | a swatch | an actual colour value — a hex, a CSS colour |
"icon" | the searchable icon picker | an icon class name |
"media" | the media library | a picture the author picks |
"url" | an address field | a link the author types |
"number" | a stepper, honouring min/max/step | a count or a duration, EVEN IF its type is a string |
"toggle" | a switch | a flag, even if its type is a string |
"textarea" | a box with room | prose |
"select" | a dropdown, or pills when the list is short | drawn from the attribute's own values |
"columns" | a row of shapes | a layout count, drawn from values |
"text" | a plain text box | the kit OVERRULING a guess — an attribute called icon that holds something else |
Three properties make this safe for a kit nobody here has seen:
- A declaration always wins. It is read before every heuristic and before the value list, so a kit is never argued with about its own attributes.
- An unknown value degrades, it does not break. A bundle written against a later version of
this editor —
"editor" => "colour-wheel"— loses the refinement and falls back to inference. It does not raise, and it does not collapse to a text box. - Declaring nothing is still supported. The heuristics below are the fallback, not the plan, and they stay for exactly that reason: a kit that says nothing must still get sensible controls.
editors/0 is the accepted list, so a kit author can read it rather than guess at it.
The two sections a form adds
Everything above answers "what control does this attribute get". Two sections answer a different
question — "what does this node need in order to be a working form" — and neither is derived from a
kit's attribute list, so both are declared: form_sections/0 returns them as ordinary
builder:inspector:sections descriptors carrying their controls in config, seeded into the
registry by MishkaCmsCoreWeb.Live.Admin.Builder.Panels.core_inspector_sections/0. An extension
contributing a section of its own writes the same shape.
Form shows on a node that declares a page form (form_node?/1). Its one real control is the
seed — the single word every other name on the form is derived from — so rename_ops/3 writes all
five of MishkaCmsCore.Runtime.PageBuilder.PageForms.Naming.derived/0 as one batch through
MishkaCmsCore.Runtime.PageBuilder.EditorSession.apply_ops/4, with the token rename_token/4
picks. One history entry, and the five can never disagree because nothing edits one of them alone.
seed_refusal/3 is what stops a rename landing on a name another form is already using, and
rename_warning/2 says what stored code a rename would stop.
data-success is on the form node, so it is a Form control rather than a Validation one even
though it is about what happens after a submit.
The rest of the Form panel
form_state/2 says which of these a node gets. A working form also has Form · Phoenix
(form_names/1, phoenix_form_controls/2, address_ops/2), a field inside one has Field options
(field_controls/3) and a button inside one has Send button (send_controls/4). Each control is
offered only where the node forwards its attribute (Fields.takes?/3), and Props and Advanced
leave form_panel_attrs/0 to these sections.
Validation shows on a bound field inside a form (validated_node?/2). Every control writes one
ordinary HTML constraint attribute — required, minlength, maxlength, min, max, pattern,
plus data-format and data-confirms for the two things HTML has no word for. There is no rule
store: MishkaCmsCore.Runtime.PageBuilder.PageForms reads these back off the markup at compile
time and MishkaCmsCore.Runtime.PageForm.Rules executes them, so the browser and the server are
reading one declaration and cannot disagree about it.
Summary
Functions
The slots that can be added to a component as <:name> blocks — every declared slot except
inner_block, for the reason given in inner_block?/1.
The ops "Send to another address" issues for what was typed: an https address writes action,
method="post", csrf_token={false} and the form's phx-trigger-action (Naming.trigger/1)
together, and removes a target, so the result opens in the tab it was sent from. It also turns
multipart on when a file field sits inside the form in tree (FormPlacement.files_ops/3). An
empty one removes the four. Anything else is refused with a sentence.
Whether the form node forwards all four attributes "Send to another address" writes.
Whether the attribute takes an ATOM rather than a string.
A component's attribute definitions, however the caller happens to hold the component.
The Events-tab handlers this form's phx-change and phx-submit currently point at.
The key this node binds in the form it sits inside, or nil.
Whether an attribute holds a collection rather than one value.
The value the component falls back to when the attribute is unset.
The attribute's documentation string, used as the control's tooltip.
Every value a kit may put in opts.editor, so it can be read rather than guessed at.
The control a RAW HTML attribute deserves, decided from the tag it is written on.
The option values available per attribute name, folded from the component's helper
discriminators and its attributes' own opts.values.
The autofill a field has when it names none, from the kind of field it is: email for an email
field, tel, url, current-password, and on for anything else.
The field bindings that name an event, offered only to authors who may write its code.
The options of a field inside a form, each where the field forwards its attribute
(Fields.takes?/3). Autofill shows its default for the field's kind (field_autofill/2), or
off when the form it is in (form:) turns Autofill off. With code_allowed?: true the three
field bindings that name an event come last; nobody else sees them. Every control with no node.
The Form section's controls, in the order the panel draws them.
The Form section's controls for the working form node: form_controls/0, then How it works
where the form can work as they type (FormPlacement.as_typed_offered?/2). session is the
editing session.
The names this working form carries that only Form name changes, as {name, text}, with nil
for one it does not carry.
Whether the Form section applies to this node — it names a page assign to keep its state in.
Every attribute the Form panel writes on a working form, so Props and Advanced leave them to it:
the five names and :let, the Success message, How it works, the Form · Phoenix options, the
four "Send to another address" writes and multipart.
The part this node plays in a form, for the Inspector's header: :form, :field, :submit (a
button that sends the form it is in), :not_submit (a button in a form that does not send it),
:button (a button outside every form, or one whose computed type cannot say) or nil.
The two form sections, as builder:inspector:sections descriptors carrying their controls.
The seed this form is currently using, or nil when it names no params.
What this node is as a form, for the Inspector
What a form does, in two sentences for someone who does not write code: what happens while a
visitor types, and where it goes when they press Send, or as they type when the form has no Send
button (PageForms.as_typed?/1). The second reads the audit's own check
(FormChecks.receiver/2), so it never says more than form-no-destination does. record is the
page, read for its stored handlers. No event name is in either sentence.
Groups a component's attributes into the panel's sections. Anything that matches no group heading
lands in "Properties", and global attributes are pulled out entirely — they are the collapsed
"HTML attributes" editor, not a property.
The section headings, in panel order.
A component's helper definitions — where the discriminator axes live.
Whether a file field sits inside form: an <input type="file">, a live_file_input, or a
component whose markup renders one.
The icon name inside a value of an icon_map?/1 attribute, whichever shape it arrives in — the
bundle's __atom_map__ envelope, a real map, or the source of the map expression the editor
itself writes. nil when there is no icon in it.
Whether this attribute carries its icon inside a MAP rather than as a bare name.
Whether a slot definition is Phoenix's inner_block — the content written between a component's
tags rather than in a <:name> block of its own.
A human label for an attribute — start_icon becomes "Start icon". The kit ships no labels, only
names, so this is derived rather than looked up.
An attribute's declared name.
An attribute's option map (opts), always a map.
The nearest form above this node, or nil.
The Form · Phoenix section's options for the form node: the ones whose attribute it forwards to
the form it renders (Fields.takes?/3). Open result in is left out while the form sends to
another address, which opens the result in the same tab. Every one of them with no node.
The ops a rename issues, ready for MishkaCmsCore.Runtime.PageBuilder.EditorSession.apply_ops/4,
with token inside both event names (nil writes the untagged names).
The token a rename of this form writes.
What a rename would break, or nil.
Whether the component ever renders what is written INSIDE this slot.
Whether the attribute must be set. True on about 1.4% of the Chelekom kit; rendered as *.
The @rest keys a component's own body actually reads, as offerable attributes.
The stored handler names a rename moves onto token: each untagged event name of origin that a
stored handler answers by exact name, mapped to the tagged name of the same kind. Empty for a
nil token. MishkaCmsCore.Runtime.PageBuilder.PageForms.follow/3 applies it.
The op a control issues for value, ready for
MishkaCmsCore.Runtime.PageBuilder.EditorSession.apply_op/3.
What a control currently reads on this node — true/false for a switch, the attribute's text
otherwise, and the seed for the one control that is not an attribute.
Why this seed cannot be used on this form, or nil.
The options of a Send button: Text while sending where the button forwards it, and Button type
where its type is written through type (type_attr, from Fields.type_attr/3). send? is
whether it sends its form now, what Button type shows when no type is written.
A component's slot definitions.
A component's template, however the caller happens to hold the component.
An attribute's declared type, defaulting to "string".
Everything this node was told it must have and has not got — its required properties left blank, and the required slots nobody added.
Whether the Validation section applies — a node bound to a key of the form it sits inside.
The Validation section's controls, in the order the panel draws them.
The control an attribute should get. values comes from features/1; pass %{} when unknown and
the attribute falls back to a plain text input.
Types
Functions
The slots that can be added to a component as <:name> blocks — every declared slot except
inner_block, for the reason given in inner_block?/1.
@spec address_ops( MishkaCmsCore.Runtime.PageBuilder.Tree.t(), MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), String.t() | nil, [map()] ) :: {:ok, [map()]} | {:error, String.t()}
The ops "Send to another address" issues for what was typed: an https address writes action,
method="post", csrf_token={false} and the form's phx-trigger-action (Naming.trigger/1)
together, and removes a target, so the result opens in the tab it was sent from. It also turns
multipart on when a file field sits inside the form in tree (FormPlacement.files_ops/3). An
empty one removes the four. Anything else is refused with a sentence.
@spec address_takes?(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), [map()]) :: boolean()
Whether the form node forwards all four attributes "Send to another address" writes.
Whether the attribute takes an ATOM rather than a string.
It matters at the point of writing: kind="danger" and kind={:danger} are different values, and
a component whose clauses match :danger falls through every one of them when handed the string —
silently, to whatever its catch-all renders. Three attributes in the Chelekom kit are atoms, and
one of them is chelekom-alert's kind, which decides the alert's entire colour.
A component's attribute definitions, however the caller happens to hold the component.
@spec bound_handlers(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), [map()]) :: [ String.t() ]
The Events-tab handlers this form's phx-change and phx-submit currently point at.
@spec bound_key( MishkaCmsCore.Runtime.PageBuilder.Tree.t(), MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() ) :: atom() | nil
The key this node binds in the form it sits inside, or nil.
Whether an attribute holds a collection rather than one value.
The value the component falls back to when the attribute is unset.
The attribute's documentation string, used as the control's tooltip.
@spec editors() :: [String.t()]
Every value a kit may put in opts.editor, so it can be read rather than guessed at.
Anything outside this list is not an error — it degrades to inference — but it is also not doing anything, which is worth being able to check for.
The control a RAW HTML attribute deserves, decided from the tag it is written on.
element_widget("img", "src") is a picture chosen from the library; element_widget("iframe", "src") is an address typed by hand. Anything this has no opinion about falls through to the name
heuristics widget/3 uses, and then to a text box.
The option values available per attribute name, folded from the component's helper
discriminators and its attributes' own opts.values.
Accepts a palette entry (atom keys), a raw component row, or anything shaped like one.
@spec field_autofill(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), [map()]) :: String.t()
The autofill a field has when it names none, from the kind of field it is: email for an email
field, tel, url, current-password, and on for anything else.
@spec field_code_controls() :: [control()]
The field bindings that name an event, offered only to authors who may write its code.
@spec field_controls( MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | nil, [map()], keyword() ) :: [ control() ]
The options of a field inside a form, each where the field forwards its attribute
(Fields.takes?/3). Autofill shows its default for the field's kind (field_autofill/2), or
off when the form it is in (form:) turns Autofill off. With code_allowed?: true the three
field bindings that name an event come last; nobody else sees them. Every control with no node.
@spec form_controls() :: [control()]
The Form section's controls, in the order the panel draws them.
@spec form_controls(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), map()) :: [ control() ]
The Form section's controls for the working form node: form_controls/0, then How it works
where the form can work as they type (FormPlacement.as_typed_offered?/2). session is the
editing session.
@spec form_names(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()) :: [ {String.t(), String.t() | nil} ]
The names this working form carries that only Form name changes, as {name, text}, with nil
for one it does not carry.
@spec form_node?(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()) :: boolean()
Whether the Form section applies to this node — it names a page assign to keep its state in.
@spec form_panel_attrs() :: [String.t()]
Every attribute the Form panel writes on a working form, so Props and Advanced leave them to it:
the five names and :let, the Success message, How it works, the Form · Phoenix options, the
four "Send to another address" writes and multipart.
@spec form_part(map(), MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | nil) :: :form | :field | :submit | :not_submit | :button | nil
The part this node plays in a form, for the Inspector's header: :form, :field, :submit (a
button that sends the form it is in), :not_submit (a button in a form that does not send it),
:button (a button outside every form, or one whose computed type cannot say) or nil.
editor is anything carrying the document's tree and the palette's components.
@spec form_sections() :: [MishkaCmsCore.Builder.Descriptor.t()]
The two form sections, as builder:inspector:sections descriptors carrying their controls.
Sorted ahead of the styling groups, so a form's own name is near the top of the panel rather than under the styling an author reaches for later. The panel draws them above the breakpoint strip: every control here writes a plain attribute, which has one value for the whole page.
@spec form_seed(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()) :: String.t() | nil
The seed this form is currently using, or nil when it names no params.
@spec form_state(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | nil, [map()]) :: :page_form | {:idle, boolean()} | :none
What this node is as a form, for the Inspector:
:page_form- a working form (PageForms.form?/1): Phoenix's<.form>or a kit wrapper wired to the page;{:idle, hands?}- a form that does not work yet;hands?is whether it can be made one where it stands (Fields.hands_form?/2), which a raw<form>cannot;:none- not a form.
@spec form_story(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), map() | nil) :: {String.t(), String.t()}
What a form does, in two sentences for someone who does not write code: what happens while a
visitor types, and where it goes when they press Send, or as they type when the form has no Send
button (PageForms.as_typed?/1). The second reads the audit's own check
(FormChecks.receiver/2), so it never says more than form-no-destination does. record is the
page, read for its stored handlers. No event name is in either sentence.
Groups a component's attributes into the panel's sections. Anything that matches no group heading
lands in "Properties", and global attributes are pulled out entirely — they are the collapsed
"HTML attributes" editor, not a property.
@spec groups() :: [String.t()]
The section headings, in panel order.
A component's helper definitions — where the discriminator axes live.
@spec holds_file_field?( MishkaCmsCore.Runtime.PageBuilder.Tree.t(), MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), [map()] ) :: boolean()
Whether a file field sits inside form: an <input type="file">, a live_file_input, or a
component whose markup renders one.
The icon name inside a value of an icon_map?/1 attribute, whichever shape it arrives in — the
bundle's __atom_map__ envelope, a real map, or the source of the map expression the editor
itself writes. nil when there is no icon in it.
Whether this attribute carries its icon inside a MAP rather than as a bare name.
A label is not always a word. chelekom-pagination declares its five arrows and its separator as
map, each defaulting to %{type: :icon, value: "hero-chevron-left"} so an author can put either
a glyph or a caption there. Read as a plain map attribute, every one of them drew a text box the
author had nothing to type into — reported as "we cannot select the icon".
The shape is asked of the DECLARED DEFAULT, which is the only thing that says what the component will do with the value; a kit that spells the same idea on a differently-named attribute is covered without being named.
Whether a slot definition is Phoenix's inner_block — the content written between a component's
tags rather than in a <:name> block of its own.
It is the one slot that must never be offered as something to add: <:inner_block> is rejected by
the HEEx compiler outright ("the slot name :inner_block is reserved"), because writing it is the
same as writing nothing and putting the content there directly. 82 of the kit's 120 slot-bearing
components name their only slot this way, so this is the common case, not an edge one.
A human label for an attribute — start_icon becomes "Start icon". The kit ships no labels, only
names, so this is derived rather than looked up.
An attribute's declared name.
An attribute's option map (opts), always a map.
@spec owning_form( MishkaCmsCore.Runtime.PageBuilder.Tree.t(), MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() ) :: MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | nil
The nearest form above this node, or nil.
@spec phoenix_form_controls(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | nil, [ map() ]) :: [ control() ]
The Form · Phoenix section's options for the form node: the ones whose attribute it forwards to
the form it renders (Fields.takes?/3). Open result in is left out while the form sends to
another address, which opens the result in the same tab. Every one of them with no node.
@spec rename_ops( MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), String.t() | nil, String.t() | nil ) :: [map()]
The ops a rename issues, ready for MishkaCmsCore.Runtime.PageBuilder.EditorSession.apply_ops/4,
with token inside both event names (nil writes the untagged names).
All five in one batch, because five separate ops would be five undo steps and four of them would leave the page in a state where the form's id, params, assign and events disagree.
@spec rename_token( MishkaCmsCore.Runtime.PageBuilder.Tree.t(), MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), [map()], keyword() ) :: String.t() | nil
The token a rename of this form writes.
- a tagged form keeps its token, so its stored handlers are still found by it;
- an untagged form whose stored handlers answered the
:originform's event names by exact name getsnil, the untagged names, so typing the old name back reattaches them. Withcode_allowed?: trueit gets a fresh token instead, andretagged/3names the handlers that move to it; - any other untagged form gets a fresh token.
events are the page's Events-tab entries.
Options
:origin- the form as it was when this rename began. Defaults tonode.:code_allowed?- whether the author may write the page's code. Defaults tofalse.
@spec rename_warning( MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), map(), boolean() ) :: String.t() | nil
What a rename would break, or nil.
node is the form as it was when the rename began, and code is the page's stored code,
%{events: [...], infos: [...], helpers: [...]}.
- An untagged form names the handlers that answer its event names exactly: they stop. With
code_allowed?they move with the form instead (retagged/3), so they raise nothing. - Any form: when a stored entry mentions the form's seed (
:contact_form,:contactor"contact"), a rename changesasandforunder it. - A tagged form's own handlers follow it by token, so they alone raise nothing.
Whether the component ever renders what is written INSIDE this slot.
Declaring a slot and rendering its body are two different things, and thirteen of the kit's slots
do only the first: chelekom-video declares :source and its markup is
<source :for={s <- @source} src={s.src} type={s.type} /> — it reads the slot's ATTRIBUTES and
never calls render_slot. So a card dropped into :source is accepted, is stored, appears in the
tree, and is drawn nowhere. The author is left looking at a slot with something in it and a canvas
that disagrees, with nothing anywhere saying why.
Two shapes count as rendering, because Phoenix writes multi-slots the second way:
{render_slot(@caption)}
<div :for={{item, i} <- Enum.with_index(@item, 1)}>{render_slot(item)}</div>READ OFF THE MARKUP, because nothing else knows. A slot declaration says a slot exists and what attributes it takes; whether its body reaches the page is a property of the template alone. That makes this a scan of source text rather than a lookup, with the limits that implies — a slot rendered by a helper this does not see, or through a binding it cannot follow, would be read as unrendered.
Which is why it answers true when it cannot tell. Being wrong the other way would refuse drops
into a slot that does take content, and a false "you cannot put anything here" is much worse than
the silence this exists to end: inner_block (always rendered by definition), a component with no
markup stored, and any slot this cannot make sense of are all treated as taking content.
Whether the attribute must be set. True on about 1.4% of the Chelekom kit; rendered as *.
The @rest keys a component's own body actually reads, as offerable attributes.
Why these are invisible otherwise
A kit declares the switch it supports through attr :rest, :global, include: ~w(animated), and
two things drop it on the way here: the bundle exporter does not carry an include: list, and
grouped/1 pulls global attributes out entirely — they are the collapsed "HTML attributes"
editor, not properties. So a skeleton's animated had nowhere to appear, and neither did fifteen
other components' switches: pinging and circle on a badge and three buttons, disabled on six
field components, four on the pagination.
Derived from the body, not from a list
What the component CONSULTS is the honest answer to "what does it support", and it needs no kit
change and no re-export: @rest[:animated] in the template is the component saying it reads that
key. A name the body never mentions is not offered, so this cannot invent a control that does
nothing.
The TYPE comes from the use site for the same reason. @rest[:x] && "class" is a flag and gets a
toggle; @rest[:x] || "fallback" or attr={@rest[:x]} is a value and gets a text box — which is
what keeps a blockquote's cite a URL and a card's role a word rather than two switches.
Anything the component also declares properly wins: those are subtracted, so nothing appears twice.
@spec retagged(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), String.t() | nil, [ map() ]) :: %{ required(String.t()) => String.t() }
The stored handler names a rename moves onto token: each untagged event name of origin that a
stored handler answers by exact name, mapped to the tagged name of the same kind. Empty for a
nil token. MishkaCmsCore.Runtime.PageBuilder.PageForms.follow/3 applies it.
@spec rule_op( MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), control(), String.t() | boolean() | nil ) :: map()
The op a control issues for value, ready for
MishkaCmsCore.Runtime.PageBuilder.EditorSession.apply_op/3.
Turning a rule off DELETES its attribute rather than writing an empty one: required="" is still
required to a browser, and minlength="" reads back as no rule on the server, so an emptied
control that only wrote would leave the two disagreeing.
Every control here writes one attribute, so the Form section's seed is not one of them: it writes
five at once and goes through rename_ops/2 instead. A caller that dispatches the whole of
form_controls/0 through here has to send the seed the other way, and gets a FunctionClauseError
naming the control if it does not.
@spec rule_value(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), control()) :: String.t() | boolean() | nil
What a control currently reads on this node — true/false for a switch, the attribute's text
otherwise, and the seed for the one control that is not an attribute.
@spec seed_refusal( MishkaCmsCore.Runtime.PageBuilder.Tree.t(), MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), String.t() | nil ) :: String.t() | nil
Why this seed cannot be used on this form, or nil.
Answered against the MINTED name rather than the typed one, so My Form and my_form are refused
as the same answer they would both become.
@spec send_controls( MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | nil, [map()], boolean(), String.t() | nil ) :: [control()]
The options of a Send button: Text while sending where the button forwards it, and Button type
where its type is written through type (type_attr, from Fields.type_attr/3). send? is
whether it sends its form now, what Button type shows when no type is written.
A component's slot definitions.
A component's template, however the caller happens to hold the component.
An attribute's declared type, defaulting to "string".
@spec unmet( MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), MishkaCmsCore.Runtime.PageBuilder.Tree.t(), %{optional(String.t()) => map()} ) :: [{:attr | :slot, String.t()}]
Everything this node was told it must have and has not got — its required properties left blank, and the required slots nobody added.
READ OFF THE DECLARATION, never off a list of names. A kit this editor has never seen says which of
its properties are required the same way Chelekom does — opts.required on the attribute, and
Phoenix's own :required on a slot, which is the only option it allows inside one. So this asks
the component that is actually on the page and works for a component nobody here has heard of.
ONE ANSWER, THREE READERS. Publishing is refused when this is non-empty, the layers rail draws a badge from it, and the Inspector marks the field — and those three used to be two implementations of the same rule with the rail not implementing it at all. A gate the author cannot see until they press Publish is a gate that finds the problem at the worst moment; a gate that disagrees with the badge beside it is worse.
A slot's properties are declared on the component that OWNS it, which is why the tree is needed to
answer for one. inner_block is never counted: it is whatever is written between the tags, and
<:inner_block> is not something anybody adds.
@spec validated_node?( MishkaCmsCore.Runtime.PageBuilder.Tree.t(), MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() ) :: boolean()
Whether the Validation section applies — a node bound to a key of the form it sits inside.
An unbound node in a form has no rules to carry, and a bound node with no form above it has nothing
to validate it: MishkaCmsCore.Runtime.PageBuilder.PageForms.FormPlacement wraps that one on the
way in and the audit rail reports the ones that arrived some other way.
@spec validation_controls() :: [control()]
The Validation section's controls, in the order the panel draws them.
The control an attribute should get. values comes from features/1; pass %{} when unknown and
the attribute falls back to a plain text input.