Last active
November 2, 2025 13:26
-
-
Save aneury1/b1d6092b5068cad0c9f943afd6e12b25 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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