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 <noreply@anthropic.com>
65 lines
1.8 KiB
JavaScript
65 lines
1.8 KiB
JavaScript
// 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 (
|
|
<label style={{ display: 'block' }}>
|
|
<span className="field-label">{label}</span>
|
|
{children}
|
|
{hint && (
|
|
<span className="sans dim" style={{ display: 'block', fontSize: '0.78rem', marginTop: 4 }}>
|
|
{hint}
|
|
</span>
|
|
)}
|
|
</label>
|
|
)
|
|
}
|
|
|
|
export function TextField({ label, hint, value, onChange, placeholder, maxLength }) {
|
|
return (
|
|
<Field label={label} hint={hint}>
|
|
<input
|
|
type="text"
|
|
className="input"
|
|
value={value ?? ''}
|
|
placeholder={placeholder}
|
|
maxLength={maxLength}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
/>
|
|
</Field>
|
|
)
|
|
}
|
|
|
|
export function TextAreaField({ label, hint, value, onChange, placeholder, rows = 4, maxLength }) {
|
|
return (
|
|
<Field label={label} hint={hint}>
|
|
<textarea
|
|
className="input"
|
|
rows={rows}
|
|
value={value ?? ''}
|
|
placeholder={placeholder}
|
|
maxLength={maxLength}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
style={{ resize: 'vertical', fontFamily: 'inherit' }}
|
|
/>
|
|
</Field>
|
|
)
|
|
}
|
|
|
|
// options: array of [value, label] tuples.
|
|
export function SelectField({ label, hint, value, onChange, options }) {
|
|
return (
|
|
<Field label={label} hint={hint}>
|
|
<select className="select" value={value ?? ''} onChange={(e) => onChange(e.target.value)}>
|
|
{options.map(([v, l]) => (
|
|
<option key={v} value={v}>
|
|
{l}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
)
|
|
}
|