Page Builder
Visual drag-and-drop page builder powered by Puck with 36 registered components
Page Builder
Brand Studio includes a visual page builder powered by Puck (@puckeditor/core v0.21+). Pages are assembled by dragging registered @regen/ui components onto a canvas, configuring their props in the sidebar, and saving the result as structured JSON.
How Puck Works
Puck is a React-based visual editor that renders a drag-and-drop canvas alongside a component panel and props sidebar. Each registered component declares its configurable fields (text inputs, selects, numbers, arrays, slots) and a render function. The editor produces a JSON document describing the component tree, which is stored in Supabase and rendered at runtime.
The page builder configuration lives at src/lib/puck/puck-config.tsx. This single file defines all 36 registered components, the root configuration, and the category groupings.
Slot API
Puck v0.21+ uses the slot API (type: "slot") for nested content. This replaces the deprecated DropZone pattern. A slot field accepts child components dragged into it from the canvas.
Single slot -- most layout components use one content slot:
Multiple named slots -- the Columns component uses separate left and right slots:
Slot fields render as drop targets on the canvas. Drag any other registered component into a slot to nest it.
Layout Blocks
Three primary layout blocks provide page structure:
Section
Full-width wrapper with background, vertical padding, and max-width constraints. Contains a single content slot.
| Field | Options |
|---|---|
| Background | None, Muted, Card, Primary |
| Vertical Padding | None, SM, MD, LG, XL |
| Max Width | SM, MD, LG, XL, Full |
Columns
Two-column grid layout with separate left and right slots.
| Field | Options |
|---|---|
| Layout | 50/50, 1/3--2/3, 2/3--1/3 |
| Gap | SM (16px), MD (24px), LG (32px) |
Grid
N-column grid with a single content slot. Items placed inside flow into equal-width columns.
| Field | Options |
|---|---|
| Columns | 2, 3, 4 |
| Gap | SM (16px), MD (24px), LG (32px) |
All 36 Registered Components
| Component | Category | Key Fields | Description |
|---|---|---|---|
| Heading | Typography | text, level (h1--h4), size (xs--3xl) | Semantic heading with size control |
| Text | Typography | content, size (xs--lg), style (normal/muted) | Body text block |
| InlineCode | Typography | code text | Inline monospace code span |
| CodeBlockDisplay | Typography | code text (textarea) | Multi-line code block |
| Button | Actions | label, variant (6 options), size, href | Clickable action with optional link |
| Badge | Display | label, variant (default/secondary/outline/destructive) | Small status label |
| Stat | Display | label, value, trend value, trend direction | Single statistic with trend indicator |
| KpiCard | Display | title, value, subtitle, trend | Key performance indicator card |
| Banner | Display | message, variant (info/success/warning/error) | Full-width announcement banner |
| Alert | Display | title, description, variant (default/destructive) | Alert box with title and description |
| Avatar | Display | image URL, fallback text | Circular avatar with image or initials |
| ProgressBar | Display | value (0--100), size, variant | Horizontal progress indicator |
| AccordionGroup | Display | items array (title + content each) | Expandable/collapsible content sections |
| StaticTable | Display | headers array, rows array (comma-separated cells) | Static data table |
| Card | Layout | title, content slot | Bordered card with header and content area |
| Container | Layout | content slot, max width, density | Centered container with width constraints |
| Stack | Layout | items slot, gap (0--12) | Vertical flex stack |
| HStack | Layout | items slot, gap, alignment | Horizontal flex stack |
| Separator | Layout | orientation (horizontal/vertical) | Divider line |
| AspectRatioBox | Layout | content slot, ratio (16:9, 4:3, 1:1, 21:9) | Fixed aspect ratio container |
| ScrollAreaBox | Layout | content slot, max height (CSS value) | Scrollable overflow container |
| Section | Layout Blocks | content slot, background, padding, max width | Full-width page section wrapper |
| Columns | Layout Blocks | left slot, right slot, column ratio, gap | Two-column layout |
| Grid | Layout Blocks | content slot, column count, gap | N-column repeating grid |
| NavBar | Navigation | logo text, links array, CTA button | Top navigation bar with links |
| Tabs | Navigation | tabs array (label + content each) | Tabbed content switcher |
| Spinner | Loading | size (sm/md/lg) | Animated loading spinner |
| Skeleton | Loading | width (CSS), height (CSS) | Placeholder loading skeleton |
| PageHeaderBlock | Page Structure | title, description | Full-width page header |
| SectionHeaderBlock | Page Structure | title, description, border toggle | Section heading with optional border |
| EmptyStateBlock | Page Structure | title, description, icon (5 options) | Empty state placeholder with icon |
| FiveCapitalsPentagonBlock | Regen Domain | 5 capital scores (0--100), size (px) | Radar pentagon chart of Five Capitals |
| CapitalScoreBarBlock | Regen Domain | capital type, score, label, show value, size | Single capital score bar |
| ProjectPhaseGateBlock | Regen Domain | phases array (id/label/sublabel/status), compact | Phase gate timeline visualization |
| RCCSCreditBadgeBlock | Regen Domain | amount, unit, verified flag, vintage, size | RCCS credit verification badge |
| VirtueScoreBlock | Regen Domain | coherence (0--1), show bars toggle | Virtue Engine coherence score display |
Brand Token Injection
The root Puck configuration injects brand CSS variables globally using a CSS @import rule. The brandSlug field on the root component determines which brand's tokens are loaded:
The brandSlug is resolved from the brand_entities table based on the URL [id] parameter. Tokens flow as CSS custom properties to all child components. No per-component token fields are needed -- components reference tokens via standard var(--token-name) CSS.
The default brand slug is prt. Change it in the root settings panel to preview pages under any brand's token set.
Custom Field Types
Beyond Puck's built-in field types (text, textarea, number, select, radio, array), the page builder uses two custom field types:
token -- Renders a TokenColorPicker component with a swatch palette drawn from the current brand's resolved color tokens. Used for fields that accept brand colors.
r2image -- Renders an R2 media asset browser. Lets the user select an image from the Regen Media library (Cloudflare R2) instead of typing a URL manually.
AI Build Panel
The AI Build panel provides natural-language page generation. Type a prompt describing the page you want, and the system generates a Puck JSON document using the 36 registered components.
Flow:
- User types a prompt in the AI Build panel (e.g., "Create a landing page for PRT with Five Capitals pentagon, phase gate timeline, and three KPI cards")
- The prompt is sent via
POST /api/dispatch/puck - The API creates a
monkey_dispatchjob that routes to Claude Code - Claude Code generates valid Puck JSON using the registered component names and field schemas
- The generated JSON is injected into the editor via
dispatch({ type: "setData", data }) - The user can then adjust the generated page visually in the editor
The AI is aware of all 36 component names, their field schemas, and the slot nesting model. Generated pages use layout blocks (Section, Columns, Grid) for structure and populate them with content components.
PagePicker
The PagePicker is a modal dialog that lists all pages stored for the current brand. It reads from the brand_design_artifacts table filtered by entity_id and artifact_type='page'.
Capabilities:
- List all pages with their title and slug
- Create a new page (assigns a slug and empty Puck data)
- Delete an existing page
- Navigate to a page to edit it in the builder
Data Storage
Page builder data is stored in the brand_design_artifacts Supabase table:
| Column | Type | Description |
|---|---|---|
id | uuid | Primary key |
entity_id | uuid | FK to brand_entities |
artifact_type | text | Always 'page' for builder pages |
slug | text | URL-friendly page identifier (e.g., home, about) |
title | text | Human-readable page title |
puck_data | jsonb | Complete Puck editor state (component tree, root props) |
published_at | timestamptz | Publication timestamp (null = draft) |
Each brand can have multiple pages. The puck_data column stores the full Puck Data object, which includes the component tree (content), root props, and zone data.
Adding a New Component
To register a new component in the page builder:
- Import the component from
@regen/uiat the top ofsrc/lib/puck/puck-config.tsx - Add a
ComponentConfigentry to thecomponentsobject with fields, defaultProps, and a render function - Add the component name to the appropriate category in the
categoriesobject - Puck picks it up automatically -- no other file changes are needed
The component will appear in the Puck sidebar under its assigned category, ready to be dragged onto the canvas.