Canvas sections are WebProCMS's bounded free-form layout capability: a section whose pieces — photos, cards, badges, headings — sit on a snap-to 12-column placement grid, can overlap each other, and can be dragged to new positions right on the page editor's preview canvas. It brings the Webflow-style collage feel (overlapping photos, floating cards, pinned badges) to the page builder while keeping the guardrails that make non-designer sites stay presentable.
What the editor gets
- Curated starting points, not a blank canvas. Canvas rows ship in the design library like any other row — Hero Canvas Collage (hero), Feature Canvas Overlap (page content), Canvas Banner + Pinned Badge (below hero). You start from a designed composition and rearrange it; you never start from an empty 12×N grid.
- Drag tiles on the preview canvas. Hovering a canvas tile adds three controls to the element toolbar: a ✥ place grip (drag the tile to a new grid cell — it snaps to the rendered tracks, with guide lines shown while dragging), and z− / z+ (send backward / bring forward through the stacking order). Handles on the tile's right and bottom edges resize its column / row span the same way.
- Steppers in the sidebar. Each tile's design tab (paintbrush) has a Placement panel: numeric col/row start + span per breakpoint, an "+ Add breakpoint" disclosure for larger screens, and the stack (z-order) stepper. The drag UI and the steppers write the same values — use whichever is comfortable.
- Mobile stays sane automatically. Below the tablet breakpoint the canvas renders as a plain stacked flow in source order — no shearing, no hand-tuned phone coordinates. Placement applies from
mdup (larger-breakpoint overrides are opt-in per tile). - Everything else works like a normal row. Tiles are ordinary items: inline text editing, AI image generation (image tiles declare real
widths+ aspect so generated images match their cells), duplicate/hide/visibility rules, entrance animations, copy/paste styles, translations, and theme capture all behave exactly as they do in any other section.
How it works (for developers)
<x-dl.canvas>(resources/views/components/dl/canvas.blade.php) is a container primitive: a fixedgrid grid-cols-1 md:grid-cols-12 auto-rows-[minmax(2rem,auto)]grid. The single-column base IS the mobile stack — children need no base placement tokens. Schema: App\Support\DlSchemas\Canvas.- Placement is per-child data, not structure: grid-cell utilities (
md:col-start-N md:col-span-N md:row-start-N md:row-span-N, unprefixedz-N) stored in the child's own*_classesfield like any styling. No new marker grammar, no document-model changes —canvasis one more recurse-into container (thegrid-layoutprecedent) inRowBladeSurgery::wrapRules()+ the rowdoc.js twins. - One rewrite grammar, two entry points. resources/js/editor/placement.js owns the token rules (per-breakpoint set/clear, the md floor — base/sm writes promote to
md:so they can never fight the mobile stack — and decade-scale z stepping that pins at 0/50). The sidebar'splacementPickerwidget and the preview overlay's cell-snap drag both write throughAlpine.store('editor')(setPlacementClass/bumpZOrderClass) →setFieldValue→ the normal save pipeline. No new persistence surface. - The drag lives in the preview overlay (EditorPreviewOverlay): it measures the canvas's rendered grid tracks (columns, gaps, auto-row unit), snaps the pointer to cells, live-previews via inline
grid-column/grid-row, and postseditor-set-placement/editor-set-zorderto the parent — the same message pattern as the spacing drag bars. Handles only appear while the multi-column grid is actually rendering (a stacked mobile preview hides them) and share spacing's support rule ({prefix}_classesmust govern the tile's group node). - A wrapper opts into the lens with the
placementattr:<x-dl.wrapper placement="1" …>registers the{prefix}_placementfield (mirrorsgrid-item'scardattr). Curated templates set it on each tile. - Node-free installs render everything. All placement utilities are standard Tailwind classes the runtime CSS supplement emits, and
auto-rows-[minmax(…)]ridesUtilityCssGenerator::SAFE_ARBITRARY(auto-rows → grid-auto-rows). Generatability of every placement family is locked by a dataset test against the real utility map;css:supplement --checkstays flagged=0.
Boundaries (deliberate, not gaps)
- No absolute pixel positioning, ever. Pixel coordinate sets are the ugly-site failure mode and the
translate-[…]centering idiom is un-generatable node-free. Bounded percentage nudges (relative top-[-18%],rotate-[-6deg]) are the accent tier — the pinned-badge template shows the pattern. - No named grid areas or bespoke coordinate store — placement stays "classes on items" so cascade tools, theme capture, clone/promote, and slug rekeys all work unchanged.
- The AI site generator does not author canvas sections (RowAuthoringLinter allowlist unchanged) — canvas arrives only through the curated catalog.
- Curated templates never use fixed row heights —
auto-rows-[minmax(min,auto)]lets rows grow, so longer translations can't clip overlapped text.
Adding a new canvas template
Author it like any design-library row (design-library skill): outermost <x-dl.section>, one <x-dl.canvas>, children as <x-dl.wrapper placement="1"> tiles carrying md:-prefixed placement classes + z-* in field-classes. Keep the mobile stack in mind (source order = phone order), give every image tile real widths/sizes/aspect, and run php artisan design-library:index + the screenshot verify.