From 764fb0c0697d7c4254fb56ae3721edc8ee7431ec Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 20:37:34 -0500 Subject: [PATCH] Add Wave 1 block renderers + editors (page builder step 3, client half) Client block registry now carries a renderer, edit form, palette label/icon, and defaults for all seven Wave 1 blocks (self-registering via client/src/blocks/types/*): heading, rich_text, image, two_column, cta, divider, quote. - BlockRenderer + BlockList render stored blocks via the registry (respect `visible`, tolerate unknown types), reading getBlock from ./registry to avoid the index -> twoColumn -> BlockRenderer cycle. - editorKit: shared Field/TextField/TextAreaField/SelectField styled with the existing admin form classes; rich_text editor reuses RichTextEditor (variant post), image editor reuses the shared uploader. - two_column editor is a mini per-column canvas (add from the leaf-only palette, edit via each block's registry editor, reorder, remove). - theme.css: public block styles (heading/image alignment/cta/quote/ two-column responsive grid) + column sub-block editor styles. Verified: all 11 modules transform cleanly under esbuild. Full visual verification comes with the builder UI (step 5) + public route (step 6). Co-Authored-By: Claude Opus 4.8 --- client/src/blocks/BlockRenderer.jsx | 26 +++++ client/src/blocks/editorKit.jsx | 64 ++++++++++++ client/src/blocks/index.js | 16 +-- client/src/blocks/types/cta.jsx | 63 +++++++++++ client/src/blocks/types/divider.jsx | 25 +++++ client/src/blocks/types/heading.jsx | 46 ++++++++ client/src/blocks/types/image.jsx | 100 ++++++++++++++++++ client/src/blocks/types/quote.jsx | 43 ++++++++ client/src/blocks/types/richText.jsx | 39 +++++++ client/src/blocks/types/twoColumn.jsx | 120 +++++++++++++++++++++ client/src/styles/theme.css | 144 ++++++++++++++++++++++++++ 11 files changed, 678 insertions(+), 8 deletions(-) create mode 100644 client/src/blocks/BlockRenderer.jsx create mode 100644 client/src/blocks/editorKit.jsx create mode 100644 client/src/blocks/types/cta.jsx create mode 100644 client/src/blocks/types/divider.jsx create mode 100644 client/src/blocks/types/heading.jsx create mode 100644 client/src/blocks/types/image.jsx create mode 100644 client/src/blocks/types/quote.jsx create mode 100644 client/src/blocks/types/richText.jsx create mode 100644 client/src/blocks/types/twoColumn.jsx diff --git a/client/src/blocks/BlockRenderer.jsx b/client/src/blocks/BlockRenderer.jsx new file mode 100644 index 0000000..e78e94f --- /dev/null +++ b/client/src/blocks/BlockRenderer.jsx @@ -0,0 +1,26 @@ +// Renders stored blocks via their registry component. Used by the public page +// route, the draft preview, and (recursively) the two_column block. Kept +// separate from the registry so both the renderer and the builder can import it. +// Import the lookup from the registry directly (not ./index) to avoid a cycle: +// index → types/twoColumn → BlockRenderer. The page route/builder import ./index, +// which registers every block before anything renders. +import { getBlock } from './registry.js' + +/** + * Render one block. A block with `visible === false` renders nothing (admins + * hide blocks without deleting them). An unknown type also renders nothing — + * server validation prevents storing one, so this only guards a client/server + * registry skew rather than crashing the whole page. + */ +export default function BlockRenderer({ block }) { + if (!block || block.visible === false) return null + const def = getBlock(block.type) + if (!def || !def.component) return null + const Component = def.component + return +} + +/** Render an ordered array of blocks (array position = display order). */ +export function BlockList({ blocks }) { + return (blocks || []).map((block) => ) +} diff --git a/client/src/blocks/editorKit.jsx b/client/src/blocks/editorKit.jsx new file mode 100644 index 0000000..481fb21 --- /dev/null +++ b/client/src/blocks/editorKit.jsx @@ -0,0 +1,64 @@ +// Shared form controls for block editors, styled with the existing admin design +// system (.field-label / .input / .select). Every block's editor is a +// ({ props, onChange }) component; these keep the seven of them consistent and +// short. onChange always receives the full next props object. + +export function Field({ label, hint, children }) { + return ( + + ) +} + +export function TextField({ label, hint, value, onChange, placeholder, maxLength }) { + return ( + + onChange(e.target.value)} + /> + + ) +} + +export function TextAreaField({ label, hint, value, onChange, placeholder, rows = 4, maxLength }) { + return ( + +