Skip to content

Instantly share code, notes, and snippets.

@aneury1
Last active November 2, 2025 13:26
Show Gist options
  • Select an option

  • Save aneury1/b1d6092b5068cad0c9f943afd6e12b25 to your computer and use it in GitHub Desktop.

Select an option

Save aneury1/b1d6092b5068cad0c9f943afd6e12b25 to your computer and use it in GitHub Desktop.
import { useState, useCallback, useMemo } from 'react';
import {
Button,
Offcanvas,
Row,
Col,
Container,
ButtonGroup,
Form,
Card,
Image,
ListGroup,
Tab,
Tabs,
Alert,
Modal,
InputGroup,
Nav,
Navbar,
Badge,
Breadcrumb,
ProgressBar,
Spinner,
Toast,
Dropdown,
Accordion,
Carousel,
Pagination,
Placeholder
} from 'react-bootstrap';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
// ───────────────────────────────
// Constants & Types
// ───────────────────────────────
const ItemTypes = { NODE: 'node' };
const NODE_CONFIG = {
// Layout
container: {
label: 'Container',
defaultText: '',
canHaveChildren: true,
allowedChildren: ['row', 'container', 'card', 'button', 'image', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'alert', 'badge', 'breadcrumb', 'progressbar', 'spinner', 'nav', 'navbar', 'listgroup', 'table', 'accordion', 'carousel', 'dropdown', 'form', 'input', 'textarea', 'select', 'checkbox', 'radio', 'range', 'tabs', 'pagination', 'toast', 'modal']
},
row: {
label: 'Row',
defaultText: '',
canHaveChildren: true,
allowedChildren: ['column']
},
column: {
label: 'Column',
defaultText: '',
canHaveChildren: true,
allowedChildren: ['container', 'row', 'card', 'button', 'image', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'alert', 'badge', 'breadcrumb', 'progressbar', 'spinner', 'nav', 'navbar', 'listgroup', 'table', 'accordion', 'carousel', 'dropdown', 'form', 'input', 'textarea', 'select', 'checkbox', 'radio', 'range', 'tabs', 'pagination', 'toast', 'modal']
},
// Content
card: {
label: 'Card',
defaultText: '',
canHaveChildren: true,
allowedChildren: ['container', 'row', 'button', 'image', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'alert', 'badge', 'listgroup', 'form', 'input']
},
button: {
label: 'Button',
defaultText: 'Click Me',
canHaveChildren: false,
variants: ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark', 'link', 'outline-primary', 'outline-secondary', 'outline-success', 'outline-danger', 'outline-warning', 'outline-info', 'outline-light', 'outline-dark'],
sizes: ['sm', 'lg']
},
image: {
label: 'Image',
defaultText: '',
canHaveChildren: false,
shapes: ['fluid', 'thumbnail', 'rounded', 'rounded-circle']
},
// Typography
h1: { label: 'Heading 1', defaultText: 'Heading 1', canHaveChildren: false },
h2: { label: 'Heading 2', defaultText: 'Heading 2', canHaveChildren: false },
h3: { label: 'Heading 3', defaultText: 'Heading 3', canHaveChildren: false },
h4: { label: 'Heading 4', defaultText: 'Heading 4', canHaveChildren: false },
h5: { label: 'Heading 5', defaultText: 'Heading 5', canHaveChildren: false },
h6: { label: 'Heading 6', defaultText: 'Heading 6', canHaveChildren: false },
p: { label: 'Paragraph', defaultText: 'Sample paragraph text', canHaveChildren: false },
// Form Components
form: {
label: 'Form',
defaultText: '',
canHaveChildren: true,
allowedChildren: ['input', 'textarea', 'select', 'checkbox', 'radio', 'range', 'button']
},
input: {
label: 'Input',
defaultText: '',
canHaveChildren: false,
types: ['text', 'email', 'password', 'number', 'tel', 'url', 'search', 'date', 'time', 'datetime-local'],
sizes: ['sm', 'lg']
},
textarea: {
label: 'Textarea',
defaultText: '',
canHaveChildren: false,
sizes: ['sm', 'lg']
},
select: {
label: 'Select',
defaultText: '',
canHaveChildren: false,
sizes: ['sm', 'lg']
},
checkbox: {
label: 'Checkbox',
defaultText: 'Checkbox',
canHaveChildren: false,
inline: [false, true]
},
radio: {
label: 'Radio',
defaultText: 'Radio',
canHaveChildren: false,
inline: [false, true]
},
range: {
label: 'Range',
defaultText: '',
canHaveChildren: false
},
// Components
alert: {
label: 'Alert',
defaultText: 'This is an alert message',
canHaveChildren: true,
variants: ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark']
},
badge: {
label: 'Badge',
defaultText: 'Badge',
canHaveChildren: false,
variants: ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'],
styles: ['', 'pill']
},
breadcrumb: {
label: 'Breadcrumb',
defaultText: '',
canHaveChildren: true
},
progressbar: {
label: 'Progress Bar',
defaultText: '',
canHaveChildren: false,
variants: ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'],
styles: ['', 'striped', 'animated']
},
spinner: {
label: 'Spinner',
defaultText: '',
canHaveChildren: false,
variants: ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'],
types: ['border', 'grow'],
sizes: ['sm']
},
// Navigation
nav: {
label: 'Nav',
defaultText: '',
canHaveChildren: true,
variants: ['tabs', 'pills'],
layouts: ['', 'fill', 'justified']
},
navbar: {
label: 'Navbar',
defaultText: '',
canHaveChildren: true,
variants: ['light', 'dark'],
themes: ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark']
},
tabs: {
label: 'Tabs',
defaultText: '',
canHaveChildren: true,
variants: ['tabs', 'pills']
},
pagination: {
label: 'Pagination',
defaultText: '',
canHaveChildren: false,
sizes: ['sm', 'lg']
},
// Data Display
listgroup: {
label: 'List Group',
defaultText: '',
canHaveChildren: true,
variants: ['flush']
},
table: {
label: 'Table',
defaultText: '',
canHaveChildren: true,
variants: ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'],
styles: ['', 'striped', 'bordered', 'hover', 'sm']
},
// Interactive
dropdown: {
label: 'Dropdown',
defaultText: 'Dropdown',
canHaveChildren: true,
variants: ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'],
sizes: ['sm', 'lg'],
directions: ['down', 'up', 'start', 'end']
},
accordion: {
label: 'Accordion',
defaultText: '',
canHaveChildren: true,
flush: [false, true]
},
carousel: {
label: 'Carousel',
defaultText: '',
canHaveChildren: true,
variants: ['dark'],
controls: [true, false],
indicators: [true, false]
},
modal: {
label: 'Modal',
defaultText: '',
canHaveChildren: true,
sizes: ['sm', 'lg', 'xl']
},
// Feedback
toast: {
label: 'Toast',
defaultText: 'Toast message',
canHaveChildren: true,
positions: ['top-start', 'top-center', 'top-end', 'middle-start', 'middle-center', 'middle-end', 'bottom-start', 'bottom-center', 'bottom-end']
}
};
// ───────────────────────────────
// UI Components
// ───────────────────────────────
function DevContainer({ children, selected, onClick, style, nodeType }) {
return (
<Container
fluid
onClick={onClick}
className="position-relative"
style={{
border: selected ? '2px solid #0d6efd' : '1px solid #dee2e6',
padding: style?.padding || '1rem',
margin: style?.margin || '0.5rem',
backgroundColor: style?.backgroundColor || 'transparent',
borderRadius: style?.borderRadius || '0.375rem',
minHeight: '100px',
transition: 'all 0.2s ease',
...style
}}
title={nodeType}
>
{children}
{selected && (
<div
className="position-absolute top-0 start-0 badge bg-primary"
style={{ fontSize: '0.6rem', transform: 'translateY(-50%)' }}
>
{nodeType}
</div>
)}
</Container>
);
}
function DevRow({ children, selected, onClick, style, nodeType }) {
return (
<Row
onClick={onClick}
className="position-relative"
style={{
border: selected ? '2px solid #198754' : '1px dashed #6c757d',
padding: style?.padding || '1rem',
margin: style?.margin || '0.5rem',
backgroundColor: style?.backgroundColor || 'transparent',
borderRadius: style?.borderRadius || '0.375rem',
minHeight: '80px',
transition: 'all 0.2s ease',
...style
}}
title={nodeType}
>
{children}
{selected && (
<div
className="position-absolute top-0 start-0 badge bg-success"
style={{ fontSize: '0.6rem', transform: 'translateY(-50%)' }}
>
{nodeType}
</div>
)}
</Row>
);
}
function DevCol({ children, selected, onClick, style, nodeType, colSize }) {
return (
<Col
{...(colSize ? { [colSize]: true } : {})}
onClick={onClick}
className="position-relative"
style={{
border: selected ? '2px solid #6f42c1' : '1px dashed #adb5bd',
padding: style?.padding || '1rem',
margin: style?.margin || '0.5rem',
backgroundColor: style?.backgroundColor || 'transparent',
borderRadius: style?.borderRadius || '0.375rem',
minHeight: '60px',
transition: 'all 0.2s ease',
...style
}}
title={nodeType}
>
{children}
{selected && (
<div
className="position-absolute top-0 start-0 badge bg-purple"
style={{
fontSize: '0.6rem',
transform: 'translateY(-50%)',
backgroundColor: '#6f42c1'
}}
>
{nodeType} {colSize && `(${colSize})`}
</div>
)}
</Col>
);
}
function EditableText({ text, tag = 'p', onChange, style, onClick, className = '' }) {
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState(text);
const Tag = tag;
const handleSave = useCallback(() => {
setEditing(false);
if (draft !== text) {
onChange(draft);
}
}, [draft, text, onChange]);
const handleKeyDown = useCallback((e) => {
if (e.key === 'Enter') {
handleSave();
} else if (e.key === 'Escape') {
setDraft(text);
setEditing(false);
}
}, [handleSave, text]);
if (editing) {
return (
<Form.Control
type="text"
value={draft}
autoFocus
onChange={(e) => setDraft(e.target.value)}
onBlur={handleSave}
onKeyDown={handleKeyDown}
style={style}
className={className}
/>
);
}
return (
<Tag
style={{
cursor: 'pointer',
margin: 0,
...style
}}
className={`editable-text ${className}`}
onClick={(e) => {
e.stopPropagation();
setEditing(true);
onClick?.();
}}
>
{text || `Click to edit ${tag}`}
</Tag>
);
}
// ───────────────────────────────
// Tree Components
// ───────────────────────────────
function TreeNode({ node, depth, moveNode, selectNode, selectedId, onDelete, isRootNode = false }) {
const [{ isDragging }, dragRef] = useDrag({
type: ItemTypes.NODE,
item: { id: node.id },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
const [, dropRef] = useDrop({
accept: ItemTypes.NODE,
hover: (dragged) => {
if (dragged.id !== node.id && NODE_CONFIG[node.type]?.canHaveChildren) {
moveNode(dragged.id, node.id);
}
},
});
const config = NODE_CONFIG[node.type];
return (
<div
ref={(el) => dragRef(dropRef(el))}
style={{
marginLeft: depth * 20,
padding: '6px 12px',
cursor: 'grab',
backgroundColor: node.id === selectedId ? '#e7f1ff' : 'transparent',
border: node.id === selectedId ? '1px solid #0d6efd' : '1px solid transparent',
borderRadius: '4px',
opacity: isDragging ? 0.5 : 1,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '2px',
}}
onClick={() => selectNode(node.id)}
className="tree-node"
>
<div>
<span style={{ fontWeight: 'bold', marginRight: '8px' }}>
{config?.label || node.type}
</span>
{node.text && (
<small style={{ color: '#6c757d' }}>"{node.text}"</small>
)}
{node.variant && (
<small style={{ color: '#6c757d', marginLeft: '8px' }}>({node.variant})</small>
)}
</div>
{!isRootNode && (
<Button
variant="outline-danger"
size="sm"
onClick={(e) => {
e.stopPropagation();
onDelete(node.id);
}}
style={{ padding: '0.125rem 0.25rem', fontSize: '0.75rem' }}
>
×
</Button>
)}
</div>
);
}
function TreeView({ nodes, depth = 0, moveNode, selectNode, selectedId, onDelete }) {
return (
<>
{nodes.map((node, index) => (
<div key={node.id}>
<TreeNode
node={node}
depth={depth}
moveNode={moveNode}
selectNode={selectNode}
selectedId={selectedId}
onDelete={onDelete}
isRootNode={depth === 0 && index === 0}
/>
{node.children?.length > 0 && (
<TreeView
nodes={node.children}
depth={depth + 1}
moveNode={moveNode}
selectNode={selectNode}
selectedId={selectedId}
onDelete={onDelete}
/>
)}
</div>
))}
</>
);
}
// ───────────────────────────────
// Property Panels
// ───────────────────────────────
function CommonProperties({ selectedNode, onStyleChange, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Common Styles</h6>
<Form.Group className="mb-2">
<Form.Label>Background Color</Form.Label>
<Form.Control
type="color"
value={selectedNode.style?.backgroundColor || '#ffffff'}
onChange={(e) => onStyleChange('backgroundColor', e.target.value)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Text Color</Form.Label>
<Form.Control
type="color"
value={selectedNode.style?.color || '#000000'}
onChange={(e) => onStyleChange('color', e.target.value)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Padding</Form.Label>
<Form.Control
type="text"
placeholder="e.g., 10px or 1rem"
value={selectedNode.style?.padding || ''}
onChange={(e) => onStyleChange('padding', e.target.value)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Margin</Form.Label>
<Form.Control
type="text"
placeholder="e.g., 5px or 0.5rem"
value={selectedNode.style?.margin || ''}
onChange={(e) => onStyleChange('margin', e.target.value)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Border Radius</Form.Label>
<Form.Control
type="text"
placeholder="e.g., 5px or 0.5rem"
value={selectedNode.style?.borderRadius || ''}
onChange={(e) => onStyleChange('borderRadius', e.target.value)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Border</Form.Label>
<Form.Control
type="text"
placeholder="e.g., 1px solid #ccc"
value={selectedNode.style?.border || ''}
onChange={(e) => onStyleChange('border', e.target.value)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Width</Form.Label>
<Form.Control
type="text"
placeholder="e.g., 100% or 200px"
value={selectedNode.style?.width || ''}
onChange={(e) => onStyleChange('width', e.target.value)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Height</Form.Label>
<Form.Control
type="text"
placeholder="e.g., 100px or auto"
value={selectedNode.style?.height || ''}
onChange={(e) => onStyleChange('height', e.target.value)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Font Size</Form.Label>
<Form.Control
type="text"
placeholder="e.g., 16px or 1rem"
value={selectedNode.style?.fontSize || ''}
onChange={(e) => onStyleChange('fontSize', e.target.value)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Font Weight</Form.Label>
<Form.Select
value={selectedNode.style?.fontWeight || ''}
onChange={(e) => onStyleChange('fontWeight', e.target.value)}
>
<option value="">Normal</option>
<option value="bold">Bold</option>
<option value="lighter">Lighter</option>
<option value="bolder">Bolder</option>
<option value="100">100</option>
<option value="200">200</option>
<option value="300">300</option>
<option value="400">400</option>
<option value="500">500</option>
<option value="600">600</option>
<option value="700">700</option>
<option value="800">800</option>
<option value="900">900</option>
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Text Align</Form.Label>
<Form.Select
value={selectedNode.style?.textAlign || ''}
onChange={(e) => onStyleChange('textAlign', e.target.value)}
>
<option value="">Left</option>
<option value="center">Center</option>
<option value="right">Right</option>
<option value="justify">Justify</option>
</Form.Select>
</Form.Group>
</div>
);
}
function ButtonProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Button Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Variant</Form.Label>
<Form.Select
value={selectedNode.variant || 'primary'}
onChange={(e) => onPropertyChange('variant', e.target.value)}
>
{NODE_CONFIG.button.variants.map(variant => (
<option key={variant} value={variant}>
{variant.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Size</Form.Label>
<Form.Select
value={selectedNode.size || ''}
onChange={(e) => onPropertyChange('size', e.target.value)}
>
<option value="">Default</option>
{NODE_CONFIG.button.sizes.map(size => (
<option key={size} value={size}>
{size.toUpperCase()}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Disabled</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.disabled || false}
onChange={(e) => onPropertyChange('disabled', e.target.checked)}
/>
</Form.Group>
</div>
);
}
function ImageProperties({ selectedNode, onPropertyChange, onImageChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Image Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Image URL</Form.Label>
<Form.Control
type="url"
placeholder="Enter image URL"
value={selectedNode.src || ''}
onChange={(e) => onImageChange(e.target.value)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Upload Image</Form.Label>
<Form.Control
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files[0];
if (file) {
const url = URL.createObjectURL(file);
onImageChange(url);
}
}}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Image Style</Form.Label>
<Form.Select
value={selectedNode.imageStyle || 'fluid'}
onChange={(e) => onPropertyChange('imageStyle', e.target.value)}
>
{NODE_CONFIG.image.shapes.map(shape => (
<option key={shape} value={shape}>
{shape.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Alt Text</Form.Label>
<Form.Control
type="text"
placeholder="Alternative text for accessibility"
value={selectedNode.alt || ''}
onChange={(e) => onPropertyChange('alt', e.target.value)}
/>
</Form.Group>
</div>
);
}
function AlertProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Alert Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Variant</Form.Label>
<Form.Select
value={selectedNode.variant || 'primary'}
onChange={(e) => onPropertyChange('variant', e.target.value)}
>
{NODE_CONFIG.alert.variants.map(variant => (
<option key={variant} value={variant}>
{variant.charAt(0).toUpperCase() + variant.slice(1)}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Dismissible</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.dismissible || false}
onChange={(e) => onPropertyChange('dismissible', e.target.checked)}
/>
</Form.Group>
</div>
);
}
function BadgeProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Badge Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Variant</Form.Label>
<Form.Select
value={selectedNode.variant || 'primary'}
onChange={(e) => onPropertyChange('variant', e.target.value)}
>
{NODE_CONFIG.badge.variants.map(variant => (
<option key={variant} value={variant}>
{variant.charAt(0).toUpperCase() + variant.slice(1)}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Pill</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.pill || false}
onChange={(e) => onPropertyChange('pill', e.target.checked)}
/>
</Form.Group>
</div>
);
}
function ProgressBarProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Progress Bar Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Variant</Form.Label>
<Form.Select
value={selectedNode.variant || 'primary'}
onChange={(e) => onPropertyChange('variant', e.target.value)}
>
{NODE_CONFIG.progressbar.variants.map(variant => (
<option key={variant} value={variant}>
{variant.charAt(0).toUpperCase() + variant.slice(1)}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Progress (%)</Form.Label>
<Form.Range
min="0"
max="100"
value={selectedNode.now || 50}
onChange={(e) => onPropertyChange('now', parseInt(e.target.value))}
/>
<Form.Text>{selectedNode.now || 50}%</Form.Text>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Animated</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.animated || false}
onChange={(e) => onPropertyChange('animated', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Striped</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.striped || false}
onChange={(e) => onPropertyChange('striped', e.target.checked)}
/>
</Form.Group>
</div>
);
}
function SpinnerProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Spinner Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Variant</Form.Label>
<Form.Select
value={selectedNode.variant || 'primary'}
onChange={(e) => onPropertyChange('variant', e.target.value)}
>
{NODE_CONFIG.spinner.variants.map(variant => (
<option key={variant} value={variant}>
{variant.charAt(0).toUpperCase() + variant.slice(1)}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Type</Form.Label>
<Form.Select
value={selectedNode.spinnerType || 'border'}
onChange={(e) => onPropertyChange('spinnerType', e.target.value)}
>
{NODE_CONFIG.spinner.types.map(type => (
<option key={type} value={type}>
{type.charAt(0).toUpperCase() + type.slice(1)}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Size</Form.Label>
<Form.Select
value={selectedNode.size || ''}
onChange={(e) => onPropertyChange('size', e.target.value)}
>
<option value="">Default</option>
<option value="sm">Small</option>
</Form.Select>
</Form.Group>
</div>
);
}
function FormProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Form Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Validated</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.validated || false}
onChange={(e) => onPropertyChange('validated', e.target.checked)}
/>
</Form.Group>
</div>
);
}
function InputProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Input Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Type</Form.Label>
<Form.Select
value={selectedNode.inputType || 'text'}
onChange={(e) => onPropertyChange('inputType', e.target.value)}
>
{NODE_CONFIG.input.types.map(type => (
<option key={type} value={type}>
{type.charAt(0).toUpperCase() + type.slice(1)}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Size</Form.Label>
<Form.Select
value={selectedNode.size || ''}
onChange={(e) => onPropertyChange('size', e.target.value)}
>
<option value="">Default</option>
{NODE_CONFIG.input.sizes.map(size => (
<option key={size} value={size}>
{size.toUpperCase()}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Placeholder</Form.Label>
<Form.Control
type="text"
placeholder="Input placeholder"
value={selectedNode.placeholder || ''}
onChange={(e) => onPropertyChange('placeholder', e.target.value)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Disabled</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.disabled || false}
onChange={(e) => onPropertyChange('disabled', e.target.checked)}
/>
</Form.Group>
</div>
);
}
function TextareaProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Textarea Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Rows</Form.Label>
<Form.Control
type="number"
value={selectedNode.rows || 3}
onChange={(e) => onPropertyChange('rows', parseInt(e.target.value))}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Placeholder</Form.Label>
<Form.Control
type="text"
placeholder="Textarea placeholder"
value={selectedNode.placeholder || ''}
onChange={(e) => onPropertyChange('placeholder', e.target.value)}
/>
</Form.Group>
</div>
);
}
function SelectProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Select Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Size</Form.Label>
<Form.Select
value={selectedNode.size || ''}
onChange={(e) => onPropertyChange('size', e.target.value)}
>
<option value="">Default</option>
{NODE_CONFIG.select.sizes.map(size => (
<option key={size} value={size}>
{size.toUpperCase()}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Multiple</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.multiple || false}
onChange={(e) => onPropertyChange('multiple', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Options</Form.Label>
<Form.Control
as="textarea"
rows={3}
placeholder="Enter options, one per line"
value={selectedNode.options ? selectedNode.options.join('\n') : 'Option 1\nOption 2\nOption 3'}
onChange={(e) => onPropertyChange('options', e.target.value.split('\n').filter(opt => opt.trim()))}
/>
</Form.Group>
</div>
);
}
function CheckboxProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Checkbox Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Checked</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.checked || false}
onChange={(e) => onPropertyChange('checked', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Disabled</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.disabled || false}
onChange={(e) => onPropertyChange('disabled', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Inline</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.inline || false}
onChange={(e) => onPropertyChange('inline', e.target.checked)}
/>
</Form.Group>
</div>
);
}
function RadioProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Radio Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Checked</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.checked || false}
onChange={(e) => onPropertyChange('checked', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Disabled</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.disabled || false}
onChange={(e) => onPropertyChange('disabled', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Inline</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.inline || false}
onChange={(e) => onPropertyChange('inline', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Name</Form.Label>
<Form.Control
type="text"
placeholder="Radio group name"
value={selectedNode.name || 'radio-group'}
onChange={(e) => onPropertyChange('name', e.target.value)}
/>
</Form.Group>
</div>
);
}
function RangeProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Range Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Min Value</Form.Label>
<Form.Control
type="number"
value={selectedNode.min || 0}
onChange={(e) => onPropertyChange('min', parseInt(e.target.value))}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Max Value</Form.Label>
<Form.Control
type="number"
value={selectedNode.max || 100}
onChange={(e) => onPropertyChange('max', parseInt(e.target.value))}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Current Value</Form.Label>
<Form.Control
type="number"
value={selectedNode.value || 50}
onChange={(e) => onPropertyChange('value', parseInt(e.target.value))}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Step</Form.Label>
<Form.Control
type="number"
value={selectedNode.step || 1}
onChange={(e) => onPropertyChange('step', parseInt(e.target.value))}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Disabled</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.disabled || false}
onChange={(e) => onPropertyChange('disabled', e.target.checked)}
/>
</Form.Group>
</div>
);
}
function DropdownProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Dropdown Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Variant</Form.Label>
<Form.Select
value={selectedNode.variant || 'primary'}
onChange={(e) => onPropertyChange('variant', e.target.value)}
>
{NODE_CONFIG.dropdown.variants.map(variant => (
<option key={variant} value={variant}>
{variant.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Size</Form.Label>
<Form.Select
value={selectedNode.size || ''}
onChange={(e) => onPropertyChange('size', e.target.value)}
>
<option value="">Default</option>
{NODE_CONFIG.dropdown.sizes.map(size => (
<option key={size} value={size}>
{size.toUpperCase()}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Split Button</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.split || false}
onChange={(e) => onPropertyChange('split', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Menu Items</Form.Label>
<Form.Control
as="textarea"
rows={3}
placeholder="Enter menu items, one per line"
value={selectedNode.menuItems ? selectedNode.menuItems.join('\n') : 'Action\nAnother action\nSomething else'}
onChange={(e) => onPropertyChange('menuItems', e.target.value.split('\n').filter(item => item.trim()))}
/>
</Form.Group>
</div>
);
}
function NavProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Nav Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Variant</Form.Label>
<Form.Select
value={selectedNode.variant || 'tabs'}
onChange={(e) => onPropertyChange('variant', e.target.value)}
>
{NODE_CONFIG.nav.variants.map(variant => (
<option key={variant} value={variant}>
{variant.charAt(0).toUpperCase() + variant.slice(1)}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Fill</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.fill || false}
onChange={(e) => onPropertyChange('fill', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Justified</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.justified || false}
onChange={(e) => onPropertyChange('justified', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Nav Items</Form.Label>
<Form.Control
as="textarea"
rows={3}
placeholder="Enter nav items, one per line"
value={selectedNode.navItems ? selectedNode.navItems.join('\n') : 'Home\nFeatures\nPricing'}
onChange={(e) => onPropertyChange('navItems', e.target.value.split('\n').filter(item => item.trim()))}
/>
</Form.Group>
</div>
);
}
function NavbarProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Navbar Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Variant</Form.Label>
<Form.Select
value={selectedNode.variant || 'light'}
onChange={(e) => onPropertyChange('variant', e.target.value)}
>
{NODE_CONFIG.navbar.variants.map(variant => (
<option key={variant} value={variant}>
{variant.charAt(0).toUpperCase() + variant.slice(1)}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Background</Form.Label>
<Form.Select
value={selectedNode.bg || ''}
onChange={(e) => onPropertyChange('bg', e.target.value)}
>
<option value="">Default</option>
{NODE_CONFIG.navbar.themes.map(theme => (
<option key={theme} value={theme}>
{theme.charAt(0).toUpperCase() + theme.slice(1)}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Expand Breakpoint</Form.Label>
<Form.Select
value={selectedNode.expand || 'lg'}
onChange={(e) => onPropertyChange('expand', e.target.value)}
>
<option value="sm">Small</option>
<option value="md">Medium</option>
<option value="lg">Large</option>
<option value="xl">Extra Large</option>
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Fixed Top</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.fixed === 'top'}
onChange={(e) => onPropertyChange('fixed', e.target.checked ? 'top' : '')}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Brand Text</Form.Label>
<Form.Control
type="text"
placeholder="Brand name"
value={selectedNode.brand || 'Navbar'}
onChange={(e) => onPropertyChange('brand', e.target.value)}
/>
</Form.Group>
</div>
);
}
function TabsProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Tabs Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Variant</Form.Label>
<Form.Select
value={selectedNode.variant || 'tabs'}
onChange={(e) => onPropertyChange('variant', e.target.value)}
>
{NODE_CONFIG.tabs.variants.map(variant => (
<option key={variant} value={variant}>
{variant.charAt(0).toUpperCase() + variant.slice(1)}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Justify</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.justify || false}
onChange={(e) => onPropertyChange('justify', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Fill</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.fill || false}
onChange={(e) => onPropertyChange('fill', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Tab Titles</Form.Label>
<Form.Control
as="textarea"
rows={3}
placeholder="Enter tab titles, one per line"
value={selectedNode.tabTitles ? selectedNode.tabTitles.join('\n') : 'Tab 1\nTab 2\nTab 3'}
onChange={(e) => onPropertyChange('tabTitles', e.target.value.split('\n').filter(title => title.trim()))}
/>
</Form.Group>
</div>
);
}
function ModalProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Modal Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Size</Form.Label>
<Form.Select
value={selectedNode.size || ''}
onChange={(e) => onPropertyChange('size', e.target.value)}
>
<option value="">Default</option>
{NODE_CONFIG.modal.sizes.map(size => (
<option key={size} value={size}>
{size.toUpperCase()}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Backdrop</Form.Label>
<Form.Select
value={selectedNode.backdrop || 'true'}
onChange={(e) => onPropertyChange('backdrop', e.target.value)}
>
<option value="true">True</option>
<option value="false">False</option>
<option value="static">Static</option>
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Keyboard</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.keyboard !== false}
onChange={(e) => onPropertyChange('keyboard', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Scrollable</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.scrollable || false}
onChange={(e) => onPropertyChange('scrollable', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Centered</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.centered || false}
onChange={(e) => onPropertyChange('centered', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Modal Title</Form.Label>
<Form.Control
type="text"
placeholder="Modal title"
value={selectedNode.modalTitle || 'Modal Title'}
onChange={(e) => onPropertyChange('modalTitle', e.target.value)}
/>
</Form.Group>
</div>
);
}
function AccordionProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Accordion Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Flush</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.flush || false}
onChange={(e) => onPropertyChange('flush', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Always Open</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.alwaysOpen || false}
onChange={(e) => onPropertyChange('alwaysOpen', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Number of Items</Form.Label>
<Form.Control
type="number"
min="1"
max="10"
value={selectedNode.items || 3}
onChange={(e) => onPropertyChange('items', parseInt(e.target.value))}
/>
</Form.Group>
</div>
);
}
function BreadcrumbProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Breadcrumb Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Breadcrumb Items</Form.Label>
<Form.Control
as="textarea"
rows={3}
placeholder="Enter breadcrumb items, one per line"
value={selectedNode.breadcrumbItems ? selectedNode.breadcrumbItems.join('\n') : 'Home\nLibrary\nData'}
onChange={(e) => onPropertyChange('breadcrumbItems', e.target.value.split('\n').filter(item => item.trim()))}
/>
</Form.Group>
</div>
);
}
function CarouselProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Carousel Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Variant</Form.Label>
<Form.Select
value={selectedNode.variant || ''}
onChange={(e) => onPropertyChange('variant', e.target.value)}
>
<option value="">Default</option>
<option value="dark">Dark</option>
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Controls</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.controls !== false}
onChange={(e) => onPropertyChange('controls', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Indicators</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.indicators !== false}
onChange={(e) => onPropertyChange('indicators', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Auto Play</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.autoPlay || false}
onChange={(e) => onPropertyChange('autoPlay', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Interval (ms)</Form.Label>
<Form.Control
type="number"
value={selectedNode.interval || 5000}
onChange={(e) => onPropertyChange('interval', parseInt(e.target.value))}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Number of Slides</Form.Label>
<Form.Control
type="number"
min="1"
max="10"
value={selectedNode.slides || 3}
onChange={(e) => onPropertyChange('slides', parseInt(e.target.value))}
/>
</Form.Group>
</div>
);
}
function ListGroupProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">List Group Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Variant</Form.Label>
<Form.Select
value={selectedNode.variant || ''}
onChange={(e) => onPropertyChange('variant', e.target.value)}
>
<option value="">Default</option>
<option value="flush">Flush</option>
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Numbered</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.numbered || false}
onChange={(e) => onPropertyChange('numbered', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Items</Form.Label>
<Form.Control
as="textarea"
rows={3}
placeholder="Enter list items, one per line"
value={selectedNode.items ? selectedNode.items.join('\n') : 'Item 1\nItem 2\nItem 3'}
onChange={(e) => onPropertyChange('items', e.target.value.split('\n').filter(item => item.trim()))}
/>
</Form.Group>
</div>
);
}
function PaginationProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Pagination Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Size</Form.Label>
<Form.Select
value={selectedNode.size || ''}
onChange={(e) => onPropertyChange('size', e.target.value)}
>
<option value="">Default</option>
{NODE_CONFIG.pagination.sizes.map(size => (
<option key={size} value={size}>
{size.toUpperCase()}
</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Number of Pages</Form.Label>
<Form.Control
type="number"
min="1"
max="20"
value={selectedNode.pages || 5}
onChange={(e) => onPropertyChange('pages', parseInt(e.target.value))}
/>
</Form.Group>
</div>
);
}
function ToastProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Toast Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Show</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.show !== false}
onChange={(e) => onPropertyChange('show', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Autohide</Form.Label>
<Form.Check
type="switch"
checked={selectedNode.autohide || false}
onChange={(e) => onPropertyChange('autohide', e.target.checked)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Delay (ms)</Form.Label>
<Form.Control
type="number"
value={selectedNode.delay || 5000}
onChange={(e) => onPropertyChange('delay', parseInt(e.target.value))}
/>
</Form.Group>
</div>
);
}
function ColumnProperties({ selectedNode, onPropertyChange }) {
const colSizes = ['', 'xs', 'sm', 'md', 'lg', 'xl', 'xxl'];
const colSpans = ['', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', 'auto'];
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Column Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Column Size</Form.Label>
<InputGroup>
<Form.Select
value={selectedNode.colSizeType || 'xs'}
onChange={(e) => onPropertyChange('colSizeType', e.target.value)}
>
{colSizes.map(size => (
<option key={size} value={size}>
{size || 'Default'}
</option>
))}
</Form.Select>
<Form.Select
value={selectedNode.colSpan || ''}
onChange={(e) => onPropertyChange('colSpan', e.target.value)}
>
{colSpans.map(span => (
<option key={span} value={span}>
{span || 'Auto'}
</option>
))}
</Form.Select>
</InputGroup>
</Form.Group>
</div>
);
}
function CardProperties({ selectedNode, onPropertyChange }) {
return (
<div className="mb-3">
<h6 className="text-muted mb-2">Card Properties</h6>
<Form.Group className="mb-2">
<Form.Label>Card Title</Form.Label>
<Form.Control
type="text"
placeholder="Card title"
value={selectedNode.cardTitle || ''}
onChange={(e) => onPropertyChange('cardTitle', e.target.value)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Card Subtitle</Form.Label>
<Form.Control
type="text"
placeholder="Card subtitle"
value={selectedNode.cardSubtitle || ''}
onChange={(e) => onPropertyChange('cardSubtitle', e.target.value)}
/>
</Form.Group>
<Form.Group className="mb-2">
<Form.Label>Border</Form.Label>
<Form.Select
value={selectedNode.cardBorder || ''}
onChange={(e) => onPropertyChange('cardBorder', e.target.value)}
>
<option value="">Default</option>
<option value="primary">Primary</option>
<option value="secondary">Secondary</option>
<option value="success">Success</option>
<option value="danger">Danger</option>
<option value="warning">Warning</option>
<option value="info">Info</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</Form.Select>
</Form.Group>
</div>
);
}
// ───────────────────────────────
// Code Generation Utilities
// ───────────────────────────────
const generateReactCode = (nodes, indent = 0) => {
const spaces = ' '.repeat(indent);
return nodes.map(node => {
const styleString = Object.keys(node.style || {}).length > 0
? ` style={${JSON.stringify(node.style)}}`
: '';
const classNames = [];
if (node.className) classNames.push(node.className);
if (node.align) classNames.push(`text-${node.align}`);
const classNameString = classNames.length > 0
? ` className="${classNames.join(' ')}"`
: '';
switch (node.type) {
case 'container':
return `${spaces}<Container fluid${styleString}${classNameString}>\n${generateReactCode(node.children, indent + 1)}\n${spaces}</Container>`;
case 'row':
return `${spaces}<Row${styleString}${classNameString}>\n${generateReactCode(node.children, indent + 1)}\n${spaces}</Row>`;
case 'column':
const colProps = [];
if (node.colSizeType && node.colSpan) {
colProps.push(`${node.colSizeType === 'xs' ? '' : node.colSizeType + '-'}${node.colSpan}`);
}
const colPropsString = colProps.length > 0 ? ` ${colProps.join(' ')}` : '';
return `${spaces}<Col${colPropsString}${styleString}${classNameString}>\n${generateReactCode(node.children, indent + 1)}\n${spaces}</Col>`;
case 'card':
const cardProps = [];
if (node.cardBorder) cardProps.push(`border="${node.cardBorder}"`);
const cardPropsString = cardProps.length > 0 ? ` ${cardProps.join(' ')}` : '';
let cardContent = generateReactCode(node.children, indent + 2);
if (node.cardTitle || node.cardSubtitle) {
cardContent = `${spaces} <Card.Header>\n` +
(node.cardTitle ? `${spaces} <Card.Title>${node.cardTitle}</Card.Title>\n` : '') +
(node.cardSubtitle ? `${spaces} <Card.Subtitle>${node.cardSubtitle}</Card.Subtitle>\n` : '') +
`${spaces} </Card.Header>\n` +
`${spaces} <Card.Body>\n${cardContent}\n${spaces} </Card.Body>`;
} else {
cardContent = `${spaces} <Card.Body>\n${cardContent}\n${spaces} </Card.Body>`;
}
return `${spaces}<Card${cardPropsString}${styleString}${classNameString}>\n${cardContent}\n${spaces}</Card>`;
case 'button':
const buttonProps = [];
if (node.variant) buttonProps.push(`variant="${node.variant}"`);
if (node.size) buttonProps.push(`size="${node.size}"`);
if (node.disabled) buttonProps.push('disabled');
const buttonPropsString = buttonProps.length > 0 ? ` ${buttonProps.join(' ')}` : '';
return `${spaces}<Button${buttonPropsString}${styleString}${classNameString}>${node.text || 'Click Me'}</Button>`;
case 'image':
const imageProps = [];
if (node.alt) imageProps.push(`alt="${node.alt}"`);
if (node.imageStyle) {
if (node.imageStyle === 'fluid') imageProps.push('fluid');
else if (node.imageStyle === 'thumbnail') imageProps.push('thumbnail');
else if (node.imageStyle === 'rounded') imageProps.push('rounded');
else if (node.imageStyle === 'rounded-circle') imageProps.push('roundedCircle');
}
const imagePropsString = imageProps.length > 0 ? ` ${imageProps.join(' ')}` : '';
return `${spaces}<Image src="${node.src || 'https://via.placeholder.com/150'}"${imagePropsString}${styleString}${classNameString} />`;
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
return `${spaces}<${node.type}${styleString}${classNameString}>${node.text || 'Sample Heading'}</${node.type}>`;
case 'p':
return `${spaces}<p${styleString}${classNameString}>${node.text || 'Sample Paragraph'}</p>`;
case 'alert':
const alertProps = [];
if (node.variant) alertProps.push(`variant="${node.variant}"`);
if (node.dismissible) alertProps.push('dismissible');
const alertPropsString = alertProps.length > 0 ? ` ${alertProps.join(' ')}` : '';
return `${spaces}<Alert${alertPropsString}${styleString}${classNameString}>\n${spaces} ${node.text || 'Alert message'}\n${spaces}</Alert>`;
case 'badge':
const badgeProps = [];
if (node.variant) badgeProps.push(`bg="${node.variant}"`);
if (node.pill) badgeProps.push('pill');
const badgePropsString = badgeProps.length > 0 ? ` ${badgeProps.join(' ')}` : '';
return `${spaces}<Badge${badgePropsString}${styleString}${classNameString}>${node.text || 'Badge'}</Badge>`;
case 'progressbar':
const progressProps = [];
if (node.variant) progressProps.push(`variant="${node.variant}"`);
if (node.now) progressProps.push(`now={${node.now}}`);
if (node.animated) progressProps.push('animated');
if (node.striped) progressProps.push('striped');
const progressPropsString = progressProps.length > 0 ? ` ${progressProps.join(' ')}` : '';
return `${spaces}<ProgressBar${progressPropsString}${styleString}${classNameString} />`;
case 'spinner':
const spinnerProps = [];
if (node.variant) spinnerProps.push(`variant="${node.variant}"`);
if (node.spinnerType) spinnerProps.push(`animation="${node.spinnerType}"`);
if (node.size) spinnerProps.push(`size="${node.size}"`);
const spinnerPropsString = spinnerProps.length > 0 ? ` ${spinnerProps.join(' ')}` : '';
return `${spaces}<Spinner${spinnerPropsString}${styleString}${classNameString} />`;
case 'form':
const formProps = [];
if (node.validated) formProps.push('validated');
const formPropsString = formProps.length > 0 ? ` ${formProps.join(' ')}` : '';
return `${spaces}<Form${formPropsString}${styleString}${classNameString}>\n${generateReactCode(node.children, indent + 1)}\n${spaces}</Form>`;
case 'input':
const inputProps = [];
if (node.inputType) inputProps.push(`type="${node.inputType}"`);
if (node.placeholder) inputProps.push(`placeholder="${node.placeholder}"`);
if (node.size) inputProps.push(`size="${node.size}"`);
if (node.disabled) inputProps.push('disabled');
const inputPropsString = inputProps.length > 0 ? ` ${inputProps.join(' ')}` : '';
return `${spaces}<Form.Control${inputPropsString}${styleString}${classNameString} />`;
case 'textarea':
const textareaProps = [];
if (node.placeholder) textareaProps.push(`placeholder="${node.placeholder}"`);
if (node.rows) textareaProps.push(`rows={${node.rows}}`);
const textareaPropsString = textareaProps.length > 0 ? ` ${textareaProps.join(' ')}` : '';
return `${spaces}<Form.Control as="textarea"${textareaPropsString}${styleString}${classNameString} />`;
case 'select':
const selectProps = [];
if (node.multiple) selectProps.push('multiple');
const selectPropsString = selectProps.length > 0 ? ` ${selectProps.join(' ')}` : '';
const options = node.options || ['Option 1', 'Option 2', 'Option 3'];
const optionsCode = options.map(opt => `\n${spaces} <option>${opt}</option>`).join('');
return `${spaces}<Form.Select${selectPropsString}${styleString}${classNameString}>${optionsCode}\n${spaces}</Form.Select>`;
case 'checkbox':
const checkboxProps = [];
if (node.checked) checkboxProps.push('defaultChecked');
if (node.disabled) checkboxProps.push('disabled');
if (node.inline) checkboxProps.push('inline');
const checkboxPropsString = checkboxProps.length > 0 ? ` ${checkboxProps.join(' ')}` : '';
return `${spaces}<Form.Check type="checkbox"${checkboxPropsString}${styleString}${classNameString} label="${node.text || 'Checkbox'}" />`;
case 'radio':
const radioProps = [];
if (node.checked) radioProps.push('defaultChecked');
if (node.disabled) radioProps.push('disabled');
if (node.inline) radioProps.push('inline');
if (node.name) radioProps.push(`name="${node.name}"`);
const radioPropsString = radioProps.length > 0 ? ` ${radioProps.join(' ')}` : '';
return `${spaces}<Form.Check type="radio"${radioPropsString}${styleString}${classNameString} label="${node.text || 'Radio'}" />`;
case 'range':
const rangeProps = [];
if (node.min !== undefined) rangeProps.push(`min={${node.min}}`);
if (node.max !== undefined) rangeProps.push(`max={${node.max}}`);
if (node.value !== undefined) rangeProps.push(`value={${node.value}}`);
if (node.step !== undefined) rangeProps.push(`step={${node.step}}`);
if (node.disabled) rangeProps.push('disabled');
const rangePropsString = rangeProps.length > 0 ? ` ${rangeProps.join(' ')}` : '';
return `${spaces}<Form.Range${rangePropsString}${styleString}${classNameString} />`;
case 'dropdown':
const dropdownProps = [];
if (node.variant) dropdownProps.push(`variant="${node.variant}"`);
if (node.size) dropdownProps.push(`size="${node.size}"`);
if (node.split) dropdownProps.push('split');
const dropdownPropsString = dropdownProps.length > 0 ? ` ${dropdownProps.join(' ')}` : '';
const menuItems = node.menuItems || ['Action', 'Another action', 'Something else'];
const menuItemsCode = menuItems.map(item => `\n${spaces} <Dropdown.Item>${item}</Dropdown.Item>`).join('');
return `${spaces}<Dropdown${dropdownPropsString}${styleString}${classNameString}>\n${spaces} <Dropdown.Toggle>${node.text || 'Dropdown'}</Dropdown.Toggle>\n${spaces} <Dropdown.Menu>${menuItemsCode}\n${spaces} </Dropdown.Menu>\n${spaces}</Dropdown>`;
case 'nav':
const navProps = [];
if (node.variant) navProps.push(`variant="${node.variant}"`);
if (node.fill) navProps.push('fill');
if (node.justified) navProps.push('justified');
const navPropsString = navProps.length > 0 ? ` ${navProps.join(' ')}` : '';
const navItems = node.navItems || ['Home', 'Features', 'Pricing'];
const navItemsCode = navItems.map(item => `\n${spaces} <Nav.Link>${item}</Nav.Link>`).join('');
return `${spaces}<Nav${navPropsString}${styleString}${classNameString}>${navItemsCode}\n${spaces}</Nav>`;
case 'navbar':
const navbarProps = [];
if (node.variant) navbarProps.push(`variant="${node.variant}"`);
if (node.bg) navbarProps.push(`bg="${node.bg}"`);
if (node.expand) navbarProps.push(`expand="${node.expand}"`);
if (node.fixed) navbarProps.push(`fixed="${node.fixed}"`);
const navbarPropsString = navbarProps.length > 0 ? ` ${navbarProps.join(' ')}` : '';
return `${spaces}<Navbar${navbarPropsString}${styleString}${classNameString}>\n${spaces} <Container>\n${spaces} <Navbar.Brand>${node.brand || 'Navbar'}</Navbar.Brand>\n${spaces} </Container>\n${spaces}</Navbar>`;
case 'tabs':
const tabsProps = [];
if (node.variant) tabsProps.push(`variant="${node.variant}"`);
if (node.justify) tabsProps.push('justify');
if (node.fill) tabsProps.push('fill');
const tabsPropsString = tabsProps.length > 0 ? ` ${tabsProps.join(' ')}` : '';
const tabTitles = node.tabTitles || ['Tab 1', 'Tab 2', 'Tab 3'];
const tabsCode = tabTitles.map((title, index) =>
`\n${spaces} <Tab eventKey="tab${index}" title="${title}">\n${spaces} <p>Content for ${title}</p>\n${spaces} </Tab>`
).join('');
return `${spaces}<Tabs defaultActiveKey="tab0"${tabsPropsString}${styleString}${classNameString}>\n${tabsCode}\n${spaces}</Tabs>`;
case 'modal':
const modalProps = [];
if (node.size) modalProps.push(`size="${node.size}"`);
if (node.backdrop) modalProps.push(`backdrop="${node.backdrop}"`);
if (node.keyboard !== undefined) modalProps.push(`keyboard={${node.keyboard}}`);
if (node.scrollable) modalProps.push('scrollable');
if (node.centered) modalProps.push('centered');
const modalPropsString = modalProps.length > 0 ? ` ${modalProps.join(' ')}` : '';
return `${spaces}<Modal show={false}${modalPropsString}${styleString}${classNameString}>\n${spaces} <Modal.Header closeButton>\n${spaces} <Modal.Title>${node.modalTitle || 'Modal Title'}</Modal.Title>\n${spaces} </Modal.Header>\n${spaces} <Modal.Body>\n${spaces} <p>Modal body text goes here.</p>\n${spaces} </Modal.Body>\n${spaces} <Modal.Footer>\n${spaces} <Button variant="secondary">Close</Button>\n${spaces} <Button variant="primary">Save changes</Button>\n${spaces} </Modal.Footer>\n${spaces}</Modal>`;
case 'accordion':
const accordionProps = [];
if (node.flush) accordionProps.push('flush');
if (node.alwaysOpen) accordionProps.push('alwaysOpen');
const accordionPropsString = accordionProps.length > 0 ? ` ${accordionProps.join(' ')}` : '';
const accordionItems = node.items || 3;
const accordionCode = Array.from({ length: accordionItems }, (_, i) =>
`\n${spaces} <Accordion.Item eventKey="${i}">\n${spaces} <Accordion.Header>Accordion Item #${i + 1}</Accordion.Header>\n${spaces} <Accordion.Body>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</Accordion.Body>\n${spaces} </Accordion.Item>`
).join('');
return `${spaces}<Accordion${accordionPropsString}${styleString}${classNameString}>${accordionCode}\n${spaces}</Accordion>`;
case 'breadcrumb':
const breadcrumbItems = node.breadcrumbItems || ['Home', 'Library', 'Data'];
const breadcrumbCode = breadcrumbItems.map((item, index) =>
`\n${spaces} <Breadcrumb.Item${index === breadcrumbItems.length - 1 ? ' active' : ''}>${item}</Breadcrumb.Item>`
).join('');
return `${spaces}<Breadcrumb${styleString}${classNameString}>${breadcrumbCode}\n${spaces}</Breadcrumb>`;
case 'carousel':
const carouselProps = [];
if (node.variant === 'dark') carouselProps.push('variant="dark"');
if (node.controls !== false) carouselProps.push('controls');
if (node.indicators !== false) carouselProps.push('indicators');
if (node.autoPlay) carouselProps.push(`interval={${node.interval || 5000}}`);
const carouselPropsString = carouselProps.length > 0 ? ` ${carouselProps.join(' ')}` : '';
const slides = node.slides || 3;
const carouselItems = Array.from({ length: slides }, (_, i) =>
`\n${spaces} <Carousel.Item>\n${spaces} <Image className="d-block w-100" src="https://via.placeholder.com/800x400?text=Slide+${i + 1}" alt="Slide ${i + 1}" />\n${spaces} <Carousel.Caption>\n${spaces} <h3>Slide ${i + 1} Label</h3>\n${spaces} <p>Nulla vitae elit libero, a pharetra augue mollis interdum.</p>\n${spaces} </Carousel.Caption>\n${spaces} </Carousel.Item>`
).join('');
return `${spaces}<Carousel${carouselPropsString}${styleString}${classNameString}>${carouselItems}\n${spaces}</Carousel>`;
case 'listgroup':
const listgroupProps = [];
if (node.variant === 'flush') listgroupProps.push('flush');
if (node.numbered) listgroupProps.push('numbered');
const listgroupPropsString = listgroupProps.length > 0 ? ` ${listgroupProps.join(' ')}` : '';
const items = node.items || ['Item 1', 'Item 2', 'Item 3'];
const itemsCode = items.map(item => `\n${spaces} <ListGroup.Item>${item}</ListGroup.Item>`).join('');
return `${spaces}<ListGroup${listgroupPropsString}${styleString}${classNameString}>${itemsCode}\n${spaces}</ListGroup>`;
case 'pagination':
const paginationProps = [];
if (node.size) paginationProps.push(`size="${node.size}"`);
const paginationPropsString = paginationProps.length > 0 ? ` ${paginationProps.join(' ')}` : '';
const pages = node.pages || 5;
const paginationItems = Array.from({ length: pages }, (_, i) =>
`\n${spaces} <Pagination.Item key={${i + 1}}>${i + 1}</Pagination.Item>`
).join('');
return `${spaces}<Pagination${paginationPropsString}${styleString}${classNameString}>${paginationItems}\n${spaces}</Pagination>`;
case 'toast':
const toastProps = [];
if (node.show !== undefined) toastProps.push(`show={${node.show}}`);
if (node.autohide) toastProps.push(`autohide={${node.autohide}}`);
if (node.delay) toastProps.push(`delay={${node.delay}}`);
const toastPropsString = toastProps.length > 0 ? ` ${toastProps.join(' ')}` : '';
return `${spaces}<Toast${toastPropsString}${styleString}${classNameString}>\n${spaces} <Toast.Header>\n${spaces} <strong className="me-auto">Notification</strong>\n${spaces} </Toast.Header>\n${spaces} <Toast.Body>${node.text || 'Toast message'}</Toast.Body>\n${spaces}</Toast>`;
default:
return `${spaces}<!-- Unknown component: ${node.type} -->`;
}
}).join('\n');
};
const generateBootstrapHTML = (nodes, indent = 0) => {
const spaces = ' '.repeat(indent);
return nodes.map(node => {
const styleProps = [];
if (node.style) {
Object.entries(node.style).forEach(([key, value]) => {
if (value) styleProps.push(`${key.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${value};`);
});
}
const styleString = styleProps.length > 0 ? ` style="${styleProps.join(' ')}"` : '';
const classNames = [];
switch (node.type) {
case 'container':
classNames.push('container-fluid');
break;
case 'row':
classNames.push('row');
break;
case 'column':
classNames.push('col');
if (node.colSizeType && node.colSpan) {
const prefix = node.colSizeType === 'xs' ? '' : `${node.colSizeType}-`;
classNames.push(`col-${prefix}${node.colSpan}`);
}
break;
case 'card':
classNames.push('card');
if (node.cardBorder) classNames.push(`border-${node.cardBorder}`);
break;
case 'button':
classNames.push('btn', `btn-${node.variant || 'primary'}`);
if (node.size) classNames.push(`btn-${node.size}`);
if (node.disabled) classNames.push('disabled');
break;
case 'image':
classNames.push('img-fluid');
if (node.imageStyle) {
if (node.imageStyle === 'thumbnail') classNames.push('img-thumbnail');
else if (node.imageStyle === 'rounded') classNames.push('rounded');
else if (node.imageStyle === 'rounded-circle') classNames.push('rounded-circle');
}
break;
case 'alert':
classNames.push('alert', `alert-${node.variant || 'primary'}`);
if (node.dismissible) classNames.push('alert-dismissible', 'fade', 'show');
break;
case 'badge':
classNames.push('badge', `bg-${node.variant || 'primary'}`);
if (node.pill) classNames.push('rounded-pill');
break;
case 'progressbar':
classNames.push('progress-bar');
if (node.variant) classNames.push(`bg-${node.variant}`);
if (node.striped) classNames.push('progress-bar-striped');
if (node.animated) classNames.push('progress-bar-animated');
break;
case 'spinner':
classNames.push(`spinner-${node.spinnerType || 'border'}`);
if (node.variant) classNames.push(`text-${node.variant}`);
if (node.size) classNames.push(`spinner-${node.spinnerType || 'border'}-${node.size}`);
break;
case 'form':
if (node.validated) classNames.push('was-validated');
break;
case 'input':
case 'textarea':
case 'select':
classNames.push('form-control');
if (node.size) classNames.push(`form-control-${node.size}`);
break;
case 'checkbox':
case 'radio':
classNames.push('form-check-input');
break;
case 'range':
classNames.push('form-range');
break;
case 'dropdown':
classNames.push('dropdown');
break;
case 'nav':
classNames.push('nav');
if (node.variant) classNames.push(`nav-${node.variant}`);
if (node.fill) classNames.push('nav-fill');
if (node.justified) classNames.push('nav-justified');
break;
case 'navbar':
classNames.push('navbar');
if (node.variant) classNames.push(`navbar-${node.variant}`);
if (node.bg) classNames.push(`bg-${node.bg}`);
if (node.expand) classNames.push(`navbar-expand-${node.expand}`);
if (node.fixed) classNames.push(`fixed-${node.fixed}`);
break;
case 'tabs':
classNames.push('nav');
if (node.variant) classNames.push(`nav-${node.variant}`);
if (node.justify) classNames.push('nav-justified');
if (node.fill) classNames.push('nav-fill');
break;
case 'modal':
classNames.push('modal');
if (node.size) classNames.push(`modal-${node.size}`);
break;
case 'accordion':
classNames.push('accordion');
if (node.flush) classNames.push('accordion-flush');
break;
case 'breadcrumb':
classNames.push('breadcrumb');
break;
case 'carousel':
classNames.push('carousel', 'slide');
if (node.variant === 'dark') classNames.push('carousel-dark');
break;
case 'listgroup':
classNames.push('list-group');
if (node.variant === 'flush') classNames.push('list-group-flush');
if (node.numbered) classNames.push('list-group-numbered');
break;
case 'pagination':
classNames.push('pagination');
if (node.size) classNames.push(`pagination-${node.size}`);
break;
case 'toast':
classNames.push('toast');
break;
}
if (node.className) classNames.push(node.className);
if (node.align) classNames.push(`text-${node.align}`);
const classString = classNames.length > 0 ? ` class="${classNames.join(' ')}"` : '';
switch (node.type) {
case 'container':
return `${spaces}<div${classString}${styleString}>\n${generateBootstrapHTML(node.children, indent + 1)}\n${spaces}</div>`;
case 'row':
return `${spaces}<div${classString}${styleString}>\n${generateBootstrapHTML(node.children, indent + 1)}\n${spaces}</div>`;
case 'column':
return `${spaces}<div${classString}${styleString}>\n${generateBootstrapHTML(node.children, indent + 1)}\n${spaces}</div>`;
case 'card':
let cardContent = generateBootstrapHTML(node.children, indent + 1);
if (node.cardTitle || node.cardSubtitle) {
cardContent = `${spaces} <div class="card-header">\n` +
(node.cardTitle ? `${spaces} <h5 class="card-title">${node.cardTitle}</h5>\n` : '') +
(node.cardSubtitle ? `${spaces} <h6 class="card-subtitle">${node.cardSubtitle}</h6>\n` : '') +
`${spaces} </div>\n` +
`${spaces} <div class="card-body">\n${cardContent}\n${spaces} </div>`;
} else {
cardContent = `${spaces} <div class="card-body">\n${cardContent}\n${spaces} </div>`;
}
return `${spaces}<div${classString}${styleString}>\n${cardContent}\n${spaces}</div>`;
case 'button':
const disabledAttr = node.disabled ? ' disabled' : '';
return `${spaces}<button type="button"${classString}${styleString}${disabledAttr}>${node.text || 'Click Me'}</button>`;
case 'image':
const altAttr = node.alt ? ` alt="${node.alt}"` : '';
return `${spaces}<img src="${node.src || 'https://via.placeholder.com/150'}"${classString}${styleString}${altAttr} />`;
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
return `${spaces}<${node.type}${classString}${styleString}>${node.text || 'Sample Heading'}</${node.type}>`;
case 'p':
return `${spaces}<p${classString}${styleString}>${node.text || 'Sample Paragraph'}</p>`;
case 'alert':
const dismissButton = node.dismissible ? `\n${spaces} <button type="button" class="btn-close" data-bs-dismiss="alert"></button>` : '';
return `${spaces}<div${classString}${styleString} role="alert">\n${spaces} ${node.text || 'Alert message'}${dismissButton}\n${spaces}</div>`;
case 'badge':
return `${spaces}<span${classString}${styleString}>${node.text || 'Badge'}</span>`;
case 'progressbar':
const progressWidth = node.now ? ` style="width: ${node.now}%"` : ' style="width: 50%"';
return `${spaces}<div class="progress"${styleString}>\n${spaces} <div${classString}${progressWidth} role="progressbar"></div>\n${spaces}</div>`;
case 'spinner':
return `${spaces}<div${classString}${styleString} role="status">\n${spaces} <span class="visually-hidden">Loading...</span>\n${spaces}</div>`;
case 'form':
return `${spaces}<form${classString}${styleString}>\n${generateBootstrapHTML(node.children, indent + 1)}\n${spaces}</form>`;
case 'input':
const inputAttrs = [];
if (node.inputType) inputAttrs.push(`type="${node.inputType}"`);
if (node.placeholder) inputAttrs.push(`placeholder="${node.placeholder}"`);
if (node.disabled) inputAttrs.push('disabled');
const inputAttrsString = inputAttrs.length > 0 ? ` ${inputAttrs.join(' ')}` : '';
return `${spaces}<input${inputAttrsString}${classString}${styleString} />`;
case 'textarea':
const textareaAttrs = [];
if (node.placeholder) textareaAttrs.push(`placeholder="${node.placeholder}"`);
if (node.rows) textareaAttrs.push(`rows="${node.rows}"`);
const textareaAttrsString = textareaAttrs.length > 0 ? ` ${textareaAttrs.join(' ')}` : '';
return `${spaces}<textarea${textareaAttrsString}${classString}${styleString}></textarea>`;
case 'select':
const selectAttrs = [];
if (node.multiple) selectAttrs.push('multiple');
const selectAttrsString = selectAttrs.length > 0 ? ` ${selectAttrs.join(' ')}` : '';
const options = node.options || ['Option 1', 'Option 2', 'Option 3'];
const optionsCode = options.map(opt => `\n${spaces} <option>${opt}</option>`).join('');
return `${spaces}<select${selectAttrsString}${classString}${styleString}>${optionsCode}\n${spaces}</select>`;
case 'checkbox':
const checkboxAttrs = [];
if (node.checked) checkboxAttrs.push('checked');
if (node.disabled) checkboxAttrs.push('disabled');
const checkboxAttrsString = checkboxAttrs.length > 0 ? ` ${checkboxAttrs.join(' ')}` : '';
const checkboxClass = node.inline ? 'form-check form-check-inline' : 'form-check';
return `${spaces}<div class="${checkboxClass}">\n${spaces} <input class="form-check-input" type="checkbox"${checkboxAttrsString}>\n${spaces} <label class="form-check-label">${node.text || 'Checkbox'}</label>\n${spaces}</div>`;
case 'radio':
const radioAttrs = [];
if (node.checked) radioAttrs.push('checked');
if (node.disabled) radioAttrs.push('disabled');
const radioAttrsString = radioAttrs.length > 0 ? ` ${radioAttrs.join(' ')}` : '';
const radioClass = node.inline ? 'form-check form-check-inline' : 'form-check';
const nameAttr = node.name ? ` name="${node.name}"` : ' name="radio-group"';
return `${spaces}<div class="${radioClass}">\n${spaces} <input class="form-check-input" type="radio"${nameAttr}${radioAttrsString}>\n${spaces} <label class="form-check-label">${node.text || 'Radio'}</label>\n${spaces}</div>`;
case 'range':
const rangeAttrs = [];
if (node.min !== undefined) rangeAttrs.push(`min="${node.min}"`);
if (node.max !== undefined) rangeAttrs.push(`max="${node.max}"`);
if (node.value !== undefined) rangeAttrs.push(`value="${node.value}"`);
if (node.step !== undefined) rangeAttrs.push(`step="${node.step}"`);
if (node.disabled) rangeAttrs.push('disabled');
const rangeAttrsString = rangeAttrs.length > 0 ? ` ${rangeAttrs.join(' ')}` : '';
return `${spaces}<input type="range"${rangeAttrsString}${classString}${styleString} />`;
case 'dropdown':
const menuItems = node.menuItems || ['Action', 'Another action', 'Something else'];
const menuItemsCode = menuItems.map(item => `\n${spaces} <li><a class="dropdown-item" href="#">${item}</a></li>`).join('');
return `${spaces}<div${classString}${styleString}>\n${spaces} <button class="btn btn-${node.variant || 'primary'} dropdown-toggle" type="button" data-bs-toggle="dropdown">\n${spaces} ${node.text || 'Dropdown'}\n${spaces} </button>\n${spaces} <ul class="dropdown-menu">${menuItemsCode}\n${spaces} </ul>\n${spaces}</div>`;
case 'nav':
const navItems = node.navItems || ['Home', 'Features', 'Pricing'];
const navItemsCode = navItems.map(item => `\n${spaces} <li class="nav-item">\n${spaces} <a class="nav-link" href="#">${item}</a>\n${spaces} </li>`).join('');
return `${spaces}<ul${classString}${styleString}>${navItemsCode}\n${spaces}</ul>`;
case 'navbar':
return `${spaces}<nav${classString}${styleString}>\n${spaces} <div class="container-fluid">\n${spaces} <a class="navbar-brand" href="#">${node.brand || 'Navbar'}</a>\n${spaces} </div>\n${spaces}</nav>`;
case 'tabs':
const tabTitles = node.tabTitles || ['Tab 1', 'Tab 2', 'Tab 3'];
const tabsNav = tabTitles.map((title, index) =>
`\n${spaces} <li class="nav-item">\n${spaces} <a class="nav-link ${index === 0 ? 'active' : ''}" data-bs-toggle="tab" href="#tab${index}">${title}</a>\n${spaces} </li>`
).join('');
const tabsContent = tabTitles.map((title, index) =>
`\n${spaces} <div class="tab-pane fade ${index === 0 ? 'show active' : ''}" id="tab${index}">\n${spaces} <p>Content for ${title}</p>\n${spaces} </div>`
).join('');
return `${spaces}<ul${classString}${styleString}>${tabsNav}\n${spaces}</ul>\n${spaces}<div class="tab-content"${styleString}>${tabsContent}\n${spaces}</div>`;
case 'modal':
return `${spaces}<div${classString}${styleString} tabindex="-1">\n${spaces} <div class="modal-dialog">\n${spaces} <div class="modal-content">\n${spaces} <div class="modal-header">\n${spaces} <h5 class="modal-title">${node.modalTitle || 'Modal Title'}</h5>\n${spaces} <button type="button" class="btn-close" data-bs-dismiss="modal"></button>\n${spaces} </div>\n${spaces} <div class="modal-body">\n${spaces} <p>Modal body text goes here.</p>\n${spaces} </div>\n${spaces} <div class="modal-footer">\n${spaces} <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>\n${spaces} <button type="button" class="btn btn-primary">Save changes</button>\n${spaces} </div>\n${spaces} </div>\n${spaces} </div>\n${spaces}</div>`;
case 'accordion':
const accordionItems = node.items || 3;
const accordionCode = Array.from({ length: accordionItems }, (_, i) =>
`\n${spaces} <div class="accordion-item">\n${spaces} <h2 class="accordion-header">\n${spaces} <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapse${i}">\n${spaces} Accordion Item #${i + 1}\n${spaces} </button>\n${spaces} </h2>\n${spaces} <div id="collapse${i}" class="accordion-collapse collapse">\n${spaces} <div class="accordion-body">\n${spaces} Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n${spaces} </div>\n${spaces} </div>\n${spaces} </div>`
).join('');
return `${spaces}<div${classString}${styleString}>${accordionCode}\n${spaces}</div>`;
case 'breadcrumb':
const breadcrumbItems = node.breadcrumbItems || ['Home', 'Library', 'Data'];
const breadcrumbCode = breadcrumbItems.map((item, index) =>
`\n${spaces} <li class="breadcrumb-item${index === breadcrumbItems.length - 1 ? ' active' : ''}">${item}</li>`
).join('');
return `${spaces}<nav${styleString}${classNameString}>\n${spaces} <ol class="breadcrumb">${breadcrumbCode}\n${spaces} </ol>\n${spaces}</nav>`;
case 'carousel':
const slides = node.slides || 3;
const carouselItems = Array.from({ length: slides }, (_, i) =>
`\n${spaces} <div class="carousel-item${i === 0 ? ' active' : ''}">\n${spaces} <img src="https://via.placeholder.com/800x400?text=Slide+${i + 1}" class="d-block w-100" alt="Slide ${i + 1}">\n${spaces} <div class="carousel-caption d-none d-md-block">\n${spaces} <h5>Slide ${i + 1} Label</h5>\n${spaces} <p>Nulla vitae elit libero, a pharetra augue mollis interdum.</p>\n${spaces} </div>\n${spaces} </div>`
).join('');
const controls = node.controls !== false ? `\n${spaces} <button class="carousel-control-prev" type="button" data-bs-target="#carouselExample" data-bs-slide="prev">\n${spaces} <span class="carousel-control-prev-icon"></span>\n${spaces} </button>\n${spaces} <button class="carousel-control-next" type="button" data-bs-target="#carouselExample" data-bs-slide="next">\n${spaces} <span class="carousel-control-next-icon"></span>\n${spaces} </button>` : '';
const indicators = node.indicators !== false ? Array.from({ length: slides }, (_, i) =>
`\n${spaces} <button type="button" data-bs-target="#carouselExample" data-bs-slide-to="${i}"${i === 0 ? ' class="active"' : ''}></button>`
).join('') : '';
return `${spaces}<div id="carouselExample"${classString}${styleString} data-bs-ride="${node.autoPlay ? 'carousel' : 'false'}">${indicators ? `\n${spaces} <div class="carousel-indicators">${indicators}\n${spaces} </div>` : ''}${carouselItems}${controls}\n${spaces}</div>`;
case 'listgroup':
const items = node.items || ['Item 1', 'Item 2', 'Item 3'];
const itemsCode = items.map(item => `\n${spaces} <li class="list-group-item">${item}</li>`).join('');
return `${spaces}<ul${classString}${styleString}>${itemsCode}\n${spaces}</ul>`;
case 'pagination':
const pages = node.pages || 5;
const paginationItems = Array.from({ length: pages }, (_, i) =>
`\n${spaces} <li class="page-item"><a class="page-link" href="#">${i + 1}</a></li>`
).join('');
return `${spaces}<nav${styleString}>\n${spaces} <ul${classString}>${paginationItems}\n${spaces} </ul>\n${spaces}</nav>`;
case 'toast':
return `${spaces}<div${classString}${styleString}>\n${spaces} <div class="toast-header">\n${spaces} <strong class="me-auto">Notification</strong>\n${spaces} </div>\n${spaces} <div class="toast-body">\n${spaces} ${node.text || 'Toast message'}\n${spaces} </div>\n${spaces}</div>`;
default:
return `${spaces}<!-- Unknown component: ${node.type} -->`;
}
}).join('\n');
};
const generateHTML5 = (nodes, indent = 0) => {
const spaces = ' '.repeat(indent);
return nodes.map(node => {
const styleProps = [];
if (node.style) {
Object.entries(node.style).forEach(([key, value]) => {
if (value) styleProps.push(`${key.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${value};`);
});
}
const styleString = styleProps.length > 0 ? ` style="${styleProps.join(' ')}"` : '';
const classNames = [];
if (node.className) classNames.push(node.className);
const classString = classNames.length > 0 ? ` class="${classNames.join(' ')}"` : '';
switch (node.type) {
case 'container':
return `${spaces}<div${classString}${styleString}>\n${generateHTML5(node.children, indent + 1)}\n${spaces}</div>`;
case 'row':
return `${spaces}<div${classString}${styleString}>\n${generateHTML5(node.children, indent + 1)}\n${spaces}</div>`;
case 'column':
return `${spaces}<div${classString}${styleString}>\n${generateHTML5(node.children, indent + 1)}\n${spaces}</div>`;
case 'card':
let cardContent = generateHTML5(node.children, indent + 1);
if (node.cardTitle || node.cardSubtitle) {
cardContent = `${spaces} <div class="card-header">\n` +
(node.cardTitle ? `${spaces} <h3>${node.cardTitle}</h3>\n` : '') +
(node.cardSubtitle ? `${spaces} <h4>${node.cardSubtitle}</h4>\n` : '') +
`${spaces} </div>\n` +
`${spaces} <div class="card-body">\n${cardContent}\n${spaces} </div>`;
} else {
cardContent = `${spaces} <div class="card-body">\n${cardContent}\n${spaces} </div>`;
}
return `${spaces}<div class="card"${styleString}>\n${cardContent}\n${spaces}</div>`;
case 'button':
const disabledAttr = node.disabled ? ' disabled' : '';
let buttonClass = 'btn';
if (node.variant) buttonClass += ` btn-${node.variant}`;
return `${spaces}<button type="button" class="${buttonClass}"${styleString}${disabledAttr}>${node.text || 'Click Me'}</button>`;
case 'image':
const altAttr = node.alt ? ` alt="${node.alt}"` : '';
return `${spaces}<img src="${node.src || 'https://via.placeholder.com/150'}"${classString}${styleString}${altAttr} />`;
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
return `${spaces}<${node.type}${classString}${styleString}>${node.text || 'Sample Heading'}</${node.type}>`;
case 'p':
return `${spaces}<p${classString}${styleString}>${node.text || 'Sample Paragraph'}</p>`;
case 'alert':
let alertClass = 'alert';
if (node.variant) alertClass += ` alert-${node.variant}`;
return `${spaces}<div class="${alertClass}"${styleString} role="alert">\n${spaces} ${node.text || 'Alert message'}\n${spaces}</div>`;
case 'badge':
let badgeClass = 'badge';
if (node.variant) badgeClass += ` bg-${node.variant}`;
if (node.pill) badgeClass += ' rounded-pill';
return `${spaces}<span class="${badgeClass}"${styleString}>${node.text || 'Badge'}</span>`;
case 'progressbar':
const progressWidth = node.now ? ` style="width: ${node.now}%"` : ' style="width: 50%"';
return `${spaces}<div class="progress"${styleString}>\n${spaces} <div class="progress-bar"${progressWidth}></div>\n${spaces}</div>`;
case 'spinner':
return `${spaces}<div class="spinner"${styleString}></div>`;
case 'form':
return `${spaces}<form${classString}${styleString}>\n${generateHTML5(node.children, indent + 1)}\n${spaces}</form>`;
case 'input':
const inputAttrs = [];
if (node.inputType) inputAttrs.push(`type="${node.inputType}"`);
if (node.placeholder) inputAttrs.push(`placeholder="${node.placeholder}"`);
if (node.disabled) inputAttrs.push('disabled');
const inputAttrsString = inputAttrs.length > 0 ? ` ${inputAttrs.join(' ')}` : '';
return `${spaces}<input${inputAttrsString}${classString}${styleString} />`;
case 'textarea':
const textareaAttrs = [];
if (node.placeholder) textareaAttrs.push(`placeholder="${node.placeholder}"`);
if (node.rows) textareaAttrs.push(`rows="${node.rows}"`);
const textareaAttrsString = textareaAttrs.length > 0 ? ` ${textareaAttrs.join(' ')}` : '';
return `${spaces}<textarea${textareaAttrsString}${classString}${styleString}></textarea>`;
case 'select':
const options = node.options || ['Option 1', 'Option 2', 'Option 3'];
const optionsCode = options.map(opt => `\n${spaces} <option>${opt}</option>`).join('');
return `${spaces}<select${classString}${styleString}>${optionsCode}\n${spaces}</select>`;
case 'checkbox':
const checkboxAttrs = [];
if (node.checked) checkboxAttrs.push('checked');
if (node.disabled) checkboxAttrs.push('disabled');
const checkboxAttrsString = checkboxAttrs.length > 0 ? ` ${checkboxAttrs.join(' ')}` : '';
return `${spaces}<label>\n${spaces} <input type="checkbox"${checkboxAttrsString}> ${node.text || 'Checkbox'}\n${spaces}</label>`;
case 'radio':
const radioAttrs = [];
if (node.checked) radioAttrs.push('checked');
if (node.disabled) radioAttrs.push('disabled');
const radioAttrsString = radioAttrs.length > 0 ? ` ${radioAttrs.join(' ')}` : '';
const nameAttr = node.name ? ` name="${node.name}"` : ' name="radio-group"';
return `${spaces}<label>\n${spaces} <input type="radio"${nameAttr}${radioAttrsString}> ${node.text || 'Radio'}\n${spaces}</label>`;
case 'range':
const rangeAttrs = [];
if (node.min !== undefined) rangeAttrs.push(`min="${node.min}"`);
if (node.max !== undefined) rangeAttrs.push(`max="${node.max}"`);
if (node.value !== undefined) rangeAttrs.push(`value="${node.value}"`);
if (node.step !== undefined) rangeAttrs.push(`step="${node.step}"`);
if (node.disabled) rangeAttrs.push('disabled');
const rangeAttrsString = rangeAttrs.length > 0 ? ` ${rangeAttrs.join(' ')}` : '';
return `${spaces}<input type="range"${rangeAttrsString}${classString}${styleString} />`;
default:
return `${spaces}<!-- Unknown component: ${node.type} -->`;
}
}).join('\n');
};
const generateCompleteReactComponent = (tree) => {
const componentCode = `import React from 'react';
import {
Container, Row, Col, Card, Button, Image,
Alert, Badge, ProgressBar, Spinner, Form,
Dropdown, Nav, Navbar, Tabs, Tab, Modal,
Accordion, Breadcrumb, Carousel, ListGroup,
Pagination, Toast
} from 'react-bootstrap';
function GeneratedPage() {
return (
${generateReactCode(tree, 1)}
);
}
export default GeneratedPage;`;
return componentCode;
};
const generateCompleteBootstrapHTML = (tree) => {
const htmlCode = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Generated Page</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { padding: 20px; }
.card { margin-bottom: 1rem; }
</style>
</head>
<body>
${generateBootstrapHTML(tree, 1)}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>`;
return htmlCode;
};
const generateCompleteHTML5 = (tree) => {
const htmlCode = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Generated Page</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
line-height: 1.6;
}
.container-fluid { width: 100%; padding: 0 15px; }
.row { display: flex; flex-wrap: wrap; margin: 0 -15px; }
.col { flex: 1; padding: 0 15px; }
.card {
border: 1px solid #ddd;
border-radius: 0.375rem;
margin-bottom: 1rem;
background: white;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.card-header {
background-color: #f8f9fa;
padding: 1rem;
border-bottom: 1px solid #ddd;
font-weight: bold;
}
.card-body { padding: 1rem; }
.card-title { margin: 0 0 0.5rem 0; font-size: 1.25rem; }
.card-subtitle { margin: 0 0 0.5rem 0; color: #6c757d; }
.btn {
display: inline-block;
padding: 0.375rem 0.75rem;
border: 1px solid transparent;
border-radius: 0.375rem;
cursor: pointer;
margin: 0.25rem;
text-decoration: none;
font-weight: 400;
text-align: center;
vertical-align: middle;
}
.btn-primary { background-color: #0d6efd; color: white; border-color: #0d6efd; }
.btn-secondary { background-color: #6c757d; color: white; border-color: #6c757d; }
.btn-success { background-color: #198754; color: white; border-color: #198754; }
.btn-danger { background-color: #dc3545; color: white; border-color: #dc3545; }
.btn-warning { background-color: #ffc107; color: black; border-color: #ffc107; }
.btn-info { background-color: #0dcaf0; color: black; border-color: #0dcaf0; }
.btn-light { background-color: #f8f9fa; color: black; border-color: #f8f9fa; }
.btn-dark { background-color: #212529; color: white; border-color: #212529; }
.btn-link { background: none; color: #0d6efd; text-decoration: underline; border: none; }
.btn-outline-primary { background-color: transparent; color: #0d6efd; border-color: #0d6efd; }
.btn:disabled { opacity: 0.65; cursor: not-allowed; }
.img-fluid { max-width: 100%; height: auto; }
.alert {
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid transparent;
border-radius: 0.375rem;
}
.alert-primary { color: #084298; background-color: #cfe2ff; border-color: #b6d4fe; }
.badge {
display: inline-block;
padding: 0.35em 0.65em;
font-size: 0.75em;
font-weight: 700;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.375rem;
}
.bg-primary { background-color: #0d6efd; color: white; }
.progress {
display: flex;
height: 1rem;
overflow: hidden;
background-color: #e9ecef;
border-radius: 0.375rem;
margin-bottom: 1rem;
}
.progress-bar {
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
color: #fff;
text-align: center;
white-space: nowrap;
background-color: #0d6efd;
transition: width 0.6s ease;
}
.form-control {
display: block;
width: 100%;
padding: 0.375rem 0.75rem;
border: 1px solid #ced4da;
border-radius: 0.375rem;
margin-bottom: 1rem;
}
.text-left { text-align: left; }
.text-center { text-align: center; }
.text-right { text-align: right; }
</style>
</head>
<body>
${generateHTML5(tree, 1)}
</body>
</html>`;
return htmlCode;
};
// ───────────────────────────────
// Main Component
// ───────────────────────────────
export default function PageEditor() {
const createNode = useCallback((type) => ({
id: crypto.randomUUID(),
type,
text: NODE_CONFIG[type]?.defaultText || '',
src: type === 'image' ? 'https://via.placeholder.com/150' : undefined,
style: {},
children: [],
...(type === 'button' && { variant: 'primary' }),
...(type === 'alert' && { variant: 'primary' }),
...(type === 'badge' && { variant: 'primary' }),
...(type === 'progressbar' && { variant: 'primary', now: 50 }),
...(type === 'spinner' && { variant: 'primary', spinnerType: 'border' }),
...(type === 'nav' && { variant: 'tabs' }),
...(type === 'navbar' && { variant: 'light', expand: 'lg', brand: 'Navbar' }),
...(type === 'tabs' && { variant: 'tabs' }),
...(type === 'dropdown' && { variant: 'primary' }),
...(type === 'modal' && { modalTitle: 'Modal Title' }),
...(type === 'range' && { min: 0, max: 100, value: 50, step: 1 }),
}), []);
const [tree, setTree] = useState([createNode('container')]);
const [selectedId, setSelectedId] = useState(null);
const [showLeft, setShowLeft] = useState(true);
const [showRight, setShowRight] = useState(true);
const [activeTab, setActiveTab] = useState('properties');
const [showExportModal, setShowExportModal] = useState(false);
const [exportedCode, setExportedCode] = useState('');
const [exportType, setExportType] = useState('react');
// Memoized node finder
const findNodeById = useCallback((nodes, id) => {
for (const node of nodes) {
if (node.id === id) return node;
const found = findNodeById(node.children, id);
if (found) return found;
}
return null;
}, []);
const selectedNode = useMemo(() =>
selectedId ? findNodeById(tree, selectedId) : null
, [selectedId, tree, findNodeById]);
// Get root node ID
const rootNodeId = useMemo(() => tree[0]?.id, [tree]);
// Node operations
const updateNode = useCallback((nodes, id, updater) =>
nodes.map((n) =>
n.id === id ? updater(n) : { ...n, children: updateNode(n.children, id, updater) }
), []);
const addNode = useCallback((type) => {
if (!selectedId && !['container', 'row'].includes(type)) {
alert('Please select a container or row first.');
return;
}
const newNode = createNode(type);
if (!selectedId) {
setTree(prev => [...prev, newNode]);
} else {
setTree(prev => {
const addToParent = (nodes) =>
nodes.map((n) =>
n.id === selectedId && NODE_CONFIG[n.type]?.canHaveChildren
? { ...n, children: [...n.children, newNode] }
: { ...n, children: addToParent(n.children) }
);
return addToParent(prev);
});
}
setSelectedId(newNode.id);
}, [selectedId, createNode]);
const deleteNode = useCallback((id) => {
if (id === rootNodeId) {
alert('Cannot delete the root container');
return;
}
setTree(prev => {
const removeNode = (nodes) =>
nodes
.map((n) =>
n.id === id
? null
: { ...n, children: removeNode(n.children).filter(Boolean) }
)
.filter(Boolean);
return removeNode(prev);
});
if (selectedId === id) {
setSelectedId(null);
}
}, [selectedId, rootNodeId]);
const moveNode = useCallback((dragId, hoverId) => {
if (dragId === hoverId) return;
const extractNode = (nodes, id) => {
for (const node of nodes) {
if (node.id === id) return node;
const found = extractNode(node.children, id);
if (found) return found;
}
};
const dragged = extractNode(tree, dragId);
if (!dragged) return;
const removeNode = (nodes, id) =>
nodes
.map((n) =>
n.id === id
? null
: { ...n, children: removeNode(n.children, id).filter(Boolean) }
)
.filter(Boolean);
const cleaned = removeNode(tree, dragId);
const insertNode = (nodes, targetId, item) =>
nodes.map((n) =>
n.id === targetId && NODE_CONFIG[n.type]?.canHaveChildren
? { ...n, children: [...n.children, item] }
: { ...n, children: insertNode(n.children, targetId, item) }
);
setTree(insertNode(cleaned, hoverId, dragged));
}, [tree]);
// Event handlers
const handleStyleChange = useCallback((key, value) => {
setTree(prev => updateNode(prev, selectedId, (n) => ({
...n,
style: { ...n.style, [key]: value }
})));
}, [selectedId, updateNode]);
const handlePropertyChange = useCallback((key, value) => {
setTree(prev => updateNode(prev, selectedId, (n) => ({
...n,
[key]: value
})));
}, [selectedId, updateNode]);
const handleTextChange = useCallback((text) => {
setTree(prev => updateNode(prev, selectedId, (n) => ({ ...n, text })));
}, [selectedId, updateNode]);
const handleImageChange = useCallback((src) => {
setTree(prev => updateNode(prev, selectedId, (n) => ({ ...n, src })));
}, [selectedId, updateNode]);
// Render specific property panel based on node type
const renderPropertyPanel = () => {
if (!selectedNode) return null;
return (
<>
{/* Common Properties */}
<CommonProperties
selectedNode={selectedNode}
onStyleChange={handleStyleChange}
onPropertyChange={handlePropertyChange}
/>
{/* Type-specific Properties */}
{selectedNode.type === 'button' && (
<ButtonProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'image' && (
<ImageProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
onImageChange={handleImageChange}
/>
)}
{selectedNode.type === 'alert' && (
<AlertProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'badge' && (
<BadgeProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'progressbar' && (
<ProgressBarProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'spinner' && (
<SpinnerProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'form' && (
<FormProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'input' && (
<InputProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'textarea' && (
<TextareaProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'select' && (
<SelectProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'checkbox' && (
<CheckboxProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'radio' && (
<RadioProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'range' && (
<RangeProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'dropdown' && (
<DropdownProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'nav' && (
<NavProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'navbar' && (
<NavbarProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'tabs' && (
<TabsProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'modal' && (
<ModalProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'accordion' && (
<AccordionProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'breadcrumb' && (
<BreadcrumbProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'carousel' && (
<CarouselProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'listgroup' && (
<ListGroupProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'pagination' && (
<PaginationProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'toast' && (
<ToastProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'column' && (
<ColumnProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{selectedNode.type === 'card' && (
<CardProperties
selectedNode={selectedNode}
onPropertyChange={handlePropertyChange}
/>
)}
{/* Text Content for text elements */}
{['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'button', 'alert', 'badge', 'checkbox', 'radio'].includes(selectedNode.type) && (
<Form.Group className="mb-3">
<Form.Label>Text Content</Form.Label>
<Form.Control
type="text"
value={selectedNode.text}
onChange={(e) => handleTextChange(e.target.value)}
/>
</Form.Group>
)}
</>
);
};
// Export functions
const exportAsJSON = useCallback(() => {
const data = JSON.stringify(tree, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'page-structure.json';
a.click();
URL.revokeObjectURL(url);
}, [tree]);
const handleExport = useCallback((type) => {
let code = '';
let filename = '';
switch (type) {
case 'react':
code = generateCompleteReactComponent(tree);
filename = 'GeneratedPage.jsx';
break;
case 'bootstrap':
code = generateCompleteBootstrapHTML(tree);
filename = 'bootstrap-page.html';
break;
case 'html5':
code = generateCompleteHTML5(tree);
filename = 'html5-page.html';
break;
default:
return;
}
setExportType(type);
setExportedCode(code);
setShowExportModal(true);
}, [tree]);
const copyToClipboard = useCallback(() => {
navigator.clipboard.writeText(exportedCode).then(() => {
alert('Code copied to clipboard!');
});
}, [exportedCode]);
const downloadAsFile = useCallback(() => {
let filename = '';
switch (exportType) {
case 'react':
filename = 'GeneratedPage.jsx';
break;
case 'bootstrap':
case 'html5':
filename = `${exportType}-page.html`;
break;
default:
filename = 'exported-code.txt';
}
const blob = new Blob([exportedCode], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}, [exportedCode, exportType]);
const importTree = useCallback((event) => {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const importedTree = JSON.parse(e.target.result);
setTree(importedTree);
setSelectedId(null);
} catch (error) {
alert('Invalid JSON file');
}
};
reader.readAsText(file);
}, []);
// Tree renderer
const renderTree = useCallback((nodes) =>
nodes.map((node) => {
const isSelected = node.id === selectedId;
const commonProps = {
selected: isSelected,
onClick: (e) => {
e.stopPropagation();
setSelectedId(node.id);
setShowRight(true);
},
style: node.style,
nodeType: node.type,
};
const className = [];
if (node.align) className.push(`text-${node.align}`);
switch (node.type) {
case 'container':
return (
<DevContainer key={node.id} {...commonProps}>
{renderTree(node.children)}
</DevContainer>
);
case 'row':
return (
<DevRow key={node.id} {...commonProps}>
{renderTree(node.children)}
</DevRow>
);
case 'column':
const colSize = node.colSizeType && node.colSpan
? `${node.colSizeType === 'xs' ? '' : node.colSizeType + '-'}${node.colSpan}`
: null;
return (
<DevCol key={node.id} {...commonProps} colSize={colSize}>
{renderTree(node.children)}
</DevCol>
);
case 'card':
const cardProps = {};
if (node.cardBorder) cardProps.border = node.cardBorder;
return (
<Card key={node.id} {...commonProps} {...cardProps} style={{ ...node.style, width: '100%' }}>
{(node.cardTitle || node.cardSubtitle) && (
<Card.Header>
{node.cardTitle && <Card.Title>{node.cardTitle}</Card.Title>}
{node.cardSubtitle && <Card.Subtitle>{node.cardSubtitle}</Card.Subtitle>}
</Card.Header>
)}
<Card.Body>{renderTree(node.children)}</Card.Body>
</Card>
);
case 'button':
const buttonProps = {
variant: node.variant || 'primary',
size: node.size,
disabled: node.disabled || false,
};
return (
<Button key={node.id} {...commonProps} {...buttonProps}>
{node.text}
</Button>
);
case 'image':
const imageProps = {
src: node.src,
alt: node.alt,
fluid: node.imageStyle === 'fluid',
thumbnail: node.imageStyle === 'thumbnail',
rounded: node.imageStyle === 'rounded',
roundedCircle: node.imageStyle === 'rounded-circle',
};
return (
<Image
key={node.id}
{...commonProps}
{...imageProps}
style={{
maxWidth: '100%',
height: 'auto',
...node.style
}}
/>
);
case 'alert':
const alertProps = {
variant: node.variant || 'primary',
dismissible: node.dismissible,
};
return (
<Alert key={node.id} {...commonProps} {...alertProps}>
{node.text}
</Alert>
);
case 'badge':
const badgeProps = {
bg: node.variant || 'primary',
pill: node.pill,
};
return (
<Badge key={node.id} {...commonProps} {...badgeProps}>
{node.text}
</Badge>
);
case 'progressbar':
const progressProps = {
variant: node.variant || 'primary',
now: node.now || 50,
animated: node.animated,
striped: node.striped,
};
return (
<ProgressBar key={node.id} {...commonProps} {...progressProps} />
);
case 'spinner':
const spinnerProps = {
variant: node.variant || 'primary',
animation: node.spinnerType || 'border',
size: node.size,
};
return (
<Spinner key={node.id} {...commonProps} {...spinnerProps} />
);
case 'form':
const formProps = {
validated: node.validated,
};
return (
<Form key={node.id} {...commonProps} {...formProps}>
{renderTree(node.children)}
</Form>
);
case 'input':
const inputProps = {
type: node.inputType || 'text',
placeholder: node.placeholder,
size: node.size,
disabled: node.disabled,
};
return (
<Form.Control key={node.id} {...commonProps} {...inputProps} />
);
case 'textarea':
const textareaProps = {
as: 'textarea',
placeholder: node.placeholder,
rows: node.rows || 3,
};
return (
<Form.Control key={node.id} {...commonProps} {...textareaProps} />
);
case 'select':
const options = node.options || ['Option 1', 'Option 2', 'Option 3'];
return (
<Form.Select key={node.id} {...commonProps} multiple={node.multiple}>
{options.map((option, index) => (
<option key={index}>{option}</option>
))}
</Form.Select>
);
case 'checkbox':
const checkboxProps = {
type: 'checkbox',
label: node.text || 'Checkbox',
checked: node.checked,
disabled: node.disabled,
inline: node.inline,
};
return (
<Form.Check key={node.id} {...commonProps} {...checkboxProps} />
);
case 'radio':
const radioProps = {
type: 'radio',
label: node.text || 'Radio',
checked: node.checked,
disabled: node.disabled,
inline: node.inline,
name: node.name || 'radio-group',
};
return (
<Form.Check key={node.id} {...commonProps} {...radioProps} />
);
case 'range':
const rangeProps = {
min: node.min || 0,
max: node.max || 100,
value: node.value || 50,
step: node.step || 1,
disabled: node.disabled,
};
return (
<Form.Range key={node.id} {...commonProps} {...rangeProps} />
);
case 'dropdown':
const menuItems = node.menuItems || ['Action', 'Another action', 'Something else'];
return (
<Dropdown key={node.id} {...commonProps}>
<Dropdown.Toggle variant={node.variant || 'primary'} size={node.size}>
{node.text || 'Dropdown'}
</Dropdown.Toggle>
<Dropdown.Menu>
{menuItems.map((item, index) => (
<Dropdown.Item key={index}>{item}</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
);
case 'nav':
const navItems = node.navItems || ['Home', 'Features', 'Pricing'];
return (
<Nav key={node.id} {...commonProps} variant={node.variant} fill={node.fill} justify={node.justified}>
{navItems.map((item, index) => (
<Nav.Item key={index}>
<Nav.Link>{item}</Nav.Link>
</Nav.Item>
))}
</Nav>
);
case 'navbar':
const navbarProps = {
variant: node.variant,
bg: node.bg,
expand: node.expand,
fixed: node.fixed,
};
return (
<Navbar key={node.id} {...commonProps} {...navbarProps}>
<Container>
<Navbar.Brand>{node.brand || 'Navbar'}</Navbar.Brand>
</Container>
</Navbar>
);
case 'tabs':
const tabTitles = node.tabTitles || ['Tab 1', 'Tab 2', 'Tab 3'];
return (
<Tabs key={node.id} {...commonProps} defaultActiveKey="tab0" variant={node.variant} justify={node.justify} fill={node.fill}>
{tabTitles.map((title, index) => (
<Tab key={index} eventKey={`tab${index}`} title={title}>
<p>Content for {title}</p>
</Tab>
))}
</Tabs>
);
case 'modal':
const modalProps = {
size: node.size,
backdrop: node.backdrop,
keyboard: node.keyboard,
scrollable: node.scrollable,
centered: node.centered,
};
return (
<Modal key={node.id} {...commonProps} {...modalProps} show={false}>
<Modal.Header closeButton>
<Modal.Title>{node.modalTitle || 'Modal Title'}</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>Modal body text goes here.</p>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary">Close</Button>
<Button variant="primary">Save changes</Button>
</Modal.Footer>
</Modal>
);
case 'accordion':
const accordionItems = node.items || 3;
return (
<Accordion key={node.id} {...commonProps} flush={node.flush} alwaysOpen={node.alwaysOpen}>
{Array.from({ length: accordionItems }, (_, i) => (
<Accordion.Item key={i} eventKey={i.toString()}>
<Accordion.Header>Accordion Item #{i + 1}</Accordion.Header>
<Accordion.Body>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</Accordion.Body>
</Accordion.Item>
))}
</Accordion>
);
case 'breadcrumb':
const breadcrumbItems = node.breadcrumbItems || ['Home', 'Library', 'Data'];
return (
<Breadcrumb key={node.id} {...commonProps}>
{breadcrumbItems.map((item, index) => (
<Breadcrumb.Item key={index} active={index === breadcrumbItems.length - 1}>
{item}
</Breadcrumb.Item>
))}
</Breadcrumb>
);
case 'carousel':
const slides = node.slides || 3;
return (
<Carousel key={node.id} {...commonProps} variant={node.variant} controls={node.controls} indicators={node.indicators} interval={node.autoPlay ? node.interval : null}>
{Array.from({ length: slides }, (_, i) => (
<Carousel.Item key={i}>
<Image className="d-block w-100" src={`https://via.placeholder.com/800x400?text=Slide+${i + 1}`} alt={`Slide ${i + 1}`} />
<Carousel.Caption>
<h3>Slide {i + 1} Label</h3>
<p>Nulla vitae elit libero, a pharetra augue mollis interdum.</p>
</Carousel.Caption>
</Carousel.Item>
))}
</Carousel>
);
case 'listgroup':
const items = node.items || ['Item 1', 'Item 2', 'Item 3'];
return (
<ListGroup key={node.id} {...commonProps} variant={node.variant} numbered={node.numbered}>
{items.map((item, index) => (
<ListGroup.Item key={index}>{item}</ListGroup.Item>
))}
</ListGroup>
);
case 'pagination':
const pages = node.pages || 5;
return (
<Pagination key={node.id} {...commonProps} size={node.size}>
{Array.from({ length: pages }, (_, i) => (
<Pagination.Item key={i + 1}>{i + 1}</Pagination.Item>
))}
</Pagination>
);
case 'toast':
const toastProps = {
show: node.show !== false,
autohide: node.autohide,
delay: node.delay,
};
return (
<Toast key={node.id} {...commonProps} {...toastProps}>
<Toast.Header>
<strong className="me-auto">Notification</strong>
</Toast.Header>
<Toast.Body>{node.text || 'Toast message'}</Toast.Body>
</Toast>
);
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
return (
<EditableText
key={node.id}
tag={node.type}
text={node.text}
onChange={handleTextChange}
style={node.style}
className={className.join(' ')}
onClick={() => setSelectedId(node.id)}
/>
);
case 'p':
return (
<EditableText
key={node.id}
tag="p"
text={node.text}
onChange={handleTextChange}
style={node.style}
className={className.join(' ')}
onClick={() => setSelectedId(node.id)}
/>
);
default:
return (
<div key={node.id} {...commonProps}>
Unknown component: {node.type}
</div>
);
}
}),
[selectedId, handleTextChange]
);
// Component categories for the component palette
const componentCategories = {
'Layout': ['container', 'row', 'column'],
'Content': ['card', 'button', 'image'],
'Typography': ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'],
'Form': ['form', 'input', 'textarea', 'select', 'checkbox', 'radio', 'range'],
'Components': ['alert', 'badge', 'breadcrumb', 'progressbar', 'spinner'],
'Navigation': ['nav', 'navbar', 'tabs', 'pagination'],
'Data Display': ['listgroup', 'table'],
'Interactive': ['dropdown', 'accordion', 'carousel', 'modal', 'toast']
};
return (
<DndProvider backend={HTML5Backend}>
<div className="d-flex flex-column vh-100">
{/* Top Navigation */}
<Navbar bg="dark" variant="dark" expand="lg" className="px-3">
<Navbar.Brand>Simple Page Builder</Navbar.Brand>
<Nav className="me-auto">
<Nav.Link onClick={exportAsJSON}>Export JSON</Nav.Link>
<Nav.Link onClick={() => handleExport('react')}>Export React</Nav.Link>
<Nav.Link onClick={() => handleExport('bootstrap')}>Export Bootstrap</Nav.Link>
<Nav.Link onClick={() => handleExport('html5')}>Export HTML5</Nav.Link>
</Nav>
<div className="d-flex">
<b style={{
color:"white",
textAlign:"center",
marginTop:"10px",
marginRight:"10px"
}}>
Import JSON Layout
</b>
<Form.Control
type="file"
accept=".json"
onChange={importTree}
style={{ width: 'auto' }}
className="me-2"
/>
<ButtonGroup>
<Button
variant="outline-light"
size="sm"
onClick={() => setShowLeft(!showLeft)}
>
{showLeft ? 'Hide' : 'Show'} Components
</Button>
<Button
variant="outline-light"
size="sm"
onClick={() => setShowRight(!showRight)}
>
{showRight ? 'Hide' : 'Show'} Properties
</Button>
</ButtonGroup>
</div>
</Navbar>
{/* Main Content Area */}
<div className="d-flex flex-grow-1 overflow-hidden">
{/* Left Sidebar - Component Palette */}
{showLeft && (
<Offcanvas show={showLeft} onHide={() => setShowLeft(false)} placement="start" style={{ width: '300px' }}>
<Offcanvas.Header closeButton>
<Offcanvas.Title>Components</Offcanvas.Title>
</Offcanvas.Header>
<Offcanvas.Body className="p-0">
<Accordion defaultActiveKey={['0']} alwaysOpen>
{Object.entries(componentCategories).map(([category, components], index) => (
<Accordion.Item key={category} eventKey={index.toString()}>
<Accordion.Header>{category}</Accordion.Header>
<Accordion.Body>
<Row className="g-2">
{components.map((type) => (
<Col key={type} xs={6}>
<Button
variant="outline-primary"
size="sm"
className="w-100 text-start"
onClick={() => addNode(type)}
style={{ fontSize: '0.8rem' }}
>
{NODE_CONFIG[type]?.label || type}
</Button>
</Col>
))}
</Row>
</Accordion.Body>
</Accordion.Item>
))}
</Accordion>
</Offcanvas.Body>
</Offcanvas>
)}
{/* Center - Canvas */}
<div className="flex-grow-1 d-flex flex-column position-relative">
<div className="p-3 border-bottom bg-light">
<div className="d-flex justify-content-between align-items-center">
<h5 className="mb-0">Canvas</h5>
<div>
<Badge bg="secondary" className="me-2">
Selected: {selectedNode ? NODE_CONFIG[selectedNode.type]?.label : 'None'}
</Badge>
<Button
variant="outline-danger"
size="sm"
disabled={!selectedId || selectedId === rootNodeId}
onClick={() => deleteNode(selectedId)}
>
Delete Selected
</Button>
</div>
</div>
</div>
<div
className="flex-grow-1 p-3 overflow-auto bg-light"
style={{ minHeight: '400px' }}
onClick={() => setSelectedId(null)}
>
{renderTree(tree)}
</div>
</div>
{/* Right Sidebar - Properties & Tree View */}
{showRight && (
<Offcanvas show={showRight} onHide={() => setShowRight(false)} placement="end" style={{ width: '350px' }}>
<Offcanvas.Header closeButton>
<Offcanvas.Title>Properties & Tree</Offcanvas.Title>
</Offcanvas.Header>
<Offcanvas.Body className="p-0">
<Tabs
activeKey={activeTab}
onSelect={setActiveTab}
className="mb-3"
fill
>
<Tab eventKey="properties" title="Properties">
<div className="p-3">
{selectedNode ? (
renderPropertyPanel()
) : (
<Alert variant="info">
Select a component to edit its properties
</Alert>
)}
</div>
</Tab>
<Tab eventKey="tree" title="Tree View">
<div className="p-3">
<TreeView
nodes={tree}
moveNode={moveNode}
selectNode={setSelectedId}
selectedId={selectedId}
onDelete={deleteNode}
/>
</div>
</Tab>
</Tabs>
</Offcanvas.Body>
</Offcanvas>
)}
</div>
</div>
{/* Export Modal */}
<Modal show={showExportModal} onHide={() => setShowExportModal(false)} size="lg">
<Modal.Header closeButton>
<Modal.Title>Export Code</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form.Group>
<Form.Label>Generated Code ({exportType.toUpperCase()})</Form.Label>
<Form.Control
as="textarea"
rows={15}
value={exportedCode}
readOnly
style={{ fontFamily: 'monospace', fontSize: '0.8rem' }}
/>
</Form.Group>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={copyToClipboard}>
Copy to Clipboard
</Button>
<Button variant="primary" onClick={downloadAsFile}>
Download File
</Button>
<Button variant="outline-secondary" onClick={() => setShowExportModal(false)}>
Close
</Button>
</Modal.Footer>
</Modal>
</DndProvider>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment