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:

Card: {
  fields: {
    title: { type: "text", label: "Title" },
    content: { type: "slot", label: "Content" },
  },
  render: ({ title, content: Content }) => (
    <Card>
      <CardHeader><CardTitle>{title}</CardTitle></CardHeader>
      <CardContent><Content /></CardContent>
    </Card>
  ),
}

Multiple named slots -- the Columns component uses separate left and right slots:

Columns: {
  fields: {
    left: { type: "slot", label: "Left" },
    right: { type: "slot", label: "Right" },
    columns: { type: "select", label: "Layout", options: [...] },
  },
  render: ({ left: Left, right: Right, columns }) => (
    <div style={{ display: "grid", gridTemplateColumns: columns }}>
      <div><Left /></div>
      <div><Right /></div>
    </div>
  ),
}

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.

FieldOptions
BackgroundNone, Muted, Card, Primary
Vertical PaddingNone, SM, MD, LG, XL
Max WidthSM, MD, LG, XL, Full

Columns

Two-column grid layout with separate left and right slots.

FieldOptions
Layout50/50, 1/3--2/3, 2/3--1/3
GapSM (16px), MD (24px), LG (32px)

Grid

N-column grid with a single content slot. Items placed inside flow into equal-width columns.

FieldOptions
Columns2, 3, 4
GapSM (16px), MD (24px), LG (32px)

All 36 Registered Components

ComponentCategoryKey FieldsDescription
HeadingTypographytext, level (h1--h4), size (xs--3xl)Semantic heading with size control
TextTypographycontent, size (xs--lg), style (normal/muted)Body text block
InlineCodeTypographycode textInline monospace code span
CodeBlockDisplayTypographycode text (textarea)Multi-line code block
ButtonActionslabel, variant (6 options), size, hrefClickable action with optional link
BadgeDisplaylabel, variant (default/secondary/outline/destructive)Small status label
StatDisplaylabel, value, trend value, trend directionSingle statistic with trend indicator
KpiCardDisplaytitle, value, subtitle, trendKey performance indicator card
BannerDisplaymessage, variant (info/success/warning/error)Full-width announcement banner
AlertDisplaytitle, description, variant (default/destructive)Alert box with title and description
AvatarDisplayimage URL, fallback textCircular avatar with image or initials
ProgressBarDisplayvalue (0--100), size, variantHorizontal progress indicator
AccordionGroupDisplayitems array (title + content each)Expandable/collapsible content sections
StaticTableDisplayheaders array, rows array (comma-separated cells)Static data table
CardLayouttitle, content slotBordered card with header and content area
ContainerLayoutcontent slot, max width, densityCentered container with width constraints
StackLayoutitems slot, gap (0--12)Vertical flex stack
HStackLayoutitems slot, gap, alignmentHorizontal flex stack
SeparatorLayoutorientation (horizontal/vertical)Divider line
AspectRatioBoxLayoutcontent slot, ratio (16:9, 4:3, 1:1, 21:9)Fixed aspect ratio container
ScrollAreaBoxLayoutcontent slot, max height (CSS value)Scrollable overflow container
SectionLayout Blockscontent slot, background, padding, max widthFull-width page section wrapper
ColumnsLayout Blocksleft slot, right slot, column ratio, gapTwo-column layout
GridLayout Blockscontent slot, column count, gapN-column repeating grid
NavBarNavigationlogo text, links array, CTA buttonTop navigation bar with links
TabsNavigationtabs array (label + content each)Tabbed content switcher
SpinnerLoadingsize (sm/md/lg)Animated loading spinner
SkeletonLoadingwidth (CSS), height (CSS)Placeholder loading skeleton
PageHeaderBlockPage Structuretitle, descriptionFull-width page header
SectionHeaderBlockPage Structuretitle, description, border toggleSection heading with optional border
EmptyStateBlockPage Structuretitle, description, icon (5 options)Empty state placeholder with icon
FiveCapitalsPentagonBlockRegen Domain5 capital scores (0--100), size (px)Radar pentagon chart of Five Capitals
CapitalScoreBarBlockRegen Domaincapital type, score, label, show value, sizeSingle capital score bar
ProjectPhaseGateBlockRegen Domainphases array (id/label/sublabel/status), compactPhase gate timeline visualization
RCCSCreditBadgeBlockRegen Domainamount, unit, verified flag, vintage, sizeRCCS credit verification badge
VirtueScoreBlockRegen Domaincoherence (0--1), show bars toggleVirtue 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:

render: ({ children, brandSlug }) => (
  <div style={{ minHeight: "100vh" }}>
    {brandSlug && (
      <style>{`
        @import url("https://studio.regendevcorp.com/api/public/tokens/${brandSlug}?format=css");
      `}</style>
    )}
    {children}
  </div>
)

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:

  1. 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")
  2. The prompt is sent via POST /api/dispatch/puck
  3. The API creates a monkey_dispatch job that routes to Claude Code
  4. Claude Code generates valid Puck JSON using the registered component names and field schemas
  5. The generated JSON is injected into the editor via dispatch({ type: "setData", data })
  6. 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:

ColumnTypeDescription
iduuidPrimary key
entity_iduuidFK to brand_entities
artifact_typetextAlways 'page' for builder pages
slugtextURL-friendly page identifier (e.g., home, about)
titletextHuman-readable page title
puck_datajsonbComplete Puck editor state (component tree, root props)
published_attimestamptzPublication 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:

  1. Import the component from @regen/ui at the top of src/lib/puck/puck-config.tsx
  2. Add a ComponentConfig entry to the components object with fields, defaultProps, and a render function
  3. Add the component name to the appropriate category in the categories object
  4. Puck picks it up automatically -- no other file changes are needed
// Example: adding a new component
import { MyComponent } from "@regen/ui";
 
// In the components object:
MyComponent: {
  label: "My Component",
  fields: {
    title: { type: "text", label: "Title" },
    content: { type: "slot", label: "Content" },
  },
  defaultProps: { title: "Default Title" },
  render: ({ title, content: Content }) => (
    <MyComponent title={title as string}>
      <Content />
    </MyComponent>
  ),
}
 
// In the categories object, add to an existing or new category:
categories: {
  "my-category": {
    title: "My Category",
    components: ["MyComponent"],
  },
}

The component will appear in the Puck sidebar under its assigned category, ready to be dragged onto the canvas.

On this page