Skip to content
Bondry

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.

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"]
}
  • presets feed 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_classes enter the global class library as .bdg-<name>, and the administrator can edit them afterwards.
  • surfaces, site_parts and templates are 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:

Code
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: hover becomes :hover, focus becomes :focus-visible, active becomes :active;
  • dark mode: the dark state 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 the designer descriptor. That is what the core HTML element does.

Checklist before you publish#

  1. Every label is a callable returning __('...') from your module's language files. No string hardcoded in PHP, Blade or JS.
  2. No direct access to another module's tables. class_exists and Route::has guards on every cross-module integration.
  3. Module tables prefixed mod_<slug>_ and removed in onUninstall.
  4. Every collection query applies the visibility of the owning module.
  5. Every widget view escapes everything, emits a required alt on images (or marks them decorative) and sets width and height when known.
  6. A widget returns an empty string when it has nothing to show, rather than an empty box.
  7. No inline script: the product CSP blocks it. JS goes in your bundle.
  8. With the Designer uninstalled, your module works identically.
  9. With the Designer installed and no layout published, the screen shows exactly the Blade it showed before.
  10. Zero axe violations on every new screen, in both roles.