Builder Concepts

Contexts, Layout Variants, and Builder Types

Map perceived builders to technical contexts and understand the carousel/page distinction.

Contexts, Layout Variants, and Builders

This document describes the Tagixo context system — the mechanism that controls which modules, canvas behaviours, CSS features, and design-panel options are available in any given builder session.


What a Context Is

A context is a named string (page, form, mail, pdf, popup) that rewires the entire builder pipeline at instantiation time. It is not just a UI filter — it determines:

  1. ComponentRegistry — which module definitions are registered and therefore visible in the module palette.
  2. CanvasRegistry — which canvas Vue component is mounted inside the iframe.
  3. ContextFeaturePolicy — which design-panel groups, sub-properties, and select options are suppressed or transformed.
  4. BuilderApiService — which endpoints are used for load/save and what shape the payload takes.

Changing the context between two builder sessions is not a cosmetic operation. It rewires all four of these layers. A module that declares itself as ['page', 'mail'] will not appear in the palette of a form session even if nothing else has changed.


Built-in Contexts

page

The default and most capable context. All design-panel groups are available: typography, spacing, border, box shadow, filter, transform, animation. The canvas renders at the full configured preview width with no synthetic constraints.

form

The form builder uses a disjoint module palette — only modules that declare form in their contexts() array are shown. These are structural form elements (text input, select, checkbox, etc.) rather than presentation modules. The design panel is narrowed: animation, filter, and transform groups are suppressed because form elements are not expected to carry those styles.

mail

Forces a 600 px synthetic width on the canvas to approximate email client rendering. Removes CSS features that are unreliable in email clients: box-shadow, filter, transform, animation, position: sticky, and position: fixed. The MailCanvas component inlines all styles and strips <link> tags that reference external fonts, replacing them with @import inside a <style> block so the stylesheet survives Gmail's proxy.

pdf

Restricts the palette to modules that declare pdf in contexts(). Removes animation and interactive features. The canvas renders at a fixed A4-equivalent width. Font loading uses synchronous preload rather than async FontFace so fonts are guaranteed to be present at snapshot time.

popup

An internal alias to page. The ComponentRegistry resolves popup by delegating to the page registry:

// ComponentRegistry.php (simplified)
public function forContext(string $context): array
{
    $resolved = $context === 'popup' ? 'page' : $context;
    return $this->definitions[$resolved] ?? [];
}

The alias exists so that BuilderTypeContract::key() can return 'popup' (giving the builder type a distinct DB table and route namespace) while still sharing the full page module palette. The only behavioural difference is that ContextFeaturePolicy adds a gate on the section.sticky sub-property, since sticky sections inside a popup overlay are meaningless.


Layout Variants

A layout variant is an optional modifier applied on top of a context. It does not change which modules are registered — it changes which canvas component is mounted and may add additional feature gates.

The current built-in variant is carousel (used by the Slider builder).

carousel Variant

When layoutVariant is carousel, CanvasRegistry::resolve() returns SliderCanvas instead of the default PageCanvas:

// CanvasRegistry.php (simplified)
public function resolve(string $context, ?string $layoutVariant): string
{
    if ($layoutVariant === 'carousel') {
        return SliderCanvas::class;
    }
    return match ($context) {
        'mail' => MailCanvas::class,
        'form' => FormCanvas::class,
        default => PageCanvas::class,
    };
}

SliderCanvas mounts slide navigation controls, manages the active-slide state, and exposes the slide-level design panel that controls transition type and timing.

ContextFeaturePolicy merges variant-level gates additively on top of context-level gates. For carousel, the following are additionally suppressed regardless of context:

  • section.sticky — sticky sections inside a slide are meaningless.
  • animation group — slide transitions replace per-element animation in carousel layouts.
// ContextFeaturePolicy.php (simplified)
private const VARIANT_GATED_GROUPS = [
    'carousel' => ['animation'],
];

private const VARIANT_GATED_SUBPROPS = [
    'carousel' => ['section.sticky'],
];

public function gate(string $context, ?string $layoutVariant): GateResult
{
    $base    = $this->contextGate($context);
    $variant = $layoutVariant ? $this->variantGate($layoutVariant) : GateResult::empty();
    return $base->mergeWith($variant); // additive; variant cannot re-enable context gates
}

The merge is strictly additive: a variant cannot re-enable a feature that the context has suppressed.


Builder Type Map

Every builder is described by a BuilderTypeContract implementation. The contract exposes a key() (used as the route segment and builder_type DB column), the context, and optionally a layout variant.

Builder key() Context Layout Variant DB Table
Page Builder page page tgx_pages
Layout Builder layout page tgx_layouts
Slider Builder slider page carousel tgx_sliders
Form Builder form form tgx_forms
Mail Builder mail mail tgx_mail_templates
Popup Builder popup popup (→ page) tgx_popups

The editorPayload() method on the contract produces the JSON sent to the builder on boot. Every payload includes at minimum context, layoutVariant (null if absent), and configUrl (the endpoint the builder calls to retrieve its module registry and feature policy):

// Example editorPayload() output for the Slider builder
[
    'context'       => 'page',
    'layoutVariant' => 'carousel',
    'configUrl'     => '/tagixo/builder/config/slider/42',
    'saveUrl'       => '/tagixo/builder/save/slider/42',
    'record'        => ['id' => 42, 'name' => 'Hero Slider', ...],
]

The configUrl is mandatory. The builder will refuse to mount if it is absent.


Module Context Declaration

Modules declare which contexts they belong to via two mechanisms. Understanding the difference matters when adding a new module.

ModuleDefinition::contexts()

Used for catalogue-level filtering — whether the module appears in the module palette. Return an array of context strings:

public function contexts(): array
{
    return ['page', 'mail'];
}

The real base defaults in the abstract ModuleDefinition are ['page', 'mail', 'pdf'], not ['page']. This means a new module that does not override contexts() will appear in page, mail, and PDF builders by default. Override to restrict.

Module::getContexts()

Used for render-level filtering — whether the module's PHP renderer is invoked during PageRenderer::render(). This is checked after the palette filter and exists to prevent a module that somehow appears in a payload from being rendered in an incompatible context.

In practice the two should agree. If a module declares ['page'] in ModuleDefinition::contexts() but returns ['page', 'mail'] from Module::getContexts(), the module will not appear in the mail palette but its renderer will still accept a mail-context render call if the payload contains it. This is a data-integrity issue, not a security issue, but it should be avoided.

When to override which:

  • Override ModuleDefinition::contexts() to control palette visibility.
  • Override Module::getContexts() only when the renderer itself cannot handle a context (e.g. a module that generates JavaScript that makes no sense in a PDF renderer).

Context Feature Policy

ContextFeaturePolicy runs on the config endpoint response before it is sent to the builder. It suppresses design-panel groups, sub-properties within groups, and options within select fields.

Gated Groups

Entire design-panel sections suppressed per context:

Group Suppressed in
animation form, mail, pdf
filter mail
transform mail
box_shadow mail

Gated Sub-Properties

Individual props within a group suppressed per context:

Sub-Property Suppressed in
section.sticky form, mail, pdf, popup
section.z_index form, mail
background.fixed mail
typography.letter_spacing mail

Gated Options

Specific options within select-type props suppressed per context:

Prop Option Removed Context
position fixed, sticky mail
display grid mail
overflow scroll, auto mail

Structural Transformation via transformRegistry()

Beyond suppression, ContextFeaturePolicy::transformRegistry() can rewrite the module registry for structural elements. For the form context it replaces the Column and Row structural modules with FormRow and FormField wrappers, which enforce the form element nesting contract (a field must be inside a form row; a form row must be inside a form section).


Canvas System

All canvas components share a base interface that exposes activeElement, hoveredElement, dragState, and the selectElement / hoverElement event bus. Concrete canvas classes override defaults for their context.

Canvas Class Context / Variant What It Adds
PageCanvas page (default) Full responsive preview, breakpoint switcher
SliderCanvas page + carousel Slide navigation, active-slide state, transition preview
MailCanvas mail 600 px synthetic constraint, inline-style audit overlay
FormCanvas form Form-element nesting validator, live preview submission
PdfCanvas pdf A4 page break overlay, font-loaded gate before render

SliderCanvas default overrides:

  • previewWidth locked to the configured slider width (not user-switchable).
  • canDragBetweenSectionsfalse (modules cannot be dragged out of a slide).
  • sectionLabel'Slide' instead of 'Section'.

MailCanvas default overrides:

  • previewWidth locked to 600.
  • showResponsiveControlsfalse.
  • Adds a real-time unsupported-css-audit pass that highlights properties in the design panel that are known to be unreliable in mail clients.

Context Capability Matrix

Feature page form mail pdf popup
Full module palette Yes No (form modules only) Partial Partial Yes (= page)
Animation group Yes No No No Yes
Filter group Yes Yes No Yes Yes
Transform group Yes Yes No Yes Yes
Box shadow group Yes Yes No Yes Yes
Sticky sections Yes No No No No
Fixed positioning Yes No No No No
Responsive preview controls Yes Yes No No Yes
Slide navigation No No No No No
Form nesting validator No Yes No No No
Page break overlay No No No Yes No
Inline-style CSS audit No No Yes No No

Common Mistakes

1. Forgetting to declare a context in ModuleDefinition::contexts()

A new module that extends the abstract base inherits ['page', 'mail', 'pdf']. If the module uses JavaScript that is incompatible with the mail renderer (e.g. a GSAP animation), it will silently appear in the mail palette and may break the mail preview. Always audit contexts() on every new module.

2. Assuming feature-gated data is stripped from the payload

ContextFeaturePolicy suppresses design-panel UI. It does not strip gated properties from the stored payload. If a page is built with animations and then the context is changed to mail (e.g. by duplicating the record into a mail template), the animation data will still be in the payload — it will just not be rendered by the mail canvas. Do not rely on the policy to clean up incompatible data; handle that in the migration or duplication logic.

3. Omitting layoutVariant in editorPayload()

If layoutVariant is omitted from the payload (rather than explicitly set to null), the builder treats it as undefined on the JS side, which causes CanvasRegistry::resolve() to fall through to the default PageCanvas. For the Slider builder this means the slide navigation controls are never mounted. Always pass layoutVariant explicitly, even when it is null.

4. Treating popup as a separate module registry

Because popup is an alias to page in ComponentRegistry, any module added to the page registry automatically appears in popup builders. If a module should not appear in popups (e.g. a full-page hero that makes no sense inside an overlay), add an explicit check in ModuleDefinition::contexts()popup must be omitted just like any other context.

5. Adding variant-level gates that re-enable context gates

GateResult::mergeWith() is strictly additive. Attempting to use a variant to re-enable a feature suppressed at the context level (e.g. adding animation back for a carousel variant inside a mail context) has no effect. Context gates always win. Design the context suppression list accordingly.