Rebuilt the visual site builder from scratch using Craft.js, React 18, and TypeScript. The new editor renders directly in the DOM (no iframe), supports 40+ components, multi-page with shared header/footer, 16 templates, full-spectrum color/gradient controls, custom head code injection, save/publish workflow, and auto-save. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
22 KiB
WHP Site Builder v2 (Craft.js) - Project Documentation
Overview
A visual drag-and-drop website builder rebuilt from the ground up using Craft.js, React 18, and TypeScript. Replaces the legacy GrapesJS-based editor (/workspace/site-builder/). Users create multi-page websites without writing code, with server-side storage through WHP's PHP API layer.
Stack: Vite 6 + React 18 + TypeScript 5 + @craftjs/core 0.2.x Bundle: ~460KB JS + ~15KB CSS Version: 2.0.0
File Structure
craft/
├── index.html # HTML shell (loads fonts, FA icons, mounts React)
├── package.json # Dependencies and scripts (v2.0.0)
├── tsconfig.json # TypeScript config (ES2020, strict, path aliases)
├── vite.config.ts # Vite config (builds to dist/js/editor.js + dist/css/editor.css)
├── CLAUDE.md # This file
├── README.md # Brief project readme
├── FEATURES.md # User-facing features list
├── dist/ # Build output (not committed)
│ ├── index.html
│ ├── js/editor.js
│ ├── css/editor.css
│ └── assets/
├── src/
│ ├── main.tsx # Entry point: reads WHP_CONFIG, mounts <App>
│ ├── App.tsx # Wraps <Editor> with providers, passes resolver
│ │
│ ├── types/
│ │ └── index.ts # WhpConfig, PageData, AssetData, StyleProps, DeviceMode
│ │
│ ├── state/
│ │ ├── EditorConfigContext.tsx # React context for WHP_CONFIG (useEditorConfig hook)
│ │ ├── PageContext.tsx # Multi-page state (pages, header, footer, CRUD, switching)
│ │ └── SiteDesignContext.tsx # Site-wide design tokens (17 properties, Basic/Advanced)
│ │
│ ├── editor/
│ │ ├── EditorShell.tsx # 3-panel layout: TopBar + LeftPanel + Canvas + RightPanel + ContextMenu
│ │ └── Canvas.tsx # Craft.js <Frame> with device-width switching
│ │
│ ├── components/
│ │ ├── resolver.ts # Component map for Craft.js serialization (20 components)
│ │ ├── layout/
│ │ │ ├── Container.tsx # Generic container (div/section/article/header/footer/main)
│ │ │ ├── Section.tsx # Full-width section with centered inner container
│ │ │ ├── ColumnLayout.tsx # Flex columns (1-6, with split ratios)
│ │ │ ├── BackgroundSection.tsx # Section with background image/gradient overlay
│ │ │ ├── HeaderZone.tsx # Page-level header zone wrapper
│ │ │ └── FooterZone.tsx # Page-level footer zone wrapper
│ │ ├── basic/
│ │ │ ├── Heading.tsx # Inline-editable heading (h1-h6)
│ │ │ ├── TextBlock.tsx # Inline-editable paragraph
│ │ │ ├── ButtonLink.tsx # Styled <a> with color presets
│ │ │ ├── Navbar.tsx # Navigation bar (text/image logo, page links, external links, CTA)
│ │ │ ├── Footer.tsx # Footer component (links, copyright, social)
│ │ │ ├── Divider.tsx # Horizontal rule (color, thickness)
│ │ │ └── Spacer.tsx # Vertical spacing element
│ │ ├── media/
│ │ │ ├── ImageBlock.tsx # Image with placeholder, upload, browse, sizing
│ │ │ └── VideoBlock.tsx # Video embed (YouTube, Vimeo, direct files, background mode)
│ │ ├── sections/
│ │ │ ├── HeroSimple.tsx # Pre-built hero section with heading, subtext, CTA
│ │ │ ├── FeaturesGrid.tsx # 3-column feature cards grid
│ │ │ └── CTASection.tsx # Call-to-action banner section
│ │ └── forms/
│ │ ├── FormContainer.tsx # Form wrapper with action/method
│ │ ├── InputField.tsx # Input field with label and placeholder
│ │ ├── TextareaField.tsx # Textarea field with label
│ │ └── FormButton.tsx # Submit button with styling
│ │
│ ├── panels/
│ │ ├── topbar/
│ │ │ ├── TopBar.tsx # Back button, domain badge, device switcher, undo/redo, save, templates
│ │ │ └── TemplateModal.tsx # Template browser with categories and one-click loading
│ │ ├── left/
│ │ │ ├── LeftPanel.tsx # Tabs: Blocks | Pages | Layers | Assets
│ │ │ ├── BlocksPanel.tsx # Draggable block toolbox with categories
│ │ │ ├── PagesPanel.tsx # Multi-page CRUD, header/footer editing
│ │ │ ├── LayersPanel.tsx # Component hierarchy tree view
│ │ │ └── AssetsPanel.tsx # Asset browser with upload, drag-drop, thumbnails
│ │ ├── right/
│ │ │ ├── RightPanel.tsx # Tabs: Styles | Settings | Head
│ │ │ ├── GuidedStyles.tsx # Context-aware style panel (shows selected type)
│ │ │ └── SiteDesignPanel.tsx # Site-wide design tokens editor (Basic/Advanced tabs)
│ │ └── context-menu/
│ │ └── ContextMenu.tsx # Right-click context menu (duplicate, copy, paste, delete, etc.)
│ │
│ ├── hooks/
│ │ ├── useWhpApi.ts # Save/load/deploy via WHP API (with auto-save)
│ │ ├── useAssets.ts # Asset upload, browse, delete via WHP API
│ │ ├── useContextMenu.ts # Right-click menu state management
│ │ └── useKeyboardShortcuts.ts # Keyboard shortcut handler (undo, redo, delete, etc.)
│ │
│ ├── templates/
│ │ ├── index.ts # Template exports
│ │ └── definitions.ts # 16 template definitions across 4 categories
│ │
│ ├── ui/
│ │ └── SettingsTabs.tsx # Reusable General/Style/Advanced tabs for component settings
│ │
│ ├── constants/
│ │ └── presets.ts # Color, font, spacing, radius, gradient, device width presets
│ │
│ ├── utils/
│ │ ├── style-helpers.ts # cssPropsToString(), mergeStyles()
│ │ └── html-export.ts # Recursive node-to-HTML renderer, full page export
│ │
│ └── styles/
│ └── editor.css # Dark theme, CSS variables
Running Locally
cd /workspace/site-builder/craft
npm install
npm run dev
Opens at http://localhost:5173. The Vite dev server proxies /api requests to http://192.168.1.105:8080 (the WHP staging server) for save/load during development.
In standalone mode (no WHP_CONFIG on window), the editor runs fully client-side without save/load functionality.
Building
npm run build
Runs tsc && vite build. Output goes to dist/:
dist/index.html- HTML shelldist/js/editor.js- Single JS bundledist/css/editor.css- All stylesdist/assets/- Static assets (if any)
Deploying to WHP
Copy the built dist/ contents into the WHP site-builder web directory:
# Build
cd /workspace/site-builder/craft && npm run build
# Deploy to WHP Docker container
cp dist/index.html /docker/whp/web/site-builder/editor.html
cp -r dist/js/ /docker/whp/web/site-builder/js/
cp -r dist/css/ /docker/whp/web/site-builder/css/
The PHP wrapper (/docker/whp/web/site-builder/index.php) injects WHP_CONFIG into the HTML before serving it, so the editor gets the current user's session, CSRF token, site ID, etc.
Architecture
Key Decisions
-
No iframe - The canvas renders directly in the DOM (unlike GrapesJS which uses an iframe). This simplifies drag-and-drop and avoids cross-origin issues but means editor CSS must not leak into user content.
-
Inline styles - All component styling uses React
CSSProperties(inline styles). No class-based CSS for user content. This makes HTML export trivial and avoids stylesheet management. -
Single Frame, multi-page - Craft.js
<Frame>holds one page at a time. Page switching serializes the current state, stores it, and deserializes the new page's state. -
Header/Footer as separate pages - Header and Footer are stored as independent Craft.js states (like pages) that render above and below every page. Editing them uses the same canvas but with a distinct editing mode. This provides site-wide shared navigation and footer.
-
API compatibility - The save endpoint sends data in the same format as the GrapesJS version (
{ site_id, name, html, css, grapesjs: serializedJson }), so the PHP backend doesn't need changes. -
Component-based architecture - Each visual element is a React component that doubles as a Craft.js
UserComponent. All rendering, settings UI, and HTML export are co-located in one file. -
Site Design Tokens - A
SiteDesignContextprovides 17 design properties (colors, fonts, radii, nav style) that components can reference. Templates import their own design tokens when loaded.
Component Architecture
Every component in src/components/ follows this pattern:
import { UserComponent, useNode } from '@craftjs/core';
// 1. Props interface
interface MyComponentProps {
text?: string;
style?: CSSProperties;
}
// 2. The component itself (renders in editor canvas)
export const MyComponent: UserComponent<MyComponentProps> = ({ text, style }) => {
const { connectors: { connect, drag } } = useNode();
return <div ref={(r) => { if (r) connect(drag(r)); }} style={style}>{text}</div>;
};
// 3. Settings panel (rendered in right panel when selected)
const MyComponentSettings: React.FC = () => {
const { actions: { setProp }, props } = useNode((node) => ({
props: node.data.props as MyComponentProps,
}));
return <div>/* preset buttons, inputs, etc. */</div>;
};
// 4. Craft config (displayName, default props, rules, related settings)
MyComponent.craft = {
displayName: 'My Component',
props: { text: 'Default text', style: {} },
rules: { canDrag: () => true, canMoveIn: () => false, canMoveOut: () => true },
related: { settings: MyComponentSettings },
};
// 5. HTML export (static method for serializing to HTML string)
(MyComponent as any).toHtml = (props: MyComponentProps, childrenHtml: string) => {
return { html: `<div style="...">${childrenHtml}</div>` };
};
Component Resolver
All components must be registered in src/components/resolver.ts. This map is passed to <Editor resolver={componentResolver}> so Craft.js can serialize/deserialize the node tree.
export const componentResolver = {
Container, Section, ColumnLayout, BackgroundSection,
Heading, TextBlock, ButtonLink, Navbar, Footer, Divider, Spacer,
ImageBlock, VideoBlock,
HeroSimple, FeaturesGrid, CTASection,
FormContainer, InputField, TextareaField, FormButton,
};
WHP Integration
PHP Wrapper
The WHP control panel serves the editor through index.php, which:
- Verifies user authentication
- Validates the
site_idparameter - Generates a CSRF token
- Injects a
WHP_CONFIGobject into the HTML as a<script>tag before the app bundle
window.WHP_CONFIG = {
user: "username",
apiUrl: "/panel/api/site-builder",
csrfToken: "abc123...",
siteId: 42,
siteDomain: "example.com",
siteName: "My Site",
backUrl: "/panel/sites",
isRoot: false
};
API Endpoints
The editor communicates with WHP through these endpoints (all require CSRF token):
| Method | Endpoint | Description |
|---|---|---|
| POST | /panel/api/site-builder?action=save |
Save project (JSON body with site_id, html, css, craft state) |
| GET | /panel/api/site-builder?action=load&site_id=N |
Load project for a site |
| POST | /panel/api/site-builder?action=upload |
Upload asset (multipart) |
| GET | /panel/api/site-builder?action=assets&site_id=N |
List assets for a site |
| DELETE | /panel/api/site-builder?action=delete_asset |
Delete an asset |
| POST | /panel/api/site-builder?action=deploy&site_id=N |
Deploy/publish site to document root |
Auto-Save
The editor auto-saves every 30 seconds when running inside WHP. The save status is displayed in the top bar.
EditorConfigContext
useEditorConfig() provides access to WHP_CONFIG throughout the React tree:
whpConfig- The full config object (or null in standalone mode)isWHP- Boolean shorthand for whether we're running inside WHP
All Components (22)
| # | Component | Type | File | Features |
|---|---|---|---|---|
| 1 | Container | Layout | layout/Container.tsx |
Generic wrapper, tag selector (div/section/article/header/footer/main), bg color, padding, radius |
| 2 | Section | Layout | layout/Section.tsx |
Full-width with centered inner container, bg color/gradient, vertical padding, inner max-width |
| 3 | ColumnLayout | Layout | layout/ColumnLayout.tsx |
1-6 columns, split ratios (50-50, 30-70, 70-30, 33-33-33, 25-25-25-25, etc.), gap control |
| 4 | BackgroundSection | Layout | layout/BackgroundSection.tsx |
Section with background image, gradient overlay, parallax-ready |
| 5 | HeaderZone | Layout | layout/HeaderZone.tsx |
Page-level header wrapper zone, used by PageContext |
| 6 | FooterZone | Layout | layout/FooterZone.tsx |
Page-level footer wrapper zone, used by PageContext |
| 7 | Heading | Basic | basic/Heading.tsx |
Inline-editable, h1-h6 level, color, font family/size/weight, text align |
| 8 | TextBlock | Basic | basic/TextBlock.tsx |
Inline-editable paragraph, color, font family/size/weight, text align, line height |
| 9 | ButtonLink | Basic | basic/ButtonLink.tsx |
Link text/URL/target, 8 color presets (auto text contrast), radius, padding, font size |
| 10 | Navbar | Basic | basic/Navbar.tsx |
Text or image logo, page links, external links, CTA buttons, light/dark nav style |
| 11 | Footer | Basic | basic/Footer.tsx |
Footer with links, copyright, social links |
| 12 | Divider | Basic | basic/Divider.tsx |
Horizontal rule with color and thickness controls |
| 13 | Spacer | Basic | basic/Spacer.tsx |
Vertical spacing element with height control |
| 14 | ImageBlock | Media | media/ImageBlock.tsx |
SVG placeholder, URL input, upload, browse assets, alt text, width/height, object-fit, radius |
| 15 | VideoBlock | Media | media/VideoBlock.tsx |
YouTube, Vimeo, direct files (.mp4/.webm/.ogg), background mode, autoplay, loop |
| 16 | HeroSimple | Section | sections/HeroSimple.tsx |
Pre-built hero with heading, subtext, CTA button, gradient/color background |
| 17 | FeaturesGrid | Section | sections/FeaturesGrid.tsx |
3-column feature cards with icons, titles, descriptions |
| 18 | CTASection | Section | sections/CTASection.tsx |
Call-to-action banner with heading, text, button |
| 19 | FormContainer | Form | forms/FormContainer.tsx |
Form wrapper with action URL and method |
| 20 | InputField | Form | forms/InputField.tsx |
Text input with label, placeholder, type (text/email/tel/password/number) |
| 21 | TextareaField | Form | forms/TextareaField.tsx |
Textarea with label and placeholder |
| 22 | FormButton | Form | forms/FormButton.tsx |
Submit button with color and style controls |
Site Design Tokens
The SiteDesignContext provides 17 design properties organized into Basic and Advanced tabs:
Basic Tab (6 properties)
| Property | Default | Description |
|---|---|---|
| primaryColor | #3b82f6 |
Primary brand color |
| secondaryColor | #8b5cf6 |
Secondary brand color |
| accentColor | #10b981 |
Accent/highlight color |
| headingFont | Inter, sans-serif |
Font for headings |
| bodyFont | Inter, sans-serif |
Font for body text |
| linkColor | #3b82f6 |
Default link color |
Advanced Tab (11 properties)
| Property | Default | Description |
|---|---|---|
| successColor | #10b981 |
Success state color |
| warningColor | #f59e0b |
Warning state color |
| errorColor | #ef4444 |
Error state color |
| backgroundColor | #ffffff |
Page background |
| textColor | #1f2937 |
Default text color |
| mutedTextColor | #6b7280 |
Muted/secondary text |
| borderColor | #e5e7eb |
Default border color |
| borderRadius | 8px |
Global border radius |
| buttonFont | Inter, sans-serif |
Button font family |
| buttonRadius | 8px |
Button border radius |
| navStyle | light |
Navbar style (light/dark) |
Design tokens are imported from templates and can be edited via the Site Design panel. Components can read these tokens via useSiteDesign().
Templates
16 pre-built templates organized into 4 categories:
| Category | Templates |
|---|---|
| Business | Restaurant, Small Business, SaaS Landing, Agency, Medical |
| Creative | Portfolio, Photography, Content Creator, Event/Conference |
| Personal | Resume/CV, Blog, Wedding, Coming Soon |
| Community | Church, Non-Profit, Fitness/Gym |
Each template includes:
- Site design tokens (color palette, fonts)
- Header content (Navbar)
- Footer content
- One or more pages with pre-built content
- SVG thumbnail preview
Templates are loaded via the Template Modal (opened from TopBar). Loading a template replaces all pages, header, footer, and optionally imports the design tokens.
Multi-Page System
Pages are managed through PageContext:
- Each page has:
id,name,slug,craftState,headCode - Header and Footer are stored as separate "page" entries with fixed IDs (
__header__,__footer__) - Page switching serializes the current canvas, stores it, then deserializes the target page
- Header/Footer editing puts the canvas in a distinct mode
- The PagesPanel provides add, rename, delete, reorder, and header/footer edit buttons
Keyboard Shortcuts
| Shortcut | Action |
|---|---|
| Ctrl/Cmd + Z | Undo |
| Ctrl/Cmd + Shift + Z | Redo |
| Ctrl/Cmd + Y | Redo (alternative) |
| Delete / Backspace | Delete selected element |
Shortcuts are disabled when focus is on input, textarea, select, or contentEditable elements.
Context Menu (Right-Click)
The context menu appears on right-click within the canvas and provides:
- Duplicate element
- Copy / Paste
- Move Up / Move Down
- Select Parent
- Delete (with danger styling)
Items are disabled contextually (e.g., cannot delete ROOT, cannot move if already first/last).
Layers Panel
The Layers panel shows a hierarchical tree of all components on the current page. Clicking a layer selects the corresponding component. The tree displays component display names and nests children with indentation.
Asset Management
The Assets panel (AssetsPanel.tsx) provides:
- Upload button and drag-and-drop zone
- Thumbnail grid of uploaded assets
- Copy URL to clipboard
- Delete asset
- Integration with WHP API for server-side storage
Image and Video components also have inline asset selection (browse button in settings).
Adding New Components
- Create
src/components/<category>/<ComponentName>.tsxfollowing the pattern above - Add the component to
src/components/resolver.ts - Add a block entry in
src/panels/left/BlocksPanel.tsxunder the appropriate category - Implement the
toHtmlstatic for HTML export - Build and test:
npm run dev, drag the block onto the canvas, verify settings panel, verify HTML export
Checklist for a new component:
- Props interface with
style?: CSSProperties useNode()withconnect(drag(ref))on the root element- Settings panel using
useNode()withsetProp() .craftconfig withdisplayName, defaultprops,rules,related.settings.toHtml()static method usingcssPropsToString()- Registered in
resolver.ts - Block added to
BlocksPanel.tsx
CSS / Theme
The editor uses a dark theme defined via CSS custom properties in src/styles/editor.css:
- Base:
#16161a - Surface:
#1c1c24 - Accent:
#3b82f6(blue) - Text:
#e4e4e7 - Border:
#2d2d3a - Font: Inter
All editor chrome (panels, topbar, settings) is styled via editor.css. User content on the canvas uses inline styles exclusively.
Presets
Style presets are defined in src/constants/presets.ts:
TEXT_COLORS- 8 text color swatchesBG_COLORS- 8 background color swatchesFONT_FAMILIES- 8 Google FontsTEXT_SIZES- XS through 2XLFONT_WEIGHTS- Light through BoldSPACING_PRESETS- None through XLRADIUS_PRESETS- None through Full (9999px)GRADIENTS- 12 gradient presetsDEVICE_WIDTHS- Desktop (100%), Tablet (768px), Mobile (375px)
HTML Export
Every component has a static toHtml(props, childrenHtml) method. The html-export.ts utility recursively walks the Craft.js node tree and calls each component's toHtml to produce a complete HTML document. Export includes:
- Full
<!DOCTYPE html>document structure - Google Fonts preload links (optional)
- Inline styles throughout
- Header and footer wrapping each page
Testing Approach
- Manual testing: Run
npm run dev, drag components, edit props, verify settings panels - Type checking:
tsc --noEmit(part of build step) - HTML export: Verify
toHtml()output matches expected HTML structure - Device preview: Switch between desktop/tablet/mobile and verify responsive behavior
- WHP integration: Deploy to staging, verify save/load, verify PHP wrapper injection
Development Notes
- Path alias
@/maps to./src/(configured in both tsconfig.json and vite.config.ts) GuidedStylesshows selected component type and delegates to per-component settings panels- Text components (Heading, TextBlock) use
contentEditablefor inline editing when selected - Button/link navigation is prevented in the editor via
e.preventDefault() - Image upload integrates with WHP API; in standalone mode falls back to local
blob:URLs - Auto-save runs every 30 seconds when connected to WHP API
- The SettingsTabs UI component provides a reusable General/Style/Advanced tab layout for component settings