Visual Editor - v1.0.0

Responsive Design Tools

Status: v1.0

The editor lets editors and developers author per-breakpoint style and structural overrides without hand-editing CSS. The same registry that drives the editor UI also drives the server-side renderer, so previews match production exactly.

This document covers both the editor-facing workflow and the developer integration surface. See also:

  • Theming — for the breakpoint registry hierarchy (where overrides live).
  • Configuration — for the host-app config override path.

1. For editors

1.1 The viewport switcher

The editor toolbar shows one button per registered breakpoint, plus an All button on the left for the unprefixed base value:

[ All ] [ Mobile ] [ Tablet ] [ Desktop ] [ Wide ] [ Full ]

Selecting a button does two things:

  1. Resizes the canvas to the breakpoint's min-width so you can preview the layout at that size.
  2. Scopes the next style edit to that breakpoint. Any Inspector control that supports responsive overrides records your change at the active breakpoint.

The All button writes to the unprefixed base value — the value that applies everywhere unless a larger breakpoint overrides it.

1.2 Setting a per-breakpoint override

  1. Select the block you want to customize.
  2. Click the viewport switcher button for the breakpoint you want to target (e.g. Mobile).
  3. Adjust the control (padding, font size, column count, alignment, …) in the Inspector.

The value you just set applies at that breakpoint and up, until another override at a larger breakpoint replaces it.

1.3 Resetting an override

Each per-breakpoint control surfaces a Reset to base button when an override is currently set at the active breakpoint. Clicking it removes that single override and lets the value cascade through from the next-smaller defined slot.

If clearing the override leaves no other overrides, the stored attribute collapses back to the simple scalar form — no extra JSON on disk.

1.4 Mobile-first cascade

The cascade is always mobile-first:

  • base applies everywhere unless overridden.
  • A value set at md applies at md (≥768px) and up, unless lg, xl, or 2xl overrides it.
  • A value set at lg does not affect sm or md — those continue to inherit base (or whatever smaller breakpoint was set).

This mirrors the way Tailwind's sm: / md: modifiers work in CSS.


2. For developers

2.1 Configuring breakpoints

Breakpoints resolve in priority order — highest layer wins on key collision:

  1. Active theme's theme.jsonsettings.custom.artisanpack.breakpoints
  2. Application configartisanpack.visual-editor.breakpoints
  3. Package defaultsBreakpointRegistry::DEFAULTS (Tailwind v4 mins)

Config example

// config/artisanpack/visual-editor.php
return [
    'breakpoints' => [
        // Resize the default `lg` breakpoint:
        'lg'  => '1100px',
        // Add a new `3xl` breakpoint:
        '3xl' => 1920,
    ],
];

theme.json example

{
    "settings": {
        "custom": {
            "artisanpack": {
                "breakpoints": {
                    "lg":  "1200px",
                    "3xl": 1920
                }
            }
        }
    }
}

Values may be integer pixels (640) or CSS-length strings ('640px'). Other lengths (rem, vw, …) are rejected at load time with a descriptive error.

The implicit base slot (no min-width, applies everywhere) is reserved — using it as a key throws.

2.2 Opting a block into responsive support

Add supports.artisanpackResponsive to the block's block.json. List the attribute paths that should expose per-breakpoint UI:

{
    "name": "artisanpack/columns",
    "supports": {
        "artisanpackResponsive": {
            "attributes": [
                "spacing",
                "align",
                "columns.count"
            ]
        }
    }
}

Out of the box the following forked layout blocks opt in: group, columns, column, buttons, spacer, cover, media-text. Blocks that don't opt in still render correctly — their Inspector simply shows the single-value control as today.

2.3 Reading a responsive attribute in a block's edit component

import { useResponsiveValue, registryFromSnapshot } from '@artisanpack-ui/visual-editor/responsive'

// `bootstrap.breakpoints` comes from the editor's PHP-stamped settings.
const registry = registryFromSnapshot( bootstrap.breakpoints )

