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:
- Resizes the canvas to the breakpoint's min-width so you can preview the layout at that size.
- 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
- Select the block you want to customize.
- Click the viewport switcher button for the breakpoint you want to target (e.g. Mobile).
- 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:
baseapplies everywhere unless overridden.- A value set at
mdapplies atmd(≥768px) and up, unlesslg,xl, or2xloverrides it. - A value set at
lgdoes not affectsmormd— those continue to inheritbase(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:
- Active theme's
theme.json—settings.custom.artisanpack.breakpoints - Application config —
artisanpack.visual-editor.breakpoints - Package defaults —
BreakpointRegistry::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 fromartisanpack.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.