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:
ComponentRegistry— which module definitions are registered and therefore visible in the module palette.CanvasRegistry— which canvas Vue component is mounted inside the iframe.ContextFeaturePolicy— which design-panel groups, sub-properties, and select options are suppressed or transformed.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.animationgroup — 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:
previewWidthlocked to the configured slider width (not user-switchable).canDragBetweenSections→false(modules cannot be dragged out of a slide).sectionLabel→'Slide'instead of'Section'.
MailCanvas default overrides:
previewWidthlocked to600.showResponsiveControls→false.- Adds a real-time
unsupported-css-auditpass 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.