Four issues from adversarial review of the block-scoped <style> feature: 1. (Critical) transformBlock() recursed once per @media/@supports/@container nesting level with no cap -- ~7000 nested rules blew the call stack, and nothing between a Custom HTML block's toHtml() and the publish pipeline catches exceptions, so this took down the whole page's publish and crashed the live editor on every keystroke. Added MAX_NESTING_DEPTH=20 (pass the body through unscoped beyond it) and wrapped scopeCss() so it never throws on any input, matching repairOrphanNodes's existing contract. Caught and fixed a variable-shadowing bug in my own first pass at this: the new depth parameter was silently shadowed by a pre-existing `let depth` used for brace-matching in the same block, which would have defeated the cap with no type error. 2. (Important) FORCE_BODY: true was unconditional, but it isn't a no-op for style-free input: it also changes how the parser preserves whitespace after a LEADING html comment, which this repo's own fixture starts with. Verified via a raw byte-diff against HtmlBlock.tsx@6a9b227 (extracted verbatim, run standalone against real dompurify+jsdom) that the fixture gained bytes. Fixed by applying FORCE_BODY only when the input has a real (non-comment) <style> tag to rescue -- confirmed empirically that this is a true no-op for every other input. Pinned the old output as a checked-in regression fixture and added a raw toBe() diff test. 3. (Important) scopeStyleBlocks() wasn't idempotent -- pasting previously published/exported output into a fresh block nested a second wrapper and re-prefixed every selector. Added isAlreadyScoped(), which detects a lone root wrapper whose <style> content is already a no-op under scopeCss for that wrapper's own class (reusing scopeCss's own idempotency guarantee) and leaves it untouched. 4. (Minor) Documented, not fixed: the 32-bit scope-id hash is brute-forceable (CSS-only impact, same trust tier as other accepted risks here), and DOMPurify's SAFE_FOR_XML silently drops an entire <style> block when its content merely looks tag-like (e.g. content: "<Read More>"). 1155/1155 tests passing (was 1141), tsc clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
WHP Site Builder v2
A visual drag-and-drop website builder for WHP, rebuilt from the ground up with Craft.js, React 18, and TypeScript. Replaces the legacy GrapesJS-based editor.
Quick Start
npm install
npm run dev
Opens at http://localhost:5173. The editor runs in standalone mode without WHP integration.
Build and Deploy
# Build production bundle
npm run build
# Deploy to WHP
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 (index.php) injects WHP_CONFIG (user session, CSRF token, site ID) into the HTML before serving.
Architecture
- Craft.js - React-based visual editor framework (no iframe, direct DOM rendering)
- Inline styles - All user content uses React CSSProperties, no class-based CSS
- Component pattern - Each component is a self-contained file with: render logic, settings panel, Craft.js config, and HTML export method
- 3-panel layout - Left (blocks/pages/layers/assets), Center (canvas with device preview), Right (styles/settings/head)
- Dark theme - CSS custom properties, Inter font, blue accent
Components (22)
| Category | Components |
|---|---|
| Layout | Container, Section, ColumnLayout (1-6 cols), BackgroundSection, HeaderZone, FooterZone |
| Basic | Heading (H1-H6), TextBlock, ButtonLink, Navbar, Footer, Divider, Spacer |
| Media | ImageBlock (upload/browse/drag-drop), VideoBlock (YouTube/Vimeo/direct/background) |
| Sections | HeroSimple, FeaturesGrid, CTASection |
| Forms | FormContainer, InputField, TextareaField, FormButton |
Features
- Visual Editor - Drag-and-drop building, real-time preview, responsive device preview (Desktop/Tablet/Mobile)
- Site Design Tokens - 17 site-wide properties (colors, fonts, radii, nav style) with Basic/Advanced tabs
- Multi-Page - Unlimited pages with shared Header and Footer across all pages
- 16 Templates - Pre-built designs across 4 categories (Business, Creative, Personal, Community)
- Asset Management - Upload, browse, drag-drop, thumbnails, server-side storage via WHP API
- HTML Export - Full document export with Google Fonts and inline styles
- Auto-Save - Saves every 30 seconds when connected to WHP
- Context Menu - Right-click for duplicate, copy, paste, move, delete
- Keyboard Shortcuts - Undo, redo, delete
- Layers Panel - Component hierarchy tree view
- Undo/Redo - Full history support
Key Files
| File | Purpose |
|---|---|
src/main.tsx |
Entry point, reads WHP_CONFIG |
src/App.tsx |
Editor + providers (EditorConfig, SiteDesign, Pages) |
src/components/resolver.ts |
Component registry (20 components) for serialization |
src/editor/EditorShell.tsx |
3-panel layout + context menu + keyboard shortcuts |
src/editor/Canvas.tsx |
Craft.js Frame with device switching |
src/state/PageContext.tsx |
Multi-page state + header/footer |
src/state/SiteDesignContext.tsx |
17 site-wide design tokens |
src/templates/definitions.ts |
16 template definitions |
src/constants/presets.ts |
Color, font, spacing presets |
src/utils/html-export.ts |
Node-tree to HTML renderer |
See CLAUDE.md for full documentation. See FEATURES.md for a complete feature list.