export function Edit( { attributes }: BlockEditProps ) {
    const padding = useResponsiveValue<string>( attributes.padding, registry )

    return <div style={ { padding: padding ?? '0' } }>…</div>
}

useResponsiveValue re-renders the component every time the editor switches breakpoints, so the preview stays in sync without manual subscriptions.

2.4 Writing per-breakpoint values from an InspectorControl

Wrap the underlying primitive in ResponsiveControl. The wrapper handles promotion (scalar → discriminated form) and reset-to-base for you:

import { ResponsiveControl } from '@artisanpack-ui/visual-editor/responsive'

<ResponsiveControl
    registry={ registry }
    value={ attributes.padding }
    onChange={ ( next ) => setAttributes( { padding: next } ) }
    label="Padding"
    render={ ( { value, setValue } ) => (
        <RangeControl
            value={ value ?? 0 }
            onChange={ ( v ) => setValue( v ) }
            min={ 0 }
            max={ 80 }
        />
    ) }
/>

2.5 Server-side rendering

The Blade renderer's ResponsiveClassResolver emits the correct class string or @media rule for any responsive attribute. Pass a token map when the values can be expressed as Tailwind utilities:

use ArtisanPackUI\VisualEditorRendererBlade\Responsive\ResponsiveClassResolver;

$resolver = app( ResponsiveClassResolver::class );

$result = $resolver->emit(
    $attribute,            // [ 'base' => 3, 'sm' => 1, 'md' => 2 ]
    'grid-template-columns',
    [
        '1' => 'grid-cols-1',
        '2' => 'grid-cols-2',
        '3' => 'grid-cols-3',
    ],
);

// $result['class'] → 'grid-cols-3 sm:grid-cols-1 md:grid-cols-2'
// $result['css']   → '' (every value tokenized)

When the value can't be tokenized, the resolver falls back to a generated wrapper class plus the scoped CSS rules:

$result = $resolver->emit(
    [ 'base' => '13px', 'md' => '18px' ],
    'font-size',
    [], // no token map
);

// $result['class'] → 've-r-abcd123456'
// $result['css']   → '.ve-r-abcd123456{font-size:13px}@media (min-width:768px){.ve-r-abcd123456{font-size:18px}}'

Block partials merge $result['class'] into the wrapper class list and push $result['css'] into the request-scoped ResponsiveCssAccumulator. The <x-ve-blocks> and <x-ve-template> components drain the accumulator at the top of the render output and emit one consolidated <style data-ve-responsive> block — there is no per-block <style> tag interleaved with the wrapper's children. Duplicate payloads (the same overrides on N siblings) collapse to one rule set, keyed by scope class.

2.6 Lazy migration

Scalar values are first-class — they load without error and only inflate to the discriminated { base, sm, … } form the first time an editor sets a per-breakpoint override. There is no batch migration; existing content keeps working.

When every override is cleared back to inheriting the base, the storage collapses back to the scalar form so saved JSON stays compact.

2.7 Auditing orphaned overrides

When a theme or config removes a breakpoint that was previously in use, the values stored under that key are preserved on save but skipped at render time. Use the audit command to surface them:

php artisan visual-editor:audit-breakpoints

Sample output:

+-------------+-----------+------------------------------------------+
| Resource    | Record ID | Orphaned overrides                       |
+-------------+-----------+------------------------------------------+
| pages       | 42        | artisanpack/columns@spacing → [legacy]   |
+-------------+-----------+------------------------------------------+
Audited 17 record(s). 1 record(s) carry orphaned overrides.

Flags:

  • --resource=<slug> — limit the audit to a single resource from artisanpack.visual-editor.resources.
  • --json — emit a machine-readable report for CI.

3. What's out of scope (v1.0)

  • Container queries — deferred to v1.x.
  • Per-breakpoint visibility — deferred to v1.x (contextual visibility rules).
  • Per-breakpoint state styles — future composition with State Design Tools.
  • Desktop-first or independent (non-cascading) modes — mobile-first only.
  • Bulk migration of existing scalar attributes — lazy promotion only.