import { useState } from 'react'
import { X, Type, Paintbrush, Palette, RotateCcw } from 'lucide-react'
import { BLOCKS } from './blocks'
import { ThemeBuilder } from './ThemeBuilder'
import type { ThemeConfig, CanvasBlock, CustomBlockDef, DataSource } from '../App'
type Tab = 'content' | 'style' | 'theme'
function ColorControl({ label, value, onChange, themeVar, themeLabel }: {
label: string
value: string
onChange: (v: string) => void
themeVar?: string
themeLabel?: string
}) {
const isTheme = value === themeVar
const hexVal = /^#[0-9a-fA-F]{6}$/.test(value) ? value : '#888888'
return (
)
}
function TextControl({ label, value, onChange, placeholder }: {
label: string; value: string; onChange: (v: string) => void; placeholder?: string
}) {
return (
{label}
onChange(e.target.value)}
placeholder={placeholder}
className="w-full text-xs px-2 py-1.5 border border-zinc-200 rounded outline-none focus:border-indigo-400 bg-white"
/>
)
}
export function BlockEditor({
block, customBlocks, theme, dataSources,
onContentChange, onStyleChange, onStyleReset, onThemeChange, onDeselect,
}: {
block: CanvasBlock
customBlocks: CustomBlockDef[]
theme: ThemeConfig
dataSources: DataSource[]
onContentChange: (instanceId: string, key: string, value: string) => void
onStyleChange: (instanceId: string, key: string, value: string) => void
onStyleReset: (instanceId: string) => void
onThemeChange: (t: ThemeConfig) => void
onDeselect: () => void
}) {
const [tab, setTab] = useState('content')
const isCustom = block.blockId.startsWith('custom:')
const def = isCustom ? null : BLOCKS.find(b => b.id === block.blockId)
const customDef = isCustom ? customBlocks.find(b => `custom:${b.id}` === block.blockId) : null
const blockLabel = isCustom ? (customDef?.name ?? 'Custom') : (def?.label ?? block.blockId)
const contentFields = def?.contentFields ?? []
const hasOverrides = Object.keys(block.styles).length > 0
const styleVal = (key: string) => block.styles[key] ?? ''
const setStyle = (key: string, val: string) => onStyleChange(block.instanceId, key, val)
const setContent = (key: string, val: string) => onContentChange(block.instanceId, key, val)
const contentVal = (key: string, fallback: string) => block.content[key] ?? fallback
return (
{/* Header */}
{/* Tabs */}
{([
{ id: 'content' as Tab, label: 'Content', icon: Type },
{ id: 'style' as Tab, label: 'Style', icon: Paintbrush },
{ id: 'theme' as Tab, label: 'Theme', icon: Palette },
]).map(({ id, label, icon: Icon }) => (
))}
{/* Content tab */}
{tab === 'content' && (
{isCustom ? (
Custom React components are edited in the code editor. Click the ✎ icon in the block library.
) : contentFields.length === 0 ? (
This block has no editable content fields.
) : (
{contentFields.map(field => {
const val = contentVal(field.key, def?.defaultContent?.[field.key] ?? '')
return (
{field.type === 'textarea' ? (
)
})}
)}
)}
{/* Style tab */}
{tab === 'style' && (
Component Overrides
{hasOverrides && (
)}
setStyle('backgroundColor', v)}
themeVar="var(--cms-bg)" themeLabel="bg" />
setStyle('color', v)}
themeVar="var(--cms-fg)" themeLabel="fg" />
Spacing
setStyle('padding', v)} placeholder="1rem 2rem" />
setStyle('marginTop', v)} placeholder="1rem" />
setStyle('marginBottom', v)} placeholder="1rem" />
Shape & Layout
setStyle('borderRadius', v)} placeholder="0.5rem" />
setStyle('maxWidth', v)} placeholder="640px" />
Opacity
setStyle('opacity', e.target.value)}
className="flex-1 accent-indigo-500" />
{styleVal('opacity') || '1'}
Border
setStyle('border', v)} placeholder="1px solid #e4e4e7" />
setStyle('boxShadow', v)} placeholder="0 4px 24px rgba(0,0,0,0.1)" />
)}
{/* Theme tab */}
{tab === 'theme' && (
Changes here apply to all blocks via CSS variables.
)}
)
}