Designer for authors
The contracts a module or a theme uses to extend the page builder, with the schema control reference.
This is the guide for extending the Designer, not for implementing it. The rule
that runs through the whole page: every registration here is inert without
the Designer. You register in your own boot(), and if the module is not
installed nothing happens. A module never depends on the page builder to work.
Where the contracts live#
They are in the core, next to the other registries. The Designer module consumes them.
| Contract | For |
|---|---|
App\Support\Widgets |
Draggable elements in the editor |
App\Support\Designer\Surfaces |
Screens whose middle the Designer can replace |
App\Support\Designer\Collections |
Data sources for the loop element |
App\Support\Designer\DynamicTags |
Dynamic values in controls |
App\Support\Designer\Conditions |
Display rules per node and per document |
App\Support\Designer\FormActions |
Actions after a form is submitted |
App\Support\Designer\Tokens |
Reading the tokens of the active theme |
App\Support\Breadcrumbs |
The trail the breadcrumb element reads |
Everything is static and registered in boot(). Labels are always callables:
at boot time your language file may not be loaded yet.
The module-side registrations are covered in Widgets and surfaces. What follows is the theme side and the reference material.
Theme tokens#
The Designer does not create tokens: it reads the ones the active theme
declares. Every customizer.json control with a var becomes a token offered
in the colour, font, radius and spacing pickers.
The parent to child chain is resolved with it, so a child theme that overrides
--color-primary changes every layout pointing at that token, without the
layout being touched.
When the theme does not cover something, the administrator creates a Designer
variable (--bd-*), stored by the module in its own stylesheet. Changing one
republishes nothing.
themes/<slug>/designer.json#
Optional, next to customizer.json.
{
"presets": {
"spacing": ["0", "0.25rem", "0.5rem", "1rem", "1.5rem", "2rem", "3rem"],
"shadows": { "soft": "0 1px 2px rgba(15, 23, 42, 0.06)" },
"type_scale": { "sm": "0.875rem", "base": "1rem", "lg": "1.125rem" }
},
"global_classes": [
{ "name": "card", "styles": { "desktop": { "normal": {
"background-color": { "token": "--color-background" },
"border-radius": { "token": "--radius" },
"padding": "1.5rem"
}}}}
],
"surfaces": { "blog.index": "designer/blog-index.json" },
"site_parts": { "header": "designer/header.json" },
"templates": ["designer/templates/hero-1.json"]
}
presetsfeed the sliders and pickers of the inspector. A theme with its own type scale makes the editor offer that scale instead of raw values.global_classesenter the global class library as.bdg-<name>, and the administrator can edit them afterwards.surfaces,site_partsandtemplatesare layouts your theme suggests. Activating the theme offers to import them; it never overwrites what the user already published without confirmation, and the layouts live in the database, so switching themes preserves the work.
A style value can be raw ("1.5rem") or point at a token
({ "token": "--radius" }). Prefer the token: it is what makes a layout follow
a theme change.
The cascade#
From weakest to strongest:
theme CSS -> module CSS -> Designer variables -> document stylesheet -> theme custom CSS
Rules of the compiled document stylesheet:
- no
!important; - one selector per node (
.bd-<id>) and one per global class (.bdg-<name>), both with the same specificity, so order decides and the local style of a node always comes after the global class; - desktop first, media queries from the widest to the narrowest, which is what makes desktop to tablet to mobile inheritance work without specificity tricks;
- states:
hoverbecomes:hover,focusbecomes:focus-visible,activebecomes:active; - dark mode: the
darkstate becomes.dark .bd-<id>. A theme that declares dark mode support has nothing else to do.
Schema control reference#
These are the type values accepted in designer.schema on a widget, in
filters on a collection and in schema on a form action. label is always a
language key.
| Type | Stored value | Descriptor options |
|---|---|---|
text |
string | maxlength, placeholder |
textarea |
string | rows |
code |
string | monospace, never executed |
richtext |
sanitised HTML | the core editor, on demand |
number |
int or float | min, max, step |
slider |
string with a unit ("24px") |
min, max, unit, presets, token_group |
select |
string | options |
select2 |
list of strings | options, multiple selection |
choose |
string | options, radios drawn as buttons |
switcher |
bool | |
color |
string or {token} |
the theme palette in the picker |
media |
{path,width,height} |
accept, meta |
icon |
icon name or an uploaded SVG | |
url |
string | safe schemes only |
gallery |
list of {image,alt,caption} |
|
repeater |
list of maps | fields, add |
dimensions |
{top,right,bottom,left} |
four sides with a link toggle |
font |
string | the theme families |
popup |
popup id | picker of published popups |
heading |
nothing | a section label |
divider |
nothing | a rule |
Controls holding a single value (text, textarea, url, media, number,
slider, color) get the dynamic tag button automatically.
Permissions#
The Designer does not invent a permission system per module. It uses the group matrix:
- a surface is restricted through
designer.surfaces.<module>, built automatically from the surfaces you registered: nothing to do in your module; - an individual element is restricted by the administrator, in Designer > Element Manager;
- to require a permission before a node of your element can be saved, declare
'requires' => 'unfiltered_html'in thedesignerdescriptor. That is what the core HTML element does.
Checklist before you publish#
- Every label is a callable returning
__('...')from your module's language files. No string hardcoded in PHP, Blade or JS. - No direct access to another module's tables.
class_existsandRoute::hasguards on every cross-module integration. - Module tables prefixed
mod_<slug>_and removed inonUninstall. - Every collection query applies the visibility of the owning module.
- Every widget view escapes everything, emits a required
alton images (or marks them decorative) and setswidthandheightwhen known. - A widget returns an empty string when it has nothing to show, rather than an empty box.
- No inline script: the product CSP blocks it. JS goes in your bundle.
- With the Designer uninstalled, your module works identically.
- With the Designer installed and no layout published, the screen shows exactly the Blade it showed before.
- Zero axe violations on every new screen, in both roles.