\n
🎨 Brand Design System
\n \n {/* Brand Colors */}\n
\n
Brand Colors
\n
\n {brandColors.map((color) => (\n
applyBrandColor(color)}\n title={color.name}\n />\n ))}\n
\n
\n \n {/* Typography */}\n
\n
Typography Scale
\n
\n {typographyScale.map((scale) => (\n
\n {scale.name}\n
\n ))}\n
\n
\n \n {/* Actions */}\n
\n Export Theme\n \n
\n);\n\nconst customPanelConfig = {\n pageConfigPanelTabsContent: {\n layers: { title: \"Layers\", content: defaultConfigTabsContent().layers.content },\n appearance: { title: \"Brand\", content:
},\n data: { title: \"Data\", content: defaultConfigTabsContent().data?.content }\n }\n};\n```\n\n### Custom Data Panel\n\nIntegrate with your data sources:\n\n```tsx\nconst CustomDataPanel = () => {\n const [dataSources, setDataSources] = useState([]);\n const [isConnecting, setIsConnecting] = useState(false);\n \n return (\n
\n
\n \n Data Sources\n
\n \n {/* Connected Sources */}\n
\n {dataSources.map((source) => (\n
\n
\n
\n {source.connected \n ? `Connected • ${source.recordCount} records`\n : 'Disconnected'\n }\n
\n
\n ))}\n
\n \n {/* Add New Source */}\n
\n {isConnecting ? 'Connecting...' : 'Add Data Source'}\n \n
\n );\n};\n```\n\n### Adding New Tabs\n\nExtend the interface with custom functionality:\n\n```tsx\nconst CustomSettingsPanel = () => (\n
\n
\n \n Project Settings\n
\n \n
\n
\n Environment \n \n Development \n Staging \n Production \n \n
\n \n
\n Framework \n \n
\n \n
\n Deploy Target \n \n
\n
\n
\n);\n\nconst extendedPanelConfig = {\n pageConfigPanelTabsContent: {\n layers: { title: \"Layers\", content: defaultConfigTabsContent().layers.content },\n appearance: { title: \"Theme\", content:
},\n data: { title: \"Data\", content:
},\n settings: { title: \"Settings\", content:
}\n }\n};\n```"
+ },
+ {
+ "id": "custom-navigation-bar",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "children": "## Custom Navigation Bar\n\nReplace the default navigation bar with your own custom implementation using the `navBar` prop:\n\n### Simple Custom Navigation\n\nCreate a minimal navigation bar with essential functionality:\n\n```tsx\nimport { useState } from 'react';\nimport { Home, Code, Eye } from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport { useLayerStore } from '@/lib/ui-builder/store/layer-store';\nimport { useEditorStore } from '@/lib/ui-builder/store/editor-store';\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\n\nconst SimpleNav = () => {\n const [showCodeDialog, setShowCodeDialog] = useState(false);\n const [showPreviewDialog, setShowPreviewDialog] = useState(false);\n \n const selectedPageId = useLayerStore((state) => state.selectedPageId);\n const findLayerById = useLayerStore((state) => state.findLayerById);\n const componentRegistry = useEditorStore((state) => state.registry);\n \n const page = findLayerById(selectedPageId);\n\n return (\n <>\n
\n
\n \n UI Builder \n
\n
\n setShowCodeDialog(true)}\n >\n \n Code\n \n setShowPreviewDialog(true)}\n >\n \n Preview\n \n
\n
\n\n {/* Simple Code Dialog */}\n {showCodeDialog && (\n
\n
\n
\n
Generated Code \n setShowCodeDialog(false)}>\n ×\n \n \n
\n
{`// Code export functionality would go here`} \n
\n
\n
\n )}\n\n {/* Simple Preview Dialog */}\n {showPreviewDialog && (\n
\n
\n
\n
Page Preview \n setShowPreviewDialog(false)}>\n ×\n \n \n
\n {page && (\n \n )}\n
\n
\n
\n )}\n >\n );\n};\n\n// Use the custom navigation\nconst customNavConfig = {\n navBar:
,\n pageConfigPanelTabsContent: {\n layers: { title: \"Layers\", content: defaultConfigTabsContent().layers.content },\n appearance: { title: \"Appearance\", content: defaultConfigTabsContent().appearance?.content },\n data: { title: \"Data\", content: defaultConfigTabsContent().data?.content }\n }\n};\n\n
\n```\n\n### Advanced Custom Navigation\n\nBuild a more sophisticated navigation with additional features:\n\n```tsx\nconst AdvancedNav = () => {\n const { pages, selectedPageId, selectPage, addPageLayer } = useLayerStore();\n const { previewMode, setPreviewMode } = useEditorStore();\n const [isPublishing, setIsPublishing] = useState(false);\n \n const selectedPage = pages.find(page => page.id === selectedPageId);\n\n const handlePublish = async () => {\n setIsPublishing(true);\n try {\n // Your publish logic here\n await publishPage(selectedPage);\n } finally {\n setIsPublishing(false);\n }\n };\n\n return (\n \n {/* Left Section - Branding & Page Selector */}\n
\n
\n
\n UB \n
\n
UI Builder Pro \n
\n \n
\n \n
selectPage(e.target.value)}\n className=\"border border-gray-300 rounded px-3 py-1 text-sm\"\n >\n {pages.map(page => (\n \n {page.name}\n \n ))}\n \n \n
addPageLayer('New Page')}\n >\n \n New Page\n \n
\n\n {/* Center Section - Preview Mode Toggle */}\n
\n Preview: \n setPreviewMode(e.target.value)}\n className=\"border border-gray-300 rounded px-2 py-1 text-sm\"\n >\n Desktop \n Tablet \n Mobile \n Responsive \n \n
\n\n {/* Right Section - Actions */}\n
\n \n \n Save Draft\n \n \n \n \n Preview\n \n \n \n {isPublishing ? (\n \n ) : (\n \n )}\n {isPublishing ? 'Publishing...' : 'Publish'}\n \n
\n
\n );\n};\n```\n"
+ },
+ {
+ "id": "panel-config-api",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "children": "## PanelConfig API Reference\n\n### Type Definition\n\n```tsx\ninterface PanelConfig {\n pageConfigPanelTabsContent?: TabsContentConfig;\n navBar?: React.ReactNode;\n editorPanel?: React.ReactNode;\n propsPanel?: React.ReactNode;\n}\n\ninterface TabsContentConfig {\n layers: { title: string; content: React.ReactNode };\n appearance?: { title: string; content: React.ReactNode };\n data?: { title: string; content: React.ReactNode };\n [key: string]: { title: string; content: React.ReactNode } | undefined;\n}\n```\n\n### Configuration Options\n\n#### `pageConfigPanelTabsContent`\nCustomizes the left panel tabs:\n\n- **Required**: `layers` - The component tree and page management tab\n- **Optional**: `appearance` - Theme and styling controls tab \n- **Optional**: `data` - Variable management tab\n- **Custom tabs**: Add your own tabs with any string key\n\n```tsx\n// Example with all options\nconst fullPanelConfig = {\n pageConfigPanelTabsContent: {\n layers: { title: \"Structure\", content: },\n appearance: { title: \"Design\", content: },\n data: { title: \"Variables\", content: },\n settings: { title: \"Settings\", content: },\n integrations: { title: \"APIs\", content: }\n }\n};\n```\n\n#### `navBar`\nCustomize the top navigation bar with your own React component:\n\n```tsx\nconst customNavBar = (\n \n
\n \n
My Custom Editor \n \n
\n Preview \n Publish \n
\n
\n);\n\nconst panelConfig = {\n navBar: customNavBar\n};\n```\n\n"
+ },
+ {
+ "id": "use-cases",
"type": "Markdown",
"name": "Markdown",
"props": {},
- "children": "## Available Panels\n\nUI Builder consists of four main panels that can be customized:\n\n### Core Panels\n- **Navigation Bar** - Top toolbar with editor controls\n- **Page Config Panel** - Left panel with Layers, Appearance, and Data tabs\n- **Editor Panel** - Center canvas for visual editing\n- **Props Panel** - Right panel for component property editing\n\n## Basic Panel Configuration\n\nUse the `panelConfig` prop to customize any panel:\n\n```tsx\nimport UIBuilder from '@/components/ui/ui-builder';\nimport { NavBar } from '@/components/ui/ui-builder/internal/components/nav';\nimport LayersPanel from '@/components/ui/ui-builder/internal/layers-panel';\nimport EditorPanel from '@/components/ui/ui-builder/internal/editor-panel';\nimport PropsPanel from '@/components/ui/ui-builder/internal/props-panel';\n\n ,\n \n // Custom editor panel\n editorPanel: ,\n \n // Custom props panel \n propsPanel: ,\n \n // Custom page config panel\n pageConfigPanel: \n }}\n/>\n```\n\n## Page Config Panel Tabs\n\nThe most common customization is modifying the left panel tabs:\n\n```tsx\nimport { VariablesPanel } from '@/components/ui/ui-builder/internal/variables-panel';\nimport { TailwindThemePanel } from '@/components/ui/ui-builder/internal/tailwind-theme-panel';\nimport { ConfigPanel } from '@/components/ui/ui-builder/internal/config-panel';\n\nconst customTabsContent = {\n // Required: Layers tab\n layers: { \n title: \"Structure\", \n content: \n },\n \n // Optional: Appearance tab\n appearance: { \n title: \"Styling\", \n content: (\n \n \n \n \n
\n )\n },\n \n // Optional: Data tab\n data: { \n title: \"Variables\", \n content: \n },\n \n // Add completely custom tabs\n assets: {\n title: \"Assets\",\n content: \n }\n};\n\n \n```\n\n## Default Panel Configuration\n\nThis is the default panel setup (equivalent to not providing `panelConfig`):\n\n```tsx\nimport { \n defaultConfigTabsContent, \n getDefaultPanelConfigValues \n} from '@/components/ui/ui-builder';\n\n// Default tabs content\nconst defaultTabs = defaultConfigTabsContent();\n// Returns:\n// {\n// layers: { title: \"Layers\", content: },\n// appearance: { title: \"Appearance\", content: },\n// data: { title: \"Data\", content: }\n// }\n\n// Default panel values\nconst defaultPanels = getDefaultPanelConfigValues(defaultTabs);\n// Returns:\n// {\n// navBar: ,\n// pageConfigPanel: ,\n// editorPanel: ,\n// propsPanel: \n// }\n```\n\n## Custom Navigation Bar\n\nReplace the default navigation with your own:\n\n```tsx\nconst MyCustomNavBar = () => {\n const showLeftPanel = useEditorStore(state => state.showLeftPanel);\n const toggleLeftPanel = useEditorStore(state => state.toggleLeftPanel);\n const showRightPanel = useEditorStore(state => state.showRightPanel);\n const toggleRightPanel = useEditorStore(state => state.toggleRightPanel);\n \n return (\n \n {/* Left side */}\n \n
\n
Page Builder \n
\n \n {/* Center actions */}\n \n \n {showLeftPanel ? 'Hide' : 'Show'} Panels\n \n \n Save\n \n \n Preview\n \n
\n \n {/* Right side */}\n \n \n \n
\n \n );\n};\n\n \n }}\n/>\n```\n\n## Custom Editor Panel\n\nReplace the canvas area with custom functionality:\n\n```tsx\nconst MyCustomEditor = ({ className }) => {\n const selectedPageId = useLayerStore(state => state.selectedPageId);\n const findLayerById = useLayerStore(state => state.findLayerById);\n const currentPage = findLayerById(selectedPageId);\n \n return (\n \n {/* Custom toolbar */}\n
\n \n \n \n \n \n \n \n \n \n \n \n \n 50% \n 100% \n 150% \n \n \n
\n \n {/* Custom canvas */}\n
\n
\n {currentPage && (\n \n )}\n
\n
\n
\n );\n};\n\n \n }}\n/>\n```\n\n## Responsive Panel Behavior\n\nUI Builder automatically handles responsive layouts:\n\n### Desktop Layout\n- **Three panels** side by side using ResizablePanelGroup\n- **Resizable handles** between panels\n- **Collapsible panels** via editor store state\n\n### Mobile Layout\n- **Single panel view** with bottom navigation\n- **Panel switcher** at the bottom\n- **Full-screen panels** for better mobile experience\n\n### Panel Visibility Control\n\n```tsx\n// Control panel visibility programmatically\nconst MyPanelController = () => {\n const { \n showLeftPanel, \n showRightPanel,\n toggleLeftPanel, \n toggleRightPanel \n } = useEditorStore();\n \n return (\n \n \n Left Panel\n \n \n Right Panel\n \n
\n );\n};\n```\n\n## Integration with Editor State\n\nPanels integrate with the editor state management:\n\n```tsx\n// Access editor state in custom panels\nconst MyCustomPanel = () => {\n const componentRegistry = useEditorStore(state => state.registry);\n const allowPagesCreation = useEditorStore(state => state.allowPagesCreation);\n const allowVariableEditing = useEditorStore(state => state.allowVariableEditing);\n \n const selectedLayerId = useLayerStore(state => state.selectedLayerId);\n const pages = useLayerStore(state => state.pages);\n const variables = useLayerStore(state => state.variables);\n \n return (\n \n
Custom Panel \n
Registry has {Object.keys(componentRegistry).length} components
\n
Current page: {pages.find(p => p.id === selectedLayerId)?.name}
\n
Variables: {variables.length}
\n
\n );\n};\n```\n\n## Best Practices\n\n### Panel Design\n- **Follow existing patterns** for consistency\n- **Use proper overflow handling** (`overflow-y-auto`) for scrollable content\n- **Include proper padding/spacing** (`px-4 py-2`)\n- **Respect theme variables** for colors and spacing\n\n### State Management\n- **Use editor and layer stores** for state access\n- **Don't duplicate state** - use the existing stores\n- **Subscribe to specific slices** to avoid unnecessary re-renders\n- **Use proper cleanup** in useEffect hooks\n\n### Performance\n- **Memoize expensive components** with React.memo\n- **Use virtualization** for large lists\n- **Debounce rapid updates** when needed\n- **Minimize re-renders** by careful state subscription\n\n### Accessibility\n- **Provide proper ARIA labels** for custom controls\n- **Ensure keyboard navigation** works correctly\n- **Use semantic HTML** where possible\n- **Test with screen readers** for complex interactions"
+ "children": "## Common Use Cases\n\n### White-Label Applications\n\nCustomize the interface to match your brand:\n\n```tsx\nconst brandedPanelConfig = {\n pageConfigPanelTabsContent: {\n layers: { \n title: \"Components\", \n content: defaultConfigTabsContent().layers.content \n },\n appearance: { \n title: \"Brand Kit\", \n content: \n },\n data: { \n title: \"Content\", \n content: \n }\n }\n};\n```\n\n### Specialized Workflows\n\nTailor the interface for specific use cases:\n\n```tsx\n// Email template builder\nconst emailBuilderConfig = {\n pageConfigPanelTabsContent: {\n layers: { title: \"Blocks\", content: defaultConfigTabsContent().layers.content },\n appearance: { title: \"Styling\", content: },\n data: { title: \"Merge Tags\", content: },\n preview: { title: \"Preview\", content: }\n }\n};\n\n// Landing page builder\nconst landingPageConfig = {\n pageConfigPanelTabsContent: {\n layers: { title: \"Sections\", content: defaultConfigTabsContent().layers.content },\n appearance: { title: \"Theme\", content: },\n data: { title: \"A/B Tests\", content: },\n analytics: { title: \"Analytics\", content: }\n }\n};\n```\n\n### Simplified Interfaces\n\nHide complexity for non-technical users:\n\n```tsx\n// Content editor - no technical options\nconst contentEditorConfig = {\n pageConfigPanelTabsContent: {\n layers: { title: \"Content\", content: defaultConfigTabsContent().layers.content }\n // Hide appearance and data tabs\n }\n};\n\n// Designer interface - focus on visual\nconst designerConfig = {\n pageConfigPanelTabsContent: {\n layers: { title: \"Layers\", content: defaultConfigTabsContent().layers.content },\n appearance: { title: \"Design\", content: }\n // Hide data/variables tab\n }\n};\n```\n\n### Integration with External Tools\n\nConnect with your existing systems:\n\n```tsx\nconst integratedConfig = {\n pageConfigPanelTabsContent: {\n layers: { title: \"Structure\", content: defaultConfigTabsContent().layers.content },\n appearance: { title: \"Styling\", content: defaultConfigTabsContent().appearance?.content },\n data: { title: \"Data\", content: defaultConfigTabsContent().data?.content },\n cms: { title: \"CMS\", content: },\n assets: { title: \"Assets\", content: }\n }\n};\n```"
}
]
} as const satisfies ComponentLayer;
\ No newline at end of file
diff --git a/app/docs/docs-data/docs-page-layers/persistence.ts b/app/docs/docs-data/docs-page-layers/persistence.ts
index 0c849ac..7fee5a7 100644
--- a/app/docs/docs-data/docs-page-layers/persistence.ts
+++ b/app/docs/docs-data/docs-page-layers/persistence.ts
@@ -23,14 +23,77 @@ export const PERSISTENCE_LAYER = {
"type": "Markdown",
"name": "Markdown",
"props": {},
- "children": "Learn how UI Builder manages state and provides flexible persistence options for your layouts and variables. Save to databases, manage auto-save behavior, and handle state changes with simple, powerful APIs."
+ "children": "UI Builder provides flexible state management and persistence options for your layouts and variables. Choose between automatic local storage, custom database integration, or complete manual control based on your application's needs."
},
{
- "id": "persistence-content",
+ "id": "persistence-state-overview",
"type": "Markdown",
"name": "Markdown",
"props": {},
- "children": "## Understanding UI Builder State\n\nUI Builder manages two main types of state:\n- **Layers**: The component hierarchy and structure\n- **Variables**: Dynamic data that can be bound to component properties\n\n## Local Storage Persistence\n\nBy default, UI Builder automatically saves state to browser local storage:\n\n```tsx\n// Default behavior - auto-saves to localStorage\n \n\n// Disable local storage persistence\n\n```\n\n## Database Integration\n\nFor production applications, you'll want to save state to your database:\n\n```tsx\nimport { useState, useEffect } from 'react';\nimport UIBuilder from '@/components/ui/ui-builder';\nimport { ComponentLayer, Variable } from '@/components/ui/ui-builder/types';\n\nfunction DatabaseIntegratedBuilder({ userId }: { userId: string }) {\n const [initialLayers, setInitialLayers] = useState();\n const [initialVariables, setInitialVariables] = useState();\n const [isLoading, setIsLoading] = useState(true);\n\n // Load initial state from database\n useEffect(() => {\n async function loadUserLayout() {\n try {\n const response = await fetch(`/api/layouts/${userId}`);\n const data = await response.json();\n \n setInitialLayers(data.layers || []);\n setInitialVariables(data.variables || []);\n } catch (error) {\n console.error('Failed to load layout:', error);\n // Fallback to empty state\n setInitialLayers([]);\n setInitialVariables([]);\n } finally {\n setIsLoading(false);\n }\n }\n\n loadUserLayout();\n }, [userId]);\n\n // Save layers to database\n const handleLayersChange = async (updatedLayers: ComponentLayer[]) => {\n try {\n await fetch(`/api/layouts/${userId}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers: updatedLayers })\n });\n } catch (error) {\n console.error('Failed to save layers:', error);\n }\n };\n\n // Save variables to database\n const handleVariablesChange = async (updatedVariables: Variable[]) => {\n try {\n await fetch(`/api/variables/${userId}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ variables: updatedVariables })\n });\n } catch (error) {\n console.error('Failed to save variables:', error);\n }\n };\n\n if (isLoading) {\n return Loading your layout...
;\n }\n\n return (\n \n );\n}\n```\n\n## Debounced Auto-Save\n\nTo avoid excessive API calls, implement debounced saving:\n\n```tsx\nimport { useCallback } from 'react';\nimport { debounce } from 'lodash';\n\nfunction AutoSaveBuilder() {\n // Debounced save function - waits 2 seconds after last change\n const debouncedSave = useCallback(\n debounce(async (layers: ComponentLayer[]) => {\n try {\n await fetch('/api/layouts/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers })\n });\n console.log('Auto-saved successfully');\n } catch (error) {\n console.error('Auto-save failed:', error);\n }\n }, 2000),\n []\n );\n\n const debouncedSaveVariables = useCallback(\n debounce(async (variables: Variable[]) => {\n try {\n await fetch('/api/variables/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ variables })\n });\n console.log('Variables auto-saved successfully');\n } catch (error) {\n console.error('Variables auto-save failed:', error);\n }\n }, 2000),\n []\n );\n\n return (\n \n );\n}\n```\n\n## Manual Save with UI Feedback\n\nProvide users with explicit save controls:\n\n```tsx\nimport { useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Badge } from '@/components/ui/badge';\n\nfunction ManualSaveBuilder() {\n const [currentLayers, setCurrentLayers] = useState([]);\n const [currentVariables, setCurrentVariables] = useState([]);\n const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);\n const [isSaving, setIsSaving] = useState(false);\n\n const handleLayersChange = (layers: ComponentLayer[]) => {\n setCurrentLayers(layers);\n setHasUnsavedChanges(true);\n };\n\n const handleVariablesChange = (variables: Variable[]) => {\n setCurrentVariables(variables);\n setHasUnsavedChanges(true);\n };\n\n const handleSave = async () => {\n setIsSaving(true);\n try {\n await Promise.all([\n fetch('/api/layouts/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers: currentLayers })\n }),\n fetch('/api/variables/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ variables: currentVariables })\n })\n ]);\n \n setHasUnsavedChanges(false);\n alert('Saved successfully!');\n } catch (error) {\n console.error('Save failed:', error);\n alert('Save failed. Please try again.');\n } finally {\n setIsSaving(false);\n }\n };\n\n return (\n \n {/* Save Controls */}\n
\n \n {isSaving ? 'Saving...' : 'Save'}\n \n \n {hasUnsavedChanges && (\n Unsaved Changes \n )}\n
\n \n {/* Builder */}\n
\n \n
\n
\n );\n}\n```\n\n## Data Format\n\nUI Builder saves data in a simple, readable JSON format:\n\n```json\n{\n \"layers\": [\n {\n \"id\": \"page-1\",\n \"type\": \"div\",\n \"name\": \"Page 1\",\n \"props\": {\n \"className\": \"p-4 bg-white\"\n },\n \"children\": [\n {\n \"id\": \"button-1\",\n \"type\": \"Button\",\n \"name\": \"Submit Button\",\n \"props\": {\n \"variant\": \"default\",\n \"children\": { \"__variableRef\": \"buttonText\" }\n },\n \"children\": []\n }\n ]\n }\n ],\n \"variables\": [\n {\n \"id\": \"buttonText\",\n \"name\": \"Button Text\",\n \"type\": \"string\",\n \"defaultValue\": \"Click Me!\"\n }\n ]\n}\n```\n\n## API Route Examples\n\nHere are example API routes for Next.js:\n\n### Save Layout API Route\n\n```tsx\n// app/api/layouts/[userId]/route.ts\nimport { NextRequest, NextResponse } from 'next/server';\n\nexport async function PUT(\n request: NextRequest,\n { params }: { params: { userId: string } }\n) {\n try {\n const { layers } = await request.json();\n const { userId } = params;\n \n // Save to your database\n await saveUserLayout(userId, layers);\n \n return NextResponse.json({ success: true });\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to save layout' },\n { status: 500 }\n );\n }\n}\n\nexport async function GET(\n request: NextRequest,\n { params }: { params: { userId: string } }\n) {\n try {\n const { userId } = params;\n const layout = await getUserLayout(userId);\n \n return NextResponse.json(layout);\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to load layout' },\n { status: 500 }\n );\n }\n}\n```\n\n### Save Variables API Route\n\n```tsx\n// app/api/variables/[userId]/route.ts\nimport { NextRequest, NextResponse } from 'next/server';\n\nexport async function PUT(\n request: NextRequest,\n { params }: { params: { userId: string } }\n) {\n try {\n const { variables } = await request.json();\n const { userId } = params;\n \n await saveUserVariables(userId, variables);\n \n return NextResponse.json({ success: true });\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to save variables' },\n { status: 500 }\n );\n }\n}\n```\n\n## Error Handling & Recovery\n\nImplement robust error handling for persistence:\n\n```tsx\nfunction RobustBuilder() {\n const [lastSavedState, setLastSavedState] = useState(null);\n const [saveError, setSaveError] = useState(null);\n\n const handleSaveWithRetry = async (layers: ComponentLayer[], retries = 3) => {\n for (let i = 0; i < retries; i++) {\n try {\n await fetch('/api/layouts/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers })\n });\n \n setLastSavedState(layers);\n setSaveError(null);\n return;\n } catch (error) {\n if (i === retries - 1) {\n setSaveError('Failed to save after multiple attempts');\n console.error('Save failed after retries:', error);\n } else {\n // Wait before retry\n await new Promise(resolve => setTimeout(resolve, 1000));\n }\n }\n }\n };\n\n const handleRecovery = () => {\n if (lastSavedState) {\n // Restore to last saved state\n window.location.reload();\n }\n };\n\n return (\n \n {saveError && (\n
\n Save Error: {saveError}\n \n Recover Last Saved\n \n
\n )}\n \n
\n
\n );\n}\n```\n\n## Best Practices\n\n1. **Always handle save errors gracefully** - Show user feedback and provide recovery options\n2. **Use debouncing for auto-save** - Avoid overwhelming your API with requests\n3. **Validate data before saving** - Ensure the data structure is correct\n4. **Provide manual save controls** - Give users explicit control over when data is saved\n5. **Consider offline support** - Store changes locally when the network is unavailable\n6. **Implement proper loading states** - Show users when data is being loaded or saved\n7. **Use proper error boundaries** - Prevent save errors from crashing the entire editor"
+ "children": "## Understanding UI Builder State\n\nUI Builder manages two main types of state:\n- **Pages & Layers**: The component hierarchy, structure, and configuration\n- **Variables**: Dynamic data definitions that can be bound to component properties\n\nBoth are managed independently and can be persisted using different strategies."
+ },
+ {
+ "id": "persistence-local-storage",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "children": "## Local Storage Persistence\n\nBy default, UI Builder automatically saves state to browser local storage:\n\n```tsx\n// Default behavior - auto-saves to localStorage\n \n\n// Disable local storage persistence\n\n```\n\n**When to use:** Development, prototyping, or single-user applications where browser storage is sufficient.\n\n**Limitations:** Data is tied to the browser/device and can be cleared by the user."
+ },
+ {
+ "id": "persistence-database-integration",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "children": "## Database Integration\n\nFor production applications, integrate with your database using the initialization and callback props:\n\n```tsx\nimport UIBuilder from '@/components/ui/ui-builder';\nimport type { ComponentLayer, Variable, LayerChangeHandler, VariableChangeHandler } from '@/components/ui/ui-builder/types';\n\nfunction DatabaseIntegratedBuilder({ userId }: { userId: string }) {\n const [initialLayers, setInitialLayers] = useState();\n const [initialVariables, setInitialVariables] = useState();\n const [isLoading, setIsLoading] = useState(true);\n\n // Load initial state from database\n useEffect(() => {\n async function loadUserLayout() {\n try {\n const [layoutRes, variablesRes] = await Promise.all([\n fetch(`/api/layouts/${userId}`),\n fetch(`/api/variables/${userId}`)\n ]);\n \n const layoutData = await layoutRes.json();\n const variablesData = await variablesRes.json();\n \n setInitialLayers(layoutData.layers || []);\n setInitialVariables(variablesData.variables || []);\n } catch (error) {\n console.error('Failed to load layout:', error);\n setInitialLayers([]);\n setInitialVariables([]);\n } finally {\n setIsLoading(false);\n }\n }\n\n loadUserLayout();\n }, [userId]);\n\n // Save layers to database\n const handleLayersChange: LayerChangeHandler = async (updatedLayers) => {\n try {\n await fetch(`/api/layouts/${userId}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers: updatedLayers })\n });\n } catch (error) {\n console.error('Failed to save layers:', error);\n }\n };\n\n // Save variables to database\n const handleVariablesChange: VariableChangeHandler = async (updatedVariables) => {\n try {\n await fetch(`/api/variables/${userId}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ variables: updatedVariables })\n });\n } catch (error) {\n console.error('Failed to save variables:', error);\n }\n };\n\n if (isLoading) {\n return Loading your layout...
;\n }\n\n return (\n \n );\n}\n```"
+ },
+ {
+ "id": "persistence-props-reference",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "children": "## Persistence-Related Props\n\n### Core Persistence Props\n\n- **`persistLayerStore`** (`boolean`, default: `true`): Controls localStorage persistence\n- **`initialLayers`** (`ComponentLayer[]`): Initial pages/layers to load\n- **`onChange`** (`LayerChangeHandler`): Callback when layers change\n- **`initialVariables`** (`Variable[]`): Initial variables to load\n- **`onVariablesChange`** (`VariableChangeHandler`): Callback when variables change\n\n### Permission Control Props\n\nControl what users can modify to prevent unwanted changes:\n\n- **`allowVariableEditing`** (`boolean`, default: `true`): Allow variable creation/editing\n- **`allowPagesCreation`** (`boolean`, default: `true`): Allow new page creation\n- **`allowPagesDeletion`** (`boolean`, default: `true`): Allow page deletion\n\n```tsx\n// Read-only editor for content review\n\n```"
+ },
+ {
+ "id": "persistence-working-examples",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "children": "## Working Examples\n\nExplore these working examples in the project:\n\n- **[Basic Example](/examples/basic)**: Simple setup with localStorage persistence\n- **[Editor Example](/examples/editor)**: Full editor with custom configuration\n- **[Immutable Pages](/examples/editor/immutable-pages)**: Read-only pages with `allowPagesCreation={false}` and `allowPagesDeletion={false}`\n\nThe examples demonstrate different persistence patterns you can adapt for your use case."
+ },
+ {
+ "id": "persistence-debounced-save",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "children": "## Debounced Auto-Save\n\nAvoid excessive API calls with debounced saving:\n\n```tsx\nimport { useCallback } from 'react';\nimport { debounce } from 'lodash';\n\nfunction AutoSaveBuilder() {\n // Debounce saves to reduce API calls\n const debouncedSaveLayers = useCallback(\n debounce(async (layers: ComponentLayer[]) => {\n try {\n await fetch('/api/layouts/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers })\n });\n } catch (error) {\n console.error('Auto-save failed:', error);\n }\n }, 2000), // 2 second delay\n []\n );\n\n const debouncedSaveVariables = useCallback(\n debounce(async (variables: Variable[]) => {\n try {\n await fetch('/api/variables/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ variables })\n });\n } catch (error) {\n console.error('Variables auto-save failed:', error);\n }\n }, 2000),\n []\n );\n\n return (\n \n );\n}\n```"
+ },
+ {
+ "id": "persistence-data-format",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "children": "## Data Format\n\nUI Builder saves data as plain JSON with a predictable structure:\n\n```json\n{\n \"layers\": [\n {\n \"id\": \"page-1\",\n \"type\": \"div\",\n \"name\": \"Page 1\",\n \"props\": {\n \"className\": \"p-4 bg-white\"\n },\n \"children\": [\n {\n \"id\": \"button-1\",\n \"type\": \"Button\",\n \"name\": \"Submit Button\",\n \"props\": {\n \"variant\": \"default\",\n \"children\": {\n \"__variableRef\": \"buttonText\"\n }\n },\n \"children\": []\n }\n ]\n }\n ],\n \"variables\": [\n {\n \"id\": \"buttonText\",\n \"name\": \"Button Text\",\n \"type\": \"string\",\n \"defaultValue\": \"Click Me!\"\n }\n ]\n}\n```\n\n**Variable References**: Component properties bound to variables use `{ \"__variableRef\": \"variableId\" }` format."
+ },
+ {
+ "id": "persistence-api-routes",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "children": "## Next.js API Route Examples\n\n### Layout API Route\n\n```tsx\n// app/api/layouts/[userId]/route.ts\nimport { NextRequest, NextResponse } from 'next/server';\n\nexport async function GET(\n request: NextRequest,\n { params }: { params: { userId: string } }\n) {\n try {\n const layout = await getUserLayout(params.userId);\n return NextResponse.json({ layers: layout });\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to load layout' },\n { status: 500 }\n );\n }\n}\n\nexport async function PUT(\n request: NextRequest,\n { params }: { params: { userId: string } }\n) {\n try {\n const { layers } = await request.json();\n await saveUserLayout(params.userId, layers);\n return NextResponse.json({ success: true });\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to save layout' },\n { status: 500 }\n );\n }\n}\n```\n\n### Variables API Route \n\n```tsx\n// app/api/variables/[userId]/route.ts\nexport async function PUT(\n request: NextRequest,\n { params }: { params: { userId: string } }\n) {\n try {\n const { variables } = await request.json();\n await saveUserVariables(params.userId, variables);\n return NextResponse.json({ success: true });\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to save variables' },\n { status: 500 }\n );\n }\n}\n```"
+ },
+ {
+ "id": "persistence-error-handling",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "children": "## Error Handling & Recovery\n\nImplement robust error handling for production applications:\n\n```tsx\nfunction RobustBuilder() {\n const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'error' | 'success'>('idle');\n const [lastError, setLastError] = useState(null);\n\n const handleSaveWithRetry = async (layers: ComponentLayer[], retries = 3) => {\n setSaveStatus('saving');\n \n for (let i = 0; i < retries; i++) {\n try {\n await fetch('/api/layouts/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers })\n });\n \n setSaveStatus('success');\n setLastError(null);\n return;\n } catch (error) {\n if (i === retries - 1) {\n setSaveStatus('error');\n setLastError('Failed to save after multiple attempts');\n } else {\n // Wait before retry with exponential backoff\n await new Promise(resolve => \n setTimeout(resolve, Math.pow(2, i) * 1000)\n );\n }\n }\n }\n };\n\n return (\n \n {/* Status Bar */}\n
\n
\n {saveStatus === 'saving' && (\n Saving... \n )}\n {saveStatus === 'success' && (\n Saved \n )}\n {saveStatus === 'error' && (\n Save Failed \n )}\n
\n \n {lastError && (\n
window.location.reload()}\n >\n Reload Page\n \n )}\n
\n \n {/* Builder */}\n
\n \n
\n
\n );\n}\n```"
+ },
+ {
+ "id": "persistence-best-practices",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "children": "## Best Practices\n\n### 1. **Choose the Right Persistence Strategy**\n- **localStorage**: Development, prototyping, single-user apps\n- **Database**: Production, multi-user, collaborative editing\n- **Hybrid**: localStorage for drafts, database for published versions\n\n### 2. **Implement Proper Error Handling**\n- Always handle save failures gracefully\n- Provide user feedback for save status\n- Implement retry logic with exponential backoff\n- Offer recovery options when saves fail\n\n### 3. **Optimize API Performance**\n- Use debouncing to reduce API calls (2-5 second delays)\n- Consider batching layer and variable saves\n- Implement optimistic updates for better UX\n- Use proper HTTP status codes and error responses\n\n### 4. **Control User Permissions**\n- Use `allowVariableEditing={false}` for content-only editing\n- Set `allowPagesCreation={false}` for fixed page structures \n- Implement role-based access control in your API routes\n\n### 5. **Plan for Scale**\n- Consider data size limits (layers can grow large)\n- Implement pagination for large datasets\n- Use database indexing on user IDs and timestamps\n- Consider caching frequently accessed layouts"
}
]
} as const satisfies ComponentLayer;
\ No newline at end of file
diff --git a/app/docs/docs-data/docs-page-layers/props-panel-customization.ts b/app/docs/docs-data/docs-page-layers/props-panel-customization.ts
deleted file mode 100644
index 546d689..0000000
--- a/app/docs/docs-data/docs-page-layers/props-panel-customization.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { ComponentLayer } from "@/components/ui/ui-builder/types";
-
-export const PROPS_PANEL_CUSTOMIZATION_LAYER = {
- "id": "props-panel-customization",
- "type": "div",
- "name": "Props Panel Customization",
- "props": {
- "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen",
- "data-group": "editor-features"
- },
- "children": [
- {
- "type": "span",
- "children": "Props Panel Customization",
- "id": "props-panel-customization-title",
- "name": "Text",
- "props": {
- "className": "text-4xl"
- }
- },
- {
- "id": "props-panel-customization-intro",
- "type": "Markdown",
- "name": "Markdown",
- "props": {},
- "children": "Customize the properties panel to create intuitive, context-aware editing experiences. Design custom field types through AutoForm field overrides and create specialized property editors for your components."
- },
- {
- "id": "props-panel-customization-demo",
- "type": "div",
- "name": "div",
- "props": {},
- "children": [
- {
- "id": "props-panel-customization-badge",
- "type": "Badge",
- "name": "Badge",
- "props": {
- "variant": "default",
- "className": "rounded rounded-b-none"
- },
- "children": [
- {
- "id": "props-panel-customization-badge-text",
- "type": "span",
- "name": "span",
- "props": {},
- "children": "Custom Property Forms"
- }
- ]
- },
- {
- "id": "props-panel-customization-demo-frame",
- "type": "div",
- "name": "div",
- "props": {
- "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden"
- },
- "children": [
- {
- "id": "props-panel-customization-iframe",
- "type": "iframe",
- "name": "iframe",
- "props": {
- "src": "http://localhost:3000/examples/editor",
- "title": "Props Panel Customization Demo",
- "className": "aspect-square md:aspect-video w-full"
- },
- "children": []
- }
- ]
- }
- ]
- },
- {
- "id": "props-panel-customization-content",
- "type": "Markdown",
- "name": "Markdown",
- "props": {},
- "children": "## Field Overrides System\n\nUI Builder uses AutoForm to generate property forms from Zod schemas. You can customize individual fields using the `fieldOverrides` property in your component registry:\n\n```tsx\nimport { z } from 'zod';\nimport { classNameFieldOverrides, childrenAsTextareaFieldOverrides } from '@/lib/ui-builder/registry/form-field-overrides';\n\nconst MyCard = {\n component: Card,\n schema: z.object({\n title: z.string().default('Card Title'),\n description: z.string().optional(),\n imageUrl: z.string().optional(),\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: '@/components/ui/card',\n fieldOverrides: {\n // Use built-in className editor with Tailwind autocomplete\n className: (layer) => classNameFieldOverrides(layer),\n \n // Use textarea for description\n description: {\n fieldType: 'textarea',\n inputProps: {\n placeholder: 'Enter card description...',\n rows: 3\n }\n },\n \n // Custom image picker\n imageUrl: {\n fieldType: 'input',\n inputProps: {\n type: 'url',\n placeholder: 'https://example.com/image.jpg'\n },\n description: 'URL of the image to display'\n },\n \n // Use textarea for children content\n children: (layer) => childrenAsTextareaFieldOverrides(layer)\n }\n};\n```\n\n## Built-in Field Overrides\n\nUI Builder provides several pre-built field overrides for common use cases:\n\n### Common Field Overrides\n\n```tsx\nimport { \n commonFieldOverrides,\n classNameFieldOverrides,\n childrenFieldOverrides,\n childrenAsTextareaFieldOverrides \n} from '@/lib/ui-builder/registry/form-field-overrides';\n\n// Apply both className and children overrides\nfieldOverrides: commonFieldOverrides()\n\n// Or use individual overrides\nfieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer),\n children: (layer) => childrenFieldOverrides(layer)\n}\n```\n\n### className Field Override\n\nProvides intelligent Tailwind CSS class suggestions:\n\n```tsx\nfieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer)\n}\n\n// Features:\n// - Autocomplete suggestions\n// - Tailwind class validation\n// - Responsive class support\n// - Common pattern suggestions\n```\n\n### children Field Override\n\nSmart handling for component children:\n\n```tsx\nfieldOverrides: {\n // Standard children editor\n children: (layer) => childrenFieldOverrides(layer),\n \n // Or force textarea for text content\n children: (layer) => childrenAsTextareaFieldOverrides(layer)\n}\n\n// Features:\n// - String content as textarea\n// - Component children as visual editor\n// - Variable binding support\n```\n\n## AutoForm Field Configuration\n\nCustomize how AutoForm renders your fields:\n\n### Basic Field Types\n\n```tsx\nfieldOverrides: {\n // Text input with placeholder\n title: {\n inputProps: {\n placeholder: 'Enter title...',\n maxLength: 100\n }\n },\n \n // Number input with constraints\n count: {\n inputProps: {\n min: 0,\n max: 999,\n step: 1\n }\n },\n \n // Textarea with custom rows\n description: {\n fieldType: 'textarea',\n inputProps: {\n rows: 4,\n placeholder: 'Describe your component...'\n }\n },\n \n // Color input\n backgroundColor: {\n fieldType: 'input',\n inputProps: {\n type: 'color'\n }\n },\n \n // URL input with validation\n link: {\n fieldType: 'input',\n inputProps: {\n type: 'url',\n placeholder: 'https://example.com'\n }\n }\n}\n```\n\n### Advanced Field Configuration\n\n```tsx\nfieldOverrides: {\n // Custom field with description and label\n apiEndpoint: {\n inputProps: {\n placeholder: '/api/data',\n pattern: '^/api/.*'\n },\n description: 'Relative API endpoint path',\n label: 'API Endpoint'\n },\n \n // Hidden field (for internal use)\n internalId: {\n isHidden: true\n },\n \n // Field with custom validation message\n email: {\n inputProps: {\n type: 'email',\n placeholder: 'user@example.com'\n },\n description: 'Valid email address required'\n }\n}\n```\n\n## Custom Field Components\n\nCreate completely custom field editors:\n\n```tsx\n// Custom spacing control component\nconst SpacingControl = ({ value, onChange }) => {\n const [spacing, setSpacing] = useState(value || { top: 0, right: 0, bottom: 0, left: 0 });\n \n const updateSpacing = (side, newValue) => {\n const updated = { ...spacing, [side]: newValue };\n setSpacing(updated);\n onChange(updated);\n };\n \n return (\n \n
\n
updateSpacing('top', parseInt(e.target.value))}\n className=\"text-center p-1 border rounded\"\n placeholder=\"T\"\n />\n
\n
updateSpacing('left', parseInt(e.target.value))}\n className=\"text-center p-1 border rounded\"\n placeholder=\"L\"\n />\n
\n
updateSpacing('right', parseInt(e.target.value))}\n className=\"text-center p-1 border rounded\"\n placeholder=\"R\"\n />\n
\n
updateSpacing('bottom', parseInt(e.target.value))}\n className=\"text-center p-1 border rounded\"\n placeholder=\"B\"\n />\n
\n );\n};\n\n// Use custom component in field override\nfieldOverrides: {\n spacing: {\n renderParent: ({ children, ...props }) => (\n \n )\n }\n}\n```\n\n## Component-Specific Customizations\n\nTailor field overrides to specific component types:\n\n### Button Component\n\n```tsx\nconst ButtonComponent = {\n component: Button,\n schema: z.object({\n variant: z.enum(['default', 'destructive', 'outline', 'secondary', 'ghost', 'link']).default('default'),\n size: z.enum(['default', 'sm', 'lg', 'icon']).default('default'),\n disabled: z.boolean().optional(),\n children: z.any().optional(),\n onClick: z.string().optional(),\n className: z.string().optional()\n }),\n from: '@/components/ui/button',\n fieldOverrides: {\n // Use common overrides for basic props\n ...commonFieldOverrides(),\n \n // Custom click handler editor\n onClick: {\n inputProps: {\n placeholder: 'console.log(\"Button clicked\")',\n family: 'monospace'\n },\n description: 'JavaScript code to execute on click',\n label: 'Click Handler'\n },\n \n // Enhanced variant selector with descriptions\n variant: {\n description: 'Visual style of the button'\n }\n }\n};\n```\n\n### Image Component\n\n```tsx\nconst ImageComponent = {\n component: 'img',\n schema: z.object({\n src: z.string().url(),\n alt: z.string(),\n width: z.coerce.number().optional(),\n height: z.coerce.number().optional(),\n className: z.string().optional()\n }),\n fieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer),\n \n // Image URL with preview\n src: {\n inputProps: {\n type: 'url',\n placeholder: 'https://example.com/image.jpg'\n },\n description: 'URL of the image to display',\n // Note: Custom preview would require a custom render component\n },\n \n // Alt text with guidance\n alt: {\n inputProps: {\n placeholder: 'Describe the image for accessibility'\n },\n description: 'Alternative text for screen readers'\n },\n \n // Dimensions with constraints\n width: {\n inputProps: {\n min: 1,\n max: 2000,\n step: 1\n }\n },\n \n height: {\n inputProps: {\n min: 1,\n max: 2000,\n step: 1\n }\n }\n }\n};\n```\n\n## Variable Binding Integration\n\nField overrides work seamlessly with variable binding:\n\n```tsx\n// Component with variable-bindable properties\nconst UserCard = {\n component: UserCard,\n schema: z.object({\n name: z.string().default(''),\n email: z.string().email().optional(),\n avatar: z.string().url().optional(),\n role: z.enum(['admin', 'user', 'guest']).default('user')\n }),\n from: '@/components/user-card',\n fieldOverrides: {\n name: {\n inputProps: {\n placeholder: 'User full name'\n },\n description: 'Can be bound to user data variable'\n },\n \n email: {\n inputProps: {\n type: 'email',\n placeholder: 'user@example.com'\n },\n description: 'Bind to user email variable for dynamic content'\n },\n \n avatar: {\n inputProps: {\n type: 'url',\n placeholder: 'https://example.com/avatar.jpg'\n },\n description: 'Profile picture URL - can be bound to user avatar variable'\n }\n }\n};\n\n// Variables for binding\nconst userVariables = [\n { id: 'user-name', name: 'currentUserName', type: 'string', defaultValue: 'John Doe' },\n { id: 'user-email', name: 'currentUserEmail', type: 'string', defaultValue: 'john@example.com' },\n { id: 'user-avatar', name: 'currentUserAvatar', type: 'string', defaultValue: '/default-avatar.png' }\n];\n```\n\n## Best Practices\n\n### Field Override Design\n- **Use built-in overrides** for common properties like `className` and `children`\n- **Provide helpful placeholders** and descriptions\n- **Match field types** to the expected data (url, email, number, etc.)\n- **Include validation hints** in descriptions\n\n### User Experience\n- **Group related fields** logically\n- **Use appropriate input types** for better mobile experience\n- **Provide clear labels** and descriptions\n- **Test with real content** to ensure usability\n\n### Performance\n- **Memoize field override functions** to prevent unnecessary re-renders\n- **Use simple field overrides** when possible instead of custom components\n- **Debounce rapid input changes** for expensive operations\n\n### Accessibility\n- **Provide proper labels** for all form fields\n- **Include helpful descriptions** for complex fields\n- **Ensure keyboard navigation** works correctly\n- **Use semantic form elements** where appropriate"
- }
- ]
- } as const satisfies ComponentLayer;
\ No newline at end of file
diff --git a/app/docs/docs-data/docs-page-layers/props-panel.ts b/app/docs/docs-data/docs-page-layers/props-panel.ts
deleted file mode 100644
index 6f063a7..0000000
--- a/app/docs/docs-data/docs-page-layers/props-panel.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { ComponentLayer } from "@/components/ui/ui-builder/types";
-
-export const PROPS_PANEL_LAYER = {
- "id": "props-panel",
- "type": "div",
- "name": "Props Panel",
- "props": {
- "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen",
- "data-group": "editor-features"
- },
- "children": [
- {
- "type": "span",
- "children": "Props Panel",
- "id": "props-panel-title",
- "name": "Text",
- "props": {
- "className": "text-4xl"
- }
- },
- {
- "id": "props-panel-intro",
- "type": "Markdown",
- "name": "Markdown",
- "props": {},
- "children": "The props panel provides an intuitive interface for editing component properties. It automatically generates appropriate controls based on component Zod schemas using AutoForm, with support for custom field overrides."
- },
- {
- "id": "props-panel-demo",
- "type": "div",
- "name": "div",
- "props": {},
- "children": [
- {
- "id": "props-panel-badge",
- "type": "Badge",
- "name": "Badge",
- "props": {
- "variant": "default",
- "className": "rounded rounded-b-none"
- },
- "children": [
- {
- "id": "props-panel-badge-text",
- "type": "span",
- "name": "span",
- "props": {},
- "children": "Auto-Generated Forms"
- }
- ]
- },
- {
- "id": "props-panel-demo-frame",
- "type": "div",
- "name": "div",
- "props": {
- "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden"
- },
- "children": [
- {
- "id": "props-panel-iframe",
- "type": "iframe",
- "name": "iframe",
- "props": {
- "src": "http://localhost:3000/examples/editor",
- "title": "Props Panel Demo",
- "className": "aspect-square md:aspect-video w-full"
- },
- "children": []
- }
- ]
- }
- ]
- },
- {
- "id": "props-panel-content",
- "type": "Markdown",
- "name": "Markdown",
- "props": {},
- "children": "## Auto-Generated Controls\n\nThe props panel automatically creates appropriate input controls based on component Zod schemas:\n\n```tsx\nimport { z } from 'zod';\nimport { Button } from '@/components/ui/button';\n\n// Component registration with Zod schema\nconst componentRegistry = {\n Button: {\n component: Button,\n schema: z.object({\n variant: z.enum(['default', 'destructive', 'outline', 'secondary', 'ghost', 'link']).default('default'),\n size: z.enum(['default', 'sm', 'lg', 'icon']).default('default'),\n disabled: z.boolean().optional(),\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: '@/components/ui/button',\n defaultChildren: 'Click me'\n }\n};\n```\n\n## Supported Field Types\n\nAutoForm automatically generates controls for these Zod types:\n\n### Basic Types\n- **`z.string()`** - Text input\n- **`z.number()`** - Number input with step controls\n- **`z.boolean()`** - Checkbox toggle\n- **`z.date()`** - Date picker\n- **`z.enum()`** - Select dropdown\n\n### Advanced Types\n- **`z.array()`** - Array input with add/remove buttons\n- **`z.object()`** - Nested object editor\n- **`z.union()`** - Multiple type selector\n- **`z.optional()`** - Optional field with toggle\n\n### Examples\n\n```tsx\nconst advancedSchema = z.object({\n // Text with validation\n title: z.string().min(1, 'Title is required').max(100, 'Too long'),\n \n // Number with constraints\n count: z.coerce.number().min(0).max(100).default(1),\n \n // Enum for dropdowns\n size: z.enum(['sm', 'md', 'lg']).default('md'),\n \n // Optional boolean\n enabled: z.boolean().optional(),\n \n // Date input\n publishDate: z.coerce.date().optional(),\n \n // Array of objects\n items: z.array(z.object({\n name: z.string(),\n value: z.string()\n })).default([]),\n \n // Nested object\n config: z.object({\n theme: z.enum(['light', 'dark']),\n autoSave: z.boolean().default(true)\n }).optional()\n});\n```\n\n## Field Overrides\n\nCustomize the auto-generated form fields using `fieldOverrides`:\n\n```tsx\nimport { classNameFieldOverrides, childrenAsTextareaFieldOverrides } from '@/lib/ui-builder/registry/form-field-overrides';\n\nconst MyComponent = {\n component: MyComponent,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n color: z.string().default('#000000'),\n description: z.string().optional(),\n }),\n from: '@/components/my-component',\n fieldOverrides: {\n // Use built-in className editor with Tailwind suggestions\n className: (layer) => classNameFieldOverrides(layer),\n \n // Use textarea for children instead of default input\n children: (layer) => childrenAsTextareaFieldOverrides(layer),\n \n // Custom color picker\n color: {\n fieldType: 'input',\n inputProps: {\n type: 'color',\n className: 'w-full h-10'\n }\n },\n \n // Custom textarea with placeholder\n description: {\n fieldType: 'textarea',\n inputProps: {\n placeholder: 'Enter description...',\n rows: 3\n }\n }\n }\n};\n```\n\n### Built-in Field Overrides\n\nUI Builder provides several pre-built field overrides:\n\n```tsx\nimport { \n commonFieldOverrides,\n classNameFieldOverrides, \n childrenAsTextareaFieldOverrides,\n childrenFieldOverrides\n} from '@/lib/ui-builder/registry/form-field-overrides';\n\n// Apply common overrides (className + children)\nfieldOverrides: commonFieldOverrides()\n\n// Or individual overrides\nfieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer),\n children: (layer) => childrenFieldOverrides(layer)\n}\n```\n\n## Variable Binding\n\nThe props panel supports variable binding for dynamic content:\n\n```tsx\n// Component with variable-bound property\nconst buttonWithVariable = {\n id: 'dynamic-button',\n type: 'Button',\n props: {\n children: { __variableRef: 'button-text-var' }, // Bound to variable\n variant: 'primary' // Static value\n }\n};\n\n// Variable definition\nconst variables = [\n {\n id: 'button-text-var',\n name: 'buttonText',\n type: 'string',\n defaultValue: 'Click me!'\n }\n];\n```\n\nVariable-bound fields show:\n- **Variable icon** indicating the binding\n- **Variable name** instead of the raw value\n- **Quick unbind** option to convert back to static value\n\n## Panel Features\n\n### Component Actions\n- **Duplicate Component** - Clone the selected component\n- **Delete Component** - Remove the component from the page\n- **Component Type** - Shows the current component type\n\n### Form Validation\n- **Real-time validation** using Zod schema constraints\n- **Error messages** displayed inline with fields\n- **Required field indicators** for mandatory properties\n\n### Responsive Design\n- **Mobile-friendly** interface with collapsible sections\n- **Touch-optimized** controls for mobile editing\n- **Adaptive layout** based on screen size\n\n## Working with Complex Components\n\n### Nested Objects\n```tsx\nconst complexSchema = z.object({\n layout: z.object({\n direction: z.enum(['row', 'column']),\n gap: z.number().default(4),\n align: z.enum(['start', 'center', 'end'])\n }),\n styling: z.object({\n background: z.string().optional(),\n border: z.boolean().default(false),\n rounded: z.boolean().default(true)\n })\n});\n```\n\n### Array Fields\n```tsx\nconst listSchema = z.object({\n items: z.array(z.object({\n label: z.string(),\n value: z.string(),\n enabled: z.boolean().default(true)\n })).default([])\n});\n```\n\n## Integration with Layer Store\n\nThe props panel integrates directly with the layer store:\n\n```tsx\n// Access props panel state\nconst selectedLayerId = useLayerStore(state => state.selectedLayerId);\nconst findLayerById = useLayerStore(state => state.findLayerById);\nconst updateLayer = useLayerStore(state => state.updateLayer);\n\n// Props panel automatically updates when:\n// - A component is selected\n// - Component properties change\n// - Variables are updated\n```\n\n## Best Practices\n\n### Schema Design\n- **Use descriptive property names** that map to actual component props\n- **Provide sensible defaults** using `.default()`\n- **Add validation** with `.min()`, `.max()`, and custom refinements\n- **Use enums** for predefined options\n\n### Field Overrides\n- **Use built-in overrides** for common props like `className` and `children`\n- **Provide helpful placeholders** and labels\n- **Consider user experience** when choosing input types\n- **Test with real content** to ensure fields work as expected\n\n### Performance\n- **Memoize field overrides** to prevent unnecessary re-renders\n- **Use specific field types** rather than generic inputs\n- **Debounce rapid changes** for better performance"
- }
- ]
- } as const satisfies ComponentLayer;
\ No newline at end of file
diff --git a/app/docs/docs-data/docs-page-layers/quick-start.ts b/app/docs/docs-data/docs-page-layers/quick-start.ts
index 68163ee..4737978 100644
--- a/app/docs/docs-data/docs-page-layers/quick-start.ts
+++ b/app/docs/docs-data/docs-page-layers/quick-start.ts
@@ -23,21 +23,28 @@ export const QUICK_START_LAYER = {
"type": "Markdown",
"name": "Markdown",
"props": {},
- "children": "Get up and running with UI Builder in minutes. This guide will walk you through installation and creating your first visual editor."
+ "children": "Get up and running with UI Builder in minutes. This guide covers installation, basic setup, and your first working editor."
+ },
+ {
+ "id": "quick-start-compatibility",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "children": "## Compatibility Notes\n\n⚠️ **Tailwind 4 + React 19**: Migration coming soon. Currently blocked by 3rd party component compatibility. If using latest shadcn/ui CLI fails, try: `npx shadcn@2.1.8 add ...`\n\n⚠️ **Server Components**: Not supported. RSC can't be re-rendered client-side for live preview. A separate RSC renderer for final page rendering is possible."
},
{
"id": "quick-start-install",
"type": "Markdown",
"name": "Markdown",
"props": {},
- "children": "## Installation\n\nIf you are using shadcn/ui in your project, you can install the component directly from the registry:\n\n```bash\nnpx shadcn@latest add https://raw.githubusercontent.com/olliethedev/ui-builder/main/registry/block-registry.json\n```\n\nOr you can start a new project with the UI Builder:\n\n```bash\nnpx shadcn@latest init https://raw.githubusercontent.com/olliethedev/ui-builder/main/registry/block-registry.json\n```\n\n**Note:** You need to use [style variables](https://ui.shadcn.com/docs/theming) to have page theming working correctly.\n\n### Fixing Dependencies\n\nAdd dev dependencies, since there currently seems to be an issue with shadcn/ui not installing them from the registry:\n\n```bash\nnpm install -D @types/lodash.template @tailwindcss/typography @types/react-syntax-highlighter tailwindcss-animate @types/object-hash\n```"
+ "children": "## Installation\n\nIf you are using shadcn/ui in your project, install the component directly from the registry:\n\n```bash\nnpx shadcn@latest add https://raw.githubusercontent.com/olliethedev/ui-builder/main/registry/block-registry.json\n```\n\nOr start a new project with UI Builder:\n\n```bash\nnpx shadcn@latest init https://raw.githubusercontent.com/olliethedev/ui-builder/main/registry/block-registry.json\n```\n\n**Note:** You need to use [style variables](https://ui.shadcn.com/docs/theming) to have page theming working correctly.\n\n### Fix Dependencies\n\nAdd dev dependencies (current shadcn/ui registry limitation):\n\n```bash\nnpm install -D @types/lodash.template @tailwindcss/typography @types/react-syntax-highlighter tailwindcss-animate @types/object-hash\n```"
},
{
- "id": "quick-start-basic-usage",
+ "id": "quick-start-basic-setup",
"type": "Markdown",
"name": "Markdown",
"props": {},
- "children": "## Basic Example\n\nTo use the UI Builder, you **must** provide a component registry:\n\n```tsx\nimport z from \"zod\";\nimport UIBuilder from \"@/components/ui/ui-builder\";\nimport { Button } from \"@/components/ui/button\";\nimport { ComponentRegistry, ComponentLayer } from \"@/components/ui/ui-builder/types\";\nimport { commonFieldOverrides, classNameFieldOverrides, childrenAsTextareaFieldOverrides } from \"@/lib/ui-builder/registry/form-field-overrides\";\n\n// Define your component registry\nconst myComponentRegistry: ComponentRegistry = {\n Button: {\n component: Button,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n variant: z\n .enum([\n \"default\",\n \"destructive\",\n \"outline\",\n \"secondary\",\n \"ghost\",\n \"link\",\n ])\n .default(\"default\"),\n size: z.enum([\"default\", \"sm\", \"lg\", \"icon\"]).default(\"default\"),\n }),\n from: \"@/components/ui/button\",\n defaultChildren: [\n {\n id: \"button-text\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Hello World\",\n } satisfies ComponentLayer,\n ],\n fieldOverrides: commonFieldOverrides()\n },\n span: {\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n fieldOverrides: {\n className:(layer)=> classNameFieldOverrides(layer),\n children: (layer)=> childrenAsTextareaFieldOverrides(layer)\n },\n defaultChildren: \"Text\"\n },\n};\n\nexport function App() {\n return (\n \n );\n}\n```\n\n**Important:** Make sure to include definitions for all component types referenced in your `defaultChildren`. In this example, the Button's `defaultChildren` references a `span` component, so we include `span` in our registry."
+ "children": "## Basic Setup\n\nThe minimal setup requires just a component registry:\n\n```tsx\nimport UIBuilder from \"@/components/ui/ui-builder\";\nimport { primitiveComponentDefinitions } from \"@/lib/ui-builder/registry/primitive-component-definitions\";\nimport { complexComponentDefinitions } from \"@/lib/ui-builder/registry/complex-component-definitions\";\n\nconst componentRegistry = {\n ...primitiveComponentDefinitions, // div, span, img, etc.\n ...complexComponentDefinitions, // Button, Badge, Card, etc.\n};\n\nexport function App() {\n return (\n \n );\n}\n```\n\nThis gives you a full visual editor with pre-built shadcn/ui components."
},
{
"id": "quick-start-example",
@@ -76,7 +83,7 @@ export const QUICK_START_LAYER = {
"type": "iframe",
"name": "iframe",
"props": {
- "src": "http://localhost:3000/examples/basic",
+ "src": "/examples/basic",
"title": "Quick Start Example",
"className": "aspect-square md:aspect-video"
},
@@ -85,6 +92,81 @@ export const QUICK_START_LAYER = {
]
}
]
+ },
+ {
+ "id": "quick-start-with-state",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "children": "## Adding State Management\n\nFor real applications, you'll want to control the initial state and persist changes:\n\n```tsx\nimport UIBuilder from \"@/components/ui/ui-builder\";\nimport { ComponentLayer, Variable } from \"@/components/ui/ui-builder/types\";\n\n// Initial page structure\nconst initialLayers: ComponentLayer[] = [\n {\n id: \"welcome-page\",\n type: \"div\",\n name: \"Welcome Page\",\n props: {\n className: \"p-8 min-h-screen flex flex-col gap-6\",\n },\n children: [\n {\n id: \"title\",\n type: \"h1\",\n name: \"Page Title\",\n props: { \n className: \"text-4xl font-bold text-center\",\n },\n children: \"Welcome to UI Builder!\",\n },\n {\n id: \"cta-button\",\n type: \"Button\",\n name: \"CTA Button\",\n props: {\n variant: \"default\",\n className: \"mx-auto w-fit\",\n },\n children: [{\n id: \"button-text\",\n type: \"span\",\n name: \"Button Text\",\n props: {},\n children: \"Get Started\",\n }],\n },\n ],\n },\n];\n\n// Variables for dynamic content\nconst initialVariables: Variable[] = [\n {\n id: \"welcome-msg\",\n name: \"welcomeMessage\",\n type: \"string\",\n defaultValue: \"Welcome to UI Builder!\"\n }\n];\n\nexport function AppWithState() {\n const handleLayersChange = (updatedLayers: ComponentLayer[]) => {\n // Save to database, localStorage, etc.\n console.log(\"Layers updated:\", updatedLayers);\n };\n\n const handleVariablesChange = (updatedVariables: Variable[]) => {\n // Save to database, localStorage, etc.\n console.log(\"Variables updated:\", updatedVariables);\n };\n\n return (\n \n );\n}\n```"
+ },
+ {
+ "id": "quick-start-key-props",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "children": "## UIBuilder Props Reference\n\n### Required Props\n- **`componentRegistry`** - Maps component names to their definitions (see **Components Intro**)\n\n### Optional Props\n- **`initialLayers`** - Set initial page structure (e.g., from database)\n- **`onChange`** - Callback when pages change (for persistence)\n- **`initialVariables`** - Set initial variables for dynamic content \n- **`onVariablesChange`** - Callback when variables change\n- **`panelConfig`** - Customize editor panels (see **Panel Configuration**)\n- **`persistLayerStore`** - Enable localStorage persistence (default: `true`)\n- **`allowVariableEditing`** - Allow users to edit variables (default: `true`) \n- **`allowPagesCreation`** - Allow users to create pages (default: `true`)\n- **`allowPagesDeletion`** - Allow users to delete pages (default: `true`)\n\n**Note**: Only `componentRegistry` is required. All other props are optional and have sensible defaults."
+ },
+ {
+ "id": "quick-start-rendering",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "children": "## Rendering Without the Editor\n\nTo display pages in production without the editor interface, use `LayerRenderer`:\n\n```tsx\nimport LayerRenderer from \"@/components/ui/ui-builder/layer-renderer\";\n\n// Basic rendering\nexport function MyPage({ page }) {\n return (\n \n );\n}\n\n// With variables for dynamic content\nexport function DynamicPage({ page, userData }) {\n const variableValues = {\n \"welcome-msg\": `Welcome back, ${userData.name}!`\n };\n \n return (\n \n );\n}\n```\n\n🎯 **Try it**: Check out the **[Renderer Demo](/examples/renderer)** and **[Variables Demo](/examples/renderer/variables)** to see LayerRenderer in action."
+ },
+ {
+ "id": "quick-start-advanced-demo",
+ "type": "div",
+ "name": "div",
+ "props": {},
+ "children": [
+ {
+ "id": "advanced-demo-badge",
+ "type": "Badge",
+ "name": "Badge",
+ "props": {
+ "variant": "outline",
+ "className": "rounded rounded-b-none"
+ },
+ "children": [
+ {
+ "id": "advanced-demo-badge-text",
+ "type": "span",
+ "name": "span",
+ "props": {},
+ "children": "Full Featured Editor"
+ }
+ ]
+ },
+ {
+ "id": "advanced-demo-frame",
+ "type": "div",
+ "name": "div",
+ "props": {
+ "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden"
+ },
+ "children": [
+ {
+ "id": "advanced-demo-iframe",
+ "type": "iframe",
+ "name": "iframe",
+ "props": {
+ "src": "/examples/editor",
+ "title": "Full Featured Editor Demo",
+ "className": "aspect-square md:aspect-video"
+ },
+ "children": []
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "quick-start-next-steps",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "children": "## Next Steps\n\nNow that you have UI Builder running, explore these key areas:\n\n### Essential Concepts\n- **Components Intro** - Understand the component registry system\n- **Variables** - Add dynamic content with typed variables\n- **Rendering Pages** - Use LayerRenderer in your production app\n\n### Customization\n- **Custom Components** - Add your own React components to the registry\n- **Panel Configuration** - Customize the editor interface for your users\n\n### Advanced Use Cases\n- **Variable Binding** - Auto-bind components to system data\n- **Immutable Pages** - Create locked templates for consistency"
}
]
} as const satisfies ComponentLayer;
\ No newline at end of file
diff --git a/app/docs/docs-data/docs-page-layers/read-only-mode.ts b/app/docs/docs-data/docs-page-layers/read-only-mode.ts
index 42af8d3..9bedcaf 100644
--- a/app/docs/docs-data/docs-page-layers/read-only-mode.ts
+++ b/app/docs/docs-data/docs-page-layers/read-only-mode.ts
@@ -3,7 +3,7 @@ import { ComponentLayer } from "@/components/ui/ui-builder/types";
export const READ_ONLY_MODE_LAYER = {
"id": "read-only-mode",
"type": "div",
- "name": "Read Only Mode",
+ "name": "Editing Restrictions",
"props": {
"className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen",
"data-group": "data-variables"
@@ -11,7 +11,7 @@ export const READ_ONLY_MODE_LAYER = {
"children": [
{
"type": "span",
- "children": "Read Only Mode",
+ "children": "Editing Restrictions",
"id": "read-only-mode-title",
"name": "Text",
"props": {
@@ -23,14 +23,35 @@ export const READ_ONLY_MODE_LAYER = {
"type": "Markdown",
"name": "Markdown",
"props": {},
- "children": "Control editing capabilities in UI Builder by restricting specific operations like variable editing, page creation, and page deletion. This enables read-only modes perfect for production environments, previews, and restricted editing scenarios."
+ "children": "Control editing capabilities in UI Builder by restricting specific operations like variable editing, page creation, and page deletion. Perfect for production environments, content-only editing, and role-based access control."
},
{
"id": "read-only-mode-content",
"type": "Markdown",
"name": "Markdown",
"props": {},
- "children": "## Controlling Edit Permissions\n\nUI Builder provides granular control over editing capabilities through specific props:\n\n```tsx\n\n```\n\n### Available Permission Controls\n\n| Prop | Default | Description |\n|------|---------|-------------|\n| `allowVariableEditing` | `true` | Controls variable add/edit/delete operations |\n| `allowPagesCreation` | `true` | Controls ability to create new pages |\n| `allowPagesDeletion` | `true` | Controls ability to delete existing pages |\n\n## Read-Only Variable Mode\n\n### Disabling Variable Editing\n\nWhen `allowVariableEditing={false}`, the Variables panel becomes read-only:\n\n```tsx\nfunction ReadOnlyVariablesExample() {\n const systemVariables = [\n {\n id: 'company-name',\n name: 'companyName',\n type: 'string',\n defaultValue: 'Acme Corp'\n },\n {\n id: 'brand-color',\n name: 'brandColor',\n type: 'string',\n defaultValue: '#3b82f6'\n }\n ];\n\n return (\n \n );\n}\n```\n\n### What's Disabled in Read-Only Variable Mode\n\n- ❌ **Add Variable** button is hidden\n- ❌ **Edit Variable** buttons are hidden on variable cards\n- ❌ **Delete Variable** buttons are hidden on variable cards\n- ✅ **Variable binding** still works in props panel\n- ✅ **Variable values** can still be overridden in LayerRenderer\n- ✅ **Immutable bindings** remain enforced\n\n## Read-Only Pages Mode\n\n### Restricting Page Operations\n\n```tsx\nfunction RestrictedPagesExample() {\n const templatePages = [\n {\n id: 'home-template',\n type: 'div',\n name: 'Home Template',\n props: { className: 'p-4' },\n children: []\n },\n {\n id: 'about-template',\n type: 'div',\n name: 'About Template',\n props: { className: 'p-4' },\n children: []\n }\n ];\n\n return (\n \n );\n}\n```\n\n### Page Restriction Effects\n\nWith `allowPagesCreation={false}`:\n- ❌ **Add Page** functionality is disabled\n- ✅ **Page content editing** remains available\n- ✅ **Page switching** between existing pages works\n\nWith `allowPagesDeletion={false}`:\n- ❌ **Delete Page** buttons are hidden in props panel\n- ✅ **Page content editing** remains available\n- ✅ **Page duplication** may still work (creates duplicates, doesn't delete)\n\n## Complete Read-Only Mode\n\n### Fully Restricted Editor\n\nFor maximum restrictions, disable all editing capabilities:\n\n```tsx\nfunction FullyReadOnlyEditor() {\n return (\n \n );\n}\n```\n\n### What Still Works in Full Read-Only Mode\n\n- ✅ **Component selection** and navigation\n- ✅ **Visual editing** of component properties\n- ✅ **Layer manipulation** (add, remove, reorder components)\n- ✅ **Variable binding** in props panel\n- ✅ **Theme configuration** in appearance panel\n- ✅ **Code generation** and export\n- ✅ **Undo/Redo** operations\n\n## Use Cases and Patterns\n\n### Production Preview Mode\n\n```tsx\nfunction ProductionPreview({ templateId }) {\n const [template, setTemplate] = useState(null);\n const [variables, setVariables] = useState([]);\n\n useEffect(() => {\n // Load template and variables from API\n Promise.all([\n fetch(`/api/templates/${templateId}`).then(r => r.json()),\n fetch(`/api/templates/${templateId}/variables`).then(r => r.json())\n ]).then(([templateData, variableData]) => {\n setTemplate(templateData);\n setVariables(variableData);\n });\n }, [templateId]);\n\n if (!template) return Loading...
;\n\n return (\n \n
\n
Template Preview \n
Read-only mode - variables locked
\n
\n \n
\n
\n );\n}\n```\n\n### Role-Based Editing Restrictions\n\n```tsx\nfunction RoleBasedEditor({ user, template }) {\n const canEditVariables = user.role === 'admin' || user.role === 'developer';\n const canManagePages = user.role === 'admin';\n\n return (\n \n );\n}\n```\n\n### Content Editor Mode\n\n```tsx\nfunction ContentEditorMode() {\n // Content editors can modify component content but not structure\n return (\n \n );\n}\n```\n\n### Environment-Based Restrictions\n\n```tsx\nfunction EnvironmentAwareEditor() {\n const isProduction = process.env.NODE_ENV === 'production';\n const isDevelopment = process.env.NODE_ENV === 'development';\n \n return (\n \n );\n}\n```\n\n## Rendering Without Editor\n\n### Using LayerRenderer for Display-Only\n\nFor pure display without any editing interface, use `LayerRenderer`:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\n\nfunction DisplayOnlyPage({ pageData, variables, userValues }) {\n return (\n \n );\n}\n```\n\n### LayerRenderer vs. Restricted UIBuilder\n\n| Feature | LayerRenderer | Restricted UIBuilder |\n|---------|---------------|----------------------|\n| Bundle size | Smaller (no editor) | Larger (full editor) |\n| Performance | Faster (no editor overhead) | Slower (editor present) |\n| Editing UI | None | Present but restricted |\n| Variable binding | ✅ | ✅ |\n| Code generation | ❌ | ✅ |\n| Visual editing | ❌ | ✅ (limited) |\n\n## Programmatic Control\n\n### Dynamic Permission Updates\n\n```tsx\nfunction DynamicPermissionsEditor() {\n const [permissions, setPermissions] = useState({\n allowVariableEditing: false,\n allowPagesCreation: false,\n allowPagesDeletion: false\n });\n\n const enableEditMode = () => {\n setPermissions({\n allowVariableEditing: true,\n allowPagesCreation: true,\n allowPagesDeletion: true\n });\n };\n\n const enableReadOnlyMode = () => {\n setPermissions({\n allowVariableEditing: false,\n allowPagesCreation: false,\n allowPagesDeletion: false\n });\n };\n\n return (\n \n
\n Enable Editing \n Read-Only Mode \n
\n \n
\n
\n );\n}\n```\n\n### Feature Flag Integration\n\n```tsx\nfunction FeatureFlagEditor({ featureFlags }) {\n return (\n \n );\n}\n```\n\n## Security Considerations\n\n### Variable Security\n\n```tsx\n// Secure sensitive variables from editing\nconst secureVariables = [\n {\n id: 'api-key',\n name: 'apiKey',\n type: 'string',\n defaultValue: 'sk_live_...'\n },\n {\n id: 'user-permissions',\n name: 'userPermissions',\n type: 'string',\n defaultValue: 'read-only'\n }\n];\n\nfunction SecureEditor() {\n return (\n \n );\n}\n```\n\n### Input Validation\n\n```tsx\nfunction ValidatedEditor({ initialData }) {\n // Validate and sanitize data before passing to UI Builder\n const sanitizedPages = sanitizePageData(initialData.pages);\n const validatedVariables = validateVariables(initialData.variables);\n \n return (\n \n );\n}\n```\n\n## Best Practices\n\n### Choosing the Right Restrictions\n\n- **Use `allowVariableEditing={false}`** for production deployments\n- **Use `allowPagesCreation={false}`** for content-only editing\n- **Use `allowPagesDeletion={false}`** to prevent accidental page loss\n- **Use `LayerRenderer`** for pure display without editing needs\n\n### User Experience Considerations\n\n- **Provide clear feedback** about restricted functionality\n- **Use role-based restrictions** rather than blanket restrictions\n- **Consider progressive permissions** (unlock features as users gain trust)\n- **Document restriction reasons** for transparency\n\n### Performance Optimization\n\n- **Use `LayerRenderer`** when editing isn't needed\n- **Minimize editor bundle** in production builds\n- **Cache restricted configurations** to avoid re-computation\n- **Consider server-side rendering** for display-only scenarios"
+ "children": "## Permission Control Props\n\nUI Builder provides three boolean props to control editing permissions:\n\n```tsx\n\n```\n\n| Prop | Default | Description |\n|------|---------|-------------|\n| `allowVariableEditing` | `true` | Controls variable add/edit/delete in Variables panel |\n| `allowPagesCreation` | `true` | Controls ability to create new pages |\n| `allowPagesDeletion` | `true` | Controls ability to delete existing pages |\n\n## Interactive Demo\n\nExperience all read-only modes in one interactive demo:\n\n- **Live Demo:** [/examples/editor/read-only-mode](/examples/editor/read-only-mode)\n- **Features:** Switch between different permission levels in real-time\n- **Modes:** Full editing, content-only, no variables, and full read-only\n- **What to try:** Toggle between modes to see UI changes and restrictions"
+ },
+ {
+ "id": "demo-iframe",
+ "type": "iframe",
+ "name": "Read-Only Demo Iframe",
+ "props": {
+ "src": "/examples/editor/read-only-mode",
+ "width": "100%",
+ "height": "600",
+ "frameBorder": "0",
+ "className": "w-full border border-gray-200 rounded-lg shadow-sm mb-6",
+ "title": "UI Builder Read-Only Mode Interactive Demo"
+ },
+ "children": []
+ },
+ {
+ "id": "read-only-mode-content-continued",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "children": "## Common Use Cases\n\n### Content-Only Editing\n\nAllow content editing while preventing structural changes:\n\n```tsx\n\n```\n\n**Use case:** Content teams updating copy, images, and variable content without changing page layouts.\n\n### Production Preview Mode\n\nLock down all structural changes:\n\n```tsx\n\n```\n\n**Use case:** Previewing templates in production with system-controlled variables.\n\n### Role-Based Access Control\n\nDifferent permissions based on user roles:\n\n```tsx\nfunction RoleBasedEditor({ user, template }) {\n const canEditVariables = user.role === 'admin' || user.role === 'developer';\n const canManagePages = user.role === 'admin';\n\n return (\n \n );\n}\n```\n\n## Variable Binding in Templates\n\nWhen creating templates with variable references, use the correct format:\n\n```tsx\n// ✅ Correct: Variable binding in component props\nconst templateLayer: ComponentLayer = {\n id: \"title\",\n type: \"span\",\n name: \"Title\",\n props: {\n className: \"text-2xl font-bold\",\n children: { __variableRef: \"pageTitle\" } // Correct format\n },\n children: []\n};\n\n// ❌ Incorrect: Variable binding directly in children\nconst badTemplate: ComponentLayer = {\n id: \"title\",\n type: \"span\",\n name: \"Title\",\n props: {\n className: \"text-2xl font-bold\"\n },\n children: { __variableRef: \"pageTitle\" } // Wrong - causes TypeScript errors\n};\n```\n\n**Key points:**\n- Variable references go in the `props` object\n- Use `__variableRef` (without quotes) as the property name\n- The value is the variable ID as a string\n- Set `children: []` when using variable binding\n\n## Additional Examples\n\n### Fixed Pages Example\n\nSee the immutable pages example that demonstrates locked page structure:\n\n- **Live Demo:** [/examples/editor/immutable-pages](/examples/editor/immutable-pages)\n- **Implementation:** Uses `allowPagesCreation={false}` and `allowPagesDeletion={false}`\n- **What's locked:** Page creation and deletion\n- **What works:** Content editing, component manipulation, theme changes\n\n### Variable Read-Only Example\n\nSee the immutable bindings example that demonstrates locked variables:\n\n- **Live Demo:** [/examples/editor/immutable-bindings](/examples/editor/immutable-bindings)\n- **Implementation:** Uses `allowVariableEditing={false}`\n- **What's locked:** Variable creation, editing, and deletion\n- **What works:** Variable binding in props panel, visual component editing\n\n## What's Still Available in Read-Only Mode\n\nEven with restrictions enabled, users can still:\n\n✅ **Visual Component Editing:** Add, remove, and modify components on the canvas \n✅ **Props Panel:** Configure component properties and bind to existing variables \n✅ **Appearance Panel:** Modify themes and styling \n✅ **Layer Navigation:** Select and organize components in the layers panel \n✅ **Undo/Redo:** Full history navigation \n✅ **Code Generation:** Export React code \n\n## When to Use LayerRenderer Instead\n\nFor pure display without any editing interface, use `LayerRenderer`:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\n\nfunction DisplayOnlyPage({ pageData, variables, userValues }) {\n return (\n \n );\n}\n```\n\n**Choose LayerRenderer when:**\n- No editing interface needed\n- Smaller bundle size required \n- Better performance for display-only scenarios\n- Rendering with dynamic data at runtime\n\n**Choose restricted UIBuilder when:**\n- Some editing capabilities needed\n- Code generation features required\n- Visual interface helps with content understanding\n- Fine-grained permission control needed\n\n## Implementation Pattern\n\n```tsx\nfunction ConfigurableEditor({ \n template, \n user, \n environment \n}) {\n const permissions = {\n allowVariableEditing: environment !== 'production' && user.canEditVariables,\n allowPagesCreation: user.role === 'admin',\n allowPagesDeletion: user.role === 'admin'\n };\n\n return (\n \n );\n}\n```\n\n## Best Practices\n\n### Security Considerations\n\n- **Validate data server-side:** Client-side restrictions are for UX, not security\n- **Sanitize inputs:** Always validate and sanitize layer data and variables\n- **Use immutable bindings:** For system variables that must never change\n- **Implement proper authentication:** Control access at the application level\n\n### User Experience\n\n- **Provide clear feedback:** Show users what's restricted and why\n- **Progressive permissions:** Unlock features as users gain trust/experience\n- **Contextual help:** Explain restrictions in context\n- **Consistent behavior:** Apply restrictions predictably across the interface\n\n### Performance\n\n- **Use LayerRenderer for display-only:** Smaller bundle, better performance\n- **Cache configurations:** Avoid re-computing permissions on every render\n- **Optimize initial data:** Only load necessary variables and pages\n- **Consider lazy loading:** Load restricted features only when needed"
}
]
} as const satisfies ComponentLayer;
\ No newline at end of file
diff --git a/app/docs/docs-data/docs-page-layers/rendering-pages.ts b/app/docs/docs-data/docs-page-layers/rendering-pages.ts
index 6dd4d15..d9b8ccb 100644
--- a/app/docs/docs-data/docs-page-layers/rendering-pages.ts
+++ b/app/docs/docs-data/docs-page-layers/rendering-pages.ts
@@ -23,7 +23,7 @@ export const RENDERING_PAGES_LAYER = {
"type": "Markdown",
"name": "Markdown",
"props": {},
- "children": "Render UI Builder pages without the editor interface using the LayerRenderer component. Perfect for displaying your designs in production with full variable binding and dynamic content support."
+ "children": "Render UI Builder pages in production using the `LayerRenderer` component. Display your designed pages without the editor interface, with full support for dynamic content through variables."
},
{
"id": "rendering-pages-demo",
@@ -45,7 +45,7 @@ export const RENDERING_PAGES_LAYER = {
"type": "span",
"name": "span",
"props": {},
- "children": "Live Rendering Demo"
+ "children": "Basic Rendering Demo"
}
]
},
@@ -124,7 +124,7 @@ export const RENDERING_PAGES_LAYER = {
"type": "Markdown",
"name": "Markdown",
"props": {},
- "children": "## Basic Rendering\n\nUse the `LayerRenderer` component to render UI Builder pages without the editor:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\nimport { ComponentLayer, ComponentRegistry } from '@/components/ui/ui-builder/types';\n\n// Your component registry (same as used in UIBuilder)\nconst myComponentRegistry: ComponentRegistry = {\n // Your component definitions\n};\n\n// Page data (from UIBuilder or database)\nconst page: ComponentLayer = {\n id: \"my-page\",\n type: \"div\",\n name: \"My Page\",\n props: {\n className: \"p-4\"\n },\n children: [\n // Your page structure\n ]\n};\n\nfunction MyRenderedPage() {\n return (\n \n );\n}\n```\n\n## Rendering with Variables\n\nThe real power of LayerRenderer comes from variable binding - same page structure with different data:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\nimport { ComponentLayer, Variable } from '@/components/ui/ui-builder/types';\n\n// Define variables for dynamic content\nconst variables: Variable[] = [\n {\n id: \"userName\",\n name: \"User Name\",\n type: \"string\",\n defaultValue: \"John Doe\"\n },\n {\n id: \"userAge\",\n name: \"User Age\", \n type: \"number\",\n defaultValue: 25\n },\n {\n id: \"isActive\",\n name: \"Is Active\",\n type: \"boolean\",\n defaultValue: true\n }\n];\n\n// Page with variable bindings\nconst pageWithVariables: ComponentLayer = {\n id: \"user-profile\",\n type: \"div\",\n props: {\n className: \"p-6 bg-white rounded-lg shadow\"\n },\n children: [\n {\n id: \"welcome-text\",\n type: \"h1\",\n props: {\n className: \"text-2xl font-bold\",\n children: { __variableRef: \"userName\" } // Bound to userName variable\n },\n children: []\n },\n {\n id: \"age-text\",\n type: \"p\",\n props: {\n children: { __variableRef: \"userAge\" } // Bound to userAge variable\n },\n children: []\n }\n ]\n};\n\n// Override variable values at runtime\nconst variableValues = {\n userName: \"Jane Smith\", // Override default\n userAge: 30, // Override default\n isActive: false // Override default\n};\n\nfunction DynamicUserProfile() {\n return (\n \n );\n}\n```\n\n## Multi-Tenant Applications\n\nPerfect for white-label applications where each customer gets customized branding:\n\n```tsx\nfunction CustomerDashboard({ customerId }: { customerId: string }) {\n const [pageData, setPageData] = useState(null);\n const [customerVariables, setCustomerVariables] = useState({});\n \n useEffect(() => {\n async function loadCustomerPage() {\n // Load the page structure (same for all customers)\n const pageResponse = await fetch('/api/templates/dashboard');\n const page = await pageResponse.json();\n \n // Load customer-specific variable values\n const varsResponse = await fetch(`/api/customers/${customerId}/branding`);\n const variables = await varsResponse.json();\n \n setPageData(page);\n setCustomerVariables(variables);\n }\n \n loadCustomerPage();\n }, [customerId]);\n \n if (!pageData) return Loading...
;\n \n return (\n \n );\n}\n```\n\n## Server-Side Rendering (SSR)\n\nLayerRenderer works with Next.js SSR for better performance and SEO:\n\n```tsx\n// pages/page/[id].tsx or app/page/[id]/page.tsx\nimport { GetServerSideProps } from 'next';\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\n\ninterface PageProps {\n page: ComponentLayer;\n variables: Variable[];\n variableValues: Record;\n}\n\n// Server-side data fetching\nexport const getServerSideProps: GetServerSideProps = async ({ params }) => {\n const pageId = params?.id as string;\n \n // Fetch page data from your database\n const [page, variables, userData] = await Promise.all([\n getPageById(pageId),\n getPageVariables(pageId),\n getCurrentUserData() // For personalization\n ]);\n \n const variableValues = {\n userName: userData.name,\n userEmail: userData.email,\n // Inject real data into variables\n };\n \n return {\n props: {\n page,\n variables,\n variableValues\n }\n };\n};\n\n// Component renders on server\nfunction ServerRenderedPage({ page, variables, variableValues }: PageProps) {\n return (\n \n );\n}\n\nexport default ServerRenderedPage;\n```\n\n## Real-Time Data Integration\n\nBind to live data sources for dynamic, real-time interfaces:\n\n```tsx\nfunction LiveDashboard() {\n const [liveData, setLiveData] = useState({\n activeUsers: 0,\n revenue: 0,\n conversionRate: 0\n });\n \n // Subscribe to real-time updates\n useEffect(() => {\n const socket = new WebSocket('ws://localhost:8080/analytics');\n \n socket.onmessage = (event) => {\n const data = JSON.parse(event.data);\n setLiveData(data);\n };\n \n return () => socket.close();\n }, []);\n \n return (\n \n );\n}\n```\n\n## A/B Testing & Feature Flags\n\nUse boolean variables for conditional rendering:\n\n```tsx\nfunction ABTestPage({ userId }: { userId: string }) {\n const [experimentFlags, setExperimentFlags] = useState({});\n \n useEffect(() => {\n // Determine which experiment variant user should see\n async function getExperimentFlags() {\n const response = await fetch(`/api/experiments/${userId}`);\n const flags = await response.json();\n setExperimentFlags(flags);\n }\n \n getExperimentFlags();\n }, [userId]);\n \n return (\n \n );\n}\n```\n\n## LayerRenderer Props Reference\n\n- **`page`** (required): The ComponentLayer to render\n- **`componentRegistry`** (required): Registry of available components\n- **`className`**: CSS class for the root container\n- **`variables`**: Array of Variable definitions for the page\n- **`variableValues`**: Object mapping variable IDs to runtime values\n- **`editorConfig`**: Internal editor configuration (rarely needed)\n\n## Performance Optimization\n\nOptimize rendering performance for large pages:\n\n```tsx\n// Memoize the renderer to prevent unnecessary re-renders\nconst MemoizedRenderer = React.memo(LayerRenderer, (prevProps, nextProps) => {\n return (\n prevProps.page === nextProps.page &&\n JSON.stringify(prevProps.variableValues) === JSON.stringify(nextProps.variableValues)\n );\n});\n\n// Use in your component\nfunction OptimizedPage() {\n return (\n \n );\n}\n```\n\n## Error Handling\n\nHandle rendering errors gracefully:\n\n```tsx\nimport { ErrorBoundary } from 'react-error-boundary';\n\nfunction ErrorFallback({ error }: { error: Error }) {\n return (\n \n
Something went wrong \n
{error.message}
\n
window.location.reload()}\n className=\"mt-2 px-4 py-2 bg-red-600 text-white rounded\"\n >\n Reload Page\n \n
\n );\n}\n\nfunction SafeRenderedPage() {\n return (\n \n \n \n );\n}\n```\n\n## Best Practices\n\n1. **Always use the same componentRegistry** in both UIBuilder and LayerRenderer\n2. **Validate variable values** before passing to LayerRenderer to prevent runtime errors\n3. **Handle loading states** while fetching page data and variables\n4. **Use memoization** for expensive variable calculations\n5. **Implement error boundaries** to gracefully handle rendering failures\n6. **Consider caching** page data and variable values for better performance\n7. **Test with different variable combinations** to ensure your pages are robust"
+ "children": "## Basic Usage\n\nUse the `LayerRenderer` component to display UI Builder pages without the editor interface:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\nimport { ComponentLayer, ComponentRegistry } from '@/components/ui/ui-builder/types';\n\n// Your component registry (same as used in UIBuilder)\nconst myComponentRegistry: ComponentRegistry = {\n // Your component definitions...\n};\n\n// Page data from UIBuilder or database\nconst page: ComponentLayer = {\n id: \"welcome-page\",\n type: \"div\",\n name: \"Welcome Page\",\n props: {\n className: \"p-6 max-w-4xl mx-auto\"\n },\n children: [\n {\n id: \"title\",\n type: \"h1\",\n name: \"Title\",\n props: {\n className: \"text-3xl font-bold mb-4\"\n },\n children: \"Welcome to My App\"\n },\n {\n id: \"description\",\n type: \"p\",\n name: \"Description\",\n props: {\n className: \"text-gray-600\"\n },\n children: \"This page was built with UI Builder.\"\n }\n ]\n};\n\nfunction MyRenderedPage() {\n return (\n \n );\n}\n```\n\n## Rendering with Variables\n\nMake your pages dynamic by binding component properties to variables:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\nimport { Variable } from '@/components/ui/ui-builder/types';\n\n// Define your variables\nconst variables: Variable[] = [\n {\n id: \"userName\",\n name: \"User Name\",\n type: \"string\",\n defaultValue: \"Guest\"\n },\n {\n id: \"userAge\",\n name: \"User Age\",\n type: \"number\",\n defaultValue: 25\n },\n {\n id: \"showWelcomeMessage\",\n name: \"Show Welcome Message\",\n type: \"boolean\",\n defaultValue: true\n }\n];\n\n// Page with variable bindings (created in UIBuilder)\nconst pageWithVariables: ComponentLayer = {\n id: \"user-profile\",\n type: \"div\",\n props: {\n className: \"p-6 bg-white rounded-lg shadow\"\n },\n children: [\n {\n id: \"welcome-message\",\n type: \"h2\",\n props: {\n className: \"text-2xl font-bold mb-2\",\n children: { __variableRef: \"userName\" } // Bound to userName variable\n },\n children: []\n },\n {\n id: \"age-display\",\n type: \"p\",\n props: {\n className: \"text-gray-600\",\n children: { __variableRef: \"userAge\" } // Bound to userAge variable\n },\n children: []\n }\n ]\n};\n\n// Provide runtime values for variables\nconst variableValues = {\n userName: \"Jane Smith\",\n userAge: 28,\n showWelcomeMessage: true\n};\n\nfunction DynamicUserProfile() {\n return (\n \n );\n}\n```\n\n## Production Integration\n\nIntegrate with your data sources to create personalized experiences:\n\n```tsx\nfunction CustomerPage({ customerId }: { customerId: string }) {\n const [pageData, setPageData] = useState(null);\n const [customerData, setCustomerData] = useState({});\n \n useEffect(() => {\n async function loadData() {\n // Load page structure from your CMS/database\n const pageResponse = await fetch('/api/pages/customer-dashboard');\n const page = await pageResponse.json();\n \n // Load customer-specific data\n const customerResponse = await fetch(`/api/customers/${customerId}`);\n const customer = await customerResponse.json();\n \n setPageData(page);\n setCustomerData(customer);\n }\n \n loadData();\n }, [customerId]);\n \n if (!pageData) return Loading...
;\n \n return (\n \n );\n}\n```\n\n## Performance Optimization\n\nOptimize rendering performance for production:\n\n```tsx\n// Memoize the renderer to prevent unnecessary re-renders\nconst MemoizedRenderer = React.memo(LayerRenderer, (prevProps, nextProps) => {\n return (\n prevProps.page === nextProps.page &&\n JSON.stringify(prevProps.variableValues) === JSON.stringify(nextProps.variableValues)\n );\n});\n\n// Use in your component\nfunction OptimizedPage() {\n return (\n \n );\n}\n```\n\n## Error Handling\n\nHandle rendering errors gracefully in production:\n\n```tsx\nimport { ErrorBoundary } from 'react-error-boundary';\n\nfunction ErrorFallback({ error }: { error: Error }) {\n return (\n \n
Page failed to load \n
{error.message}
\n
window.location.reload()}\n className=\"mt-2 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700\"\n >\n Reload Page\n \n
\n );\n}\n\nfunction SafeRenderedPage() {\n return (\n \n \n \n );\n}\n```\n\n## LayerRenderer Props\n\n- **`page`** (required): The `ComponentLayer` to render\n- **`componentRegistry`** (required): Registry mapping component types to their definitions\n- **`className`**: Optional CSS class for the root container\n- **`variables`**: Array of `Variable` definitions available for binding\n- **`variableValues`**: Object mapping variable IDs to runtime values (overrides defaults)\n- **`editorConfig`**: Internal editor configuration (rarely needed in production)\n\n## Best Practices\n\n1. **Use the same `componentRegistry`** in both `UIBuilder` and `LayerRenderer`\n2. **Validate variable values** before passing to LayerRenderer to prevent runtime errors\n3. **Handle loading states** while fetching page data and variables\n4. **Implement error boundaries** to gracefully handle rendering failures\n5. **Cache page data** when possible for better performance\n6. **Memoize expensive variable calculations** to avoid unnecessary re-computations\n7. **Test variable bindings** thoroughly to ensure robustness across different data scenarios"
}
]
} as const satisfies ComponentLayer;
\ No newline at end of file
diff --git a/app/docs/docs-data/docs-page-layers/variable-binding.ts b/app/docs/docs-data/docs-page-layers/variable-binding.ts
index fe9df8c..bbfe66c 100644
--- a/app/docs/docs-data/docs-page-layers/variable-binding.ts
+++ b/app/docs/docs-data/docs-page-layers/variable-binding.ts
@@ -23,14 +23,61 @@ export const VARIABLE_BINDING_LAYER = {
"type": "Markdown",
"name": "Markdown",
"props": {},
- "children": "Variable binding connects dynamic data to component properties, enabling interfaces that update automatically when variable values change. Learn how to bind variables through the UI and programmatically."
+ "children": "Learn how to connect variables to component properties through the UI and programmatically. This page focuses on the binding mechanics—see **Variables** for fundamentals and **Data Binding** for external data integration."
+ },
+ {
+ "id": "variable-binding-demo",
+ "type": "div",
+ "name": "div",
+ "props": {},
+ "children": [
+ {
+ "id": "variable-binding-badge",
+ "type": "Badge",
+ "name": "Badge",
+ "props": {
+ "variant": "default",
+ "className": "rounded rounded-b-none"
+ },
+ "children": [
+ {
+ "id": "variable-binding-badge-text",
+ "type": "span",
+ "name": "span",
+ "props": {},
+ "children": "Live Variable Binding Example"
+ }
+ ]
+ },
+ {
+ "id": "variable-binding-demo-frame",
+ "type": "div",
+ "name": "div",
+ "props": {
+ "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden"
+ },
+ "children": [
+ {
+ "id": "variable-binding-iframe",
+ "type": "iframe",
+ "name": "iframe",
+ "props": {
+ "src": "/examples/renderer/variables",
+ "title": "Variable Binding Demo",
+ "className": "aspect-square md:aspect-video w-full"
+ },
+ "children": []
+ }
+ ]
+ }
+ ]
},
{
"id": "variable-binding-content",
"type": "Markdown",
"name": "Markdown",
"props": {},
- "children": "## How Variable Binding Works\n\nVariable binding in UI Builder replaces static property values with dynamic references to variables. When a component renders, these references are resolved to actual values.\n\n### Binding Structure\n\nWhen bound, a component property stores a variable reference object:\n\n```tsx\n// Before binding - static value\nconst button = {\n props: {\n children: 'Click me',\n disabled: false\n }\n};\n\n// After binding - variable references\nconst button = {\n props: {\n children: { __variableRef: 'button-text-var' },\n disabled: { __variableRef: 'is-loading-var' }\n }\n};\n```\n\n## Binding Variables Through the UI\n\n### Step-by-Step Binding Process\n\n1. **Select a component** in the editor canvas\n2. **Open the Properties panel** (right sidebar)\n3. **Find the property** you want to bind\n4. **Click the link icon** (🔗) next to the property field\n5. **Choose a variable** from the dropdown menu\n6. **The property is now bound** and shows the variable info\n\n### Visual Indicators\n\nBound properties are visually distinct in the props panel:\n\n- **Link icon** indicates the property supports binding\n- **Variable card** shows when a property is bound\n- **Variable name and type** are displayed\n- **Current value** shows the variable's default/resolved value\n- **Unlink button** allows unbinding (if not immutable)\n- **Lock icon** indicates immutable bindings\n\n### Unbinding Variables\n\nTo remove a variable binding:\n\n1. **Select the component** with bound properties\n2. **Find the bound property** in the props panel\n3. **Click the unlink icon** (🔗⛌) next to the variable card\n4. **Property reverts** to its default schema value\n\n**Note:** Immutable bindings (marked with 🔒) cannot be unbound through the UI.\n\n## Variable Resolution at Runtime\n\n### In LayerRenderer\n\nWhen rendering pages, variable references are resolved to actual values:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\n\n// Page with variable bindings\nconst pageWithBindings = {\n id: 'welcome-page',\n type: 'div',\n props: { className: 'p-4' },\n children: [\n {\n id: 'welcome-button',\n type: 'Button',\n props: {\n children: { __variableRef: 'welcome-message' },\n disabled: { __variableRef: 'is-loading' }\n }\n }\n ]\n};\n\n// Variables definition\nconst variables = [\n {\n id: 'welcome-message',\n name: 'welcomeMessage',\n type: 'string',\n defaultValue: 'Welcome!'\n },\n {\n id: 'is-loading',\n name: 'isLoading',\n type: 'boolean',\n defaultValue: false\n }\n];\n\n// Runtime values override defaults\nconst variableValues = {\n 'welcome-message': 'Hello, Jane!',\n 'is-loading': true\n};\n\nfunction MyPage() {\n return (\n \n );\n}\n\n// Renders as:\n// Hello, Jane! \n```\n\n### Resolution Process\n\n1. **Scan component props** for variable reference objects\n2. **Look up variable by ID** in the variables array\n3. **Use runtime value** from `variableValues` if provided\n4. **Fall back to default value** from variable definition\n5. **Replace reference** with resolved value\n6. **Pass resolved props** to React component\n\n## Automatic Variable Binding\n\n### Default Variable Bindings\n\nComponents can automatically bind to variables when added to the canvas:\n\n```tsx\nconst componentRegistry = {\n UserCard: {\n component: UserCard,\n schema: z.object({\n userId: z.string(),\n displayName: z.string(),\n avatarUrl: z.string().optional(),\n isOnline: z.boolean().default(false)\n }),\n from: '@/components/ui/user-card',\n defaultVariableBindings: [\n {\n propName: 'userId',\n variableId: 'current-user-id',\n immutable: true // Cannot be unbound\n },\n {\n propName: 'displayName',\n variableId: 'current-user-name',\n immutable: false // Can be changed\n },\n {\n propName: 'isOnline',\n variableId: 'user-online-status',\n immutable: true\n }\n ]\n }\n};\n```\n\n### Immutable Bindings\n\nImmutable bindings provide several benefits:\n\n- **System consistency** - Critical data cannot be accidentally unbound\n- **Security** - User permissions and IDs remain locked\n- **Branding** - Company logos and colors stay consistent\n- **Template integrity** - Essential bindings are preserved\n\n```tsx\n// Example: Brand-consistent button component\nconst BrandButton = {\n component: Button,\n schema: z.object({\n children: z.string(),\n style: z.object({\n backgroundColor: z.string(),\n color: z.string()\n }).optional()\n }),\n defaultVariableBindings: [\n {\n propName: 'style.backgroundColor',\n variableId: 'brand-primary-color',\n immutable: true // Locked to brand colors\n },\n {\n propName: 'style.color',\n variableId: 'brand-text-color',\n immutable: true\n }\n // children prop is left unbound for flexibility\n ]\n};\n```\n\n## Variable Binding in Code Generation\n\nWhen generating React code, variable bindings are converted to prop references:\n\n```tsx\n// Original component layer with bindings\nconst buttonLayer = {\n type: 'Button',\n props: {\n children: { __variableRef: 'button-text' },\n disabled: { __variableRef: 'is-disabled' },\n variant: 'primary' // Static value\n }\n};\n\n// Generated React code\ninterface PageProps {\n variables: {\n buttonText: string;\n isDisabled: boolean;\n };\n}\n\nconst Page = ({ variables }: PageProps) => {\n return (\n \n {variables.buttonText}\n \n );\n};\n```\n\n## Managing Variable Bindings Programmatically\n\n### Using Layer Store Methods\n\n```tsx\nimport { useLayerStore } from '@/lib/ui-builder/store/layer-store';\n\nfunction CustomBindingControl() {\n const bindPropToVariable = useLayerStore((state) => state.bindPropToVariable);\n const unbindPropFromVariable = useLayerStore((state) => state.unbindPropFromVariable);\n const isBindingImmutable = useLayerStore((state) => state.isBindingImmutable);\n\n const handleBind = () => {\n // Bind a component's 'title' prop to a variable\n bindPropToVariable('button-123', 'title', 'page-title-var');\n };\n\n const handleUnbind = () => {\n // Check if binding is immutable first\n if (!isBindingImmutable('button-123', 'title')) {\n unbindPropFromVariable('button-123', 'title');\n }\n };\n\n return (\n \n Bind Title \n Unbind Title \n
\n );\n}\n```\n\n### Variable Reference Detection\n\n```tsx\nimport { isVariableReference } from '@/lib/ui-builder/utils/variable-resolver';\n\n// Check if a prop value is a variable reference\nconst propValue = layer.props.children;\n\nif (isVariableReference(propValue)) {\n console.log('Bound to variable:', propValue.__variableRef);\n} else {\n console.log('Static value:', propValue);\n}\n```\n\n## Advanced Binding Patterns\n\n### Conditional Property Binding\n\n```tsx\n// Use boolean variables to control component behavior\nconst variables = [\n {\n id: 'show-avatar',\n name: 'showAvatar',\n type: 'boolean',\n defaultValue: true\n },\n {\n id: 'user-role',\n name: 'userRole',\n type: 'string',\n defaultValue: 'user'\n }\n];\n\n// Bind to component properties\nconst userCard = {\n type: 'UserCard',\n props: {\n showAvatar: { __variableRef: 'show-avatar' },\n role: { __variableRef: 'user-role' }\n }\n};\n```\n\n### Multi-Component Binding\n\n```tsx\n// Bind the same variable to multiple components\nconst themeVariable = {\n id: 'current-theme',\n name: 'currentTheme',\n type: 'string',\n defaultValue: 'light'\n};\n\n// Multiple components can reference the same variable\nconst header = {\n type: 'Header',\n props: {\n theme: { __variableRef: 'current-theme' }\n }\n};\n\nconst sidebar = {\n type: 'Sidebar',\n props: {\n theme: { __variableRef: 'current-theme' }\n }\n};\n\nconst footer = {\n type: 'Footer',\n props: {\n theme: { __variableRef: 'current-theme' }\n }\n};\n```\n\n## Variable Binding Best Practices\n\n### Design Patterns\n\n- **Use meaningful variable names** that clearly indicate their purpose\n- **Group related variables** (e.g., user data, theme settings, feature flags)\n- **Set appropriate default values** for better editor preview experience\n- **Document variable purposes** in component registry definitions\n- **Use immutable bindings** for system-critical or brand-related data\n\n### Performance Considerations\n\n- **Variable resolution is optimized** through memoization in the rendering process\n- **Only bound properties** are processed during variable resolution\n- **Static values** are passed through without processing overhead\n- **Variable updates** trigger efficient re-renders only for affected components\n\n### Debugging Tips\n\n```tsx\n// Check variable bindings in browser dev tools\nconst layer = useLayerStore.getState().findLayerById('my-component');\nconsole.log('Layer props:', layer?.props);\n\n// Verify variable resolution\nimport { resolveVariableReferences } from '@/lib/ui-builder/utils/variable-resolver';\n\nconst resolved = resolveVariableReferences(\n layer.props,\n variables,\n variableValues\n);\nconsole.log('Resolved props:', resolved);\n```\n\n## Troubleshooting Common Issues\n\n### Variable Not Found\n\n- **Check variable ID** matches exactly in both definition and reference\n- **Verify variable exists** in the variables array\n- **Ensure variable scope** (editor vs. renderer) includes the needed variable\n\n### Binding Not Working\n\n- **Confirm variable reference format** uses `{ __variableRef: 'variable-id' }`\n- **Check variable type compatibility** with component prop expectations\n- **Verify component schema** allows the property to be bound\n\n### Immutable Binding Issues\n\n- **Check defaultVariableBindings** configuration in component registry\n- **Verify immutable flag** is set correctly for auto-bound properties\n- **Use layer store methods** to check binding immutability programmatically"
+ "children": "## How Variable Binding Works\n\nVariable binding replaces static property values with dynamic references. When bound, a component property stores a variable reference object:\n\n```tsx\n// Before binding - static value\nconst button = {\n props: {\n children: 'Click me',\n disabled: false\n }\n};\n\n// After binding - variable references \nconst button = {\n props: {\n children: { __variableRef: 'button-text-var' },\n disabled: { __variableRef: 'is-loading-var' }\n }\n};\n```\n\n💡 **See it in action**: The demo above shows variable bindings with real-time value resolution from the [working example](/examples/renderer/variables).\n\n## Binding Variables Through the UI\n\n### Step-by-Step Binding Process\n\n1. **Select a component** in the editor canvas\n2. **Open the Properties panel** (right sidebar) \n3. **Find the property** you want to bind\n4. **Click the link icon** (🔗) next to the property field\n5. **Choose a variable** from the dropdown menu\n6. **The property is now bound** and shows the variable info\n\n### Visual Indicators in the Props Panel\n\nBound properties are visually distinct:\n\n- **Link icon** (🔗) indicates the property supports binding\n- **Variable card** displays when a property is bound\n- **Variable name and type** are shown (e.g., `userName` • `string`)\n- **Current value** shows the variable's resolved value\n- **Unlink button** (🔗⛌) allows unbinding (if not immutable)\n- **Lock icon** (🔒) indicates immutable bindings that cannot be changed\n\n### Unbinding Variables\n\nTo remove a variable binding:\n\n1. **Select the component** with bound properties\n2. **Find the bound property** in the props panel\n3. **Click the unlink icon** next to the variable card\n4. **Property reverts** to its schema default value\n\n**Note**: Immutable bindings (🔒) cannot be unbound through the UI.\n\n## Working Example: Variable Bindings in Action\n\nHere's the actual structure from our live demo showing real variable bindings:\n\n```tsx\n// Page structure with variable bindings\nconst page: ComponentLayer = {\n id: \"variables-demo-page\",\n type: \"div\",\n props: {\n className: \"max-w-4xl mx-auto p-8 space-y-8\"\n },\n children: [\n {\n id: \"page-title\",\n type: \"h1\", \n props: {\n className: \"text-4xl font-bold text-gray-900\",\n children: { __variableRef: \"pageTitle\" } // ← Variable binding\n }\n },\n {\n id: \"user-name\",\n type: \"span\",\n props: {\n className: \"text-gray-900\",\n children: { __variableRef: \"userName\" } // ← Another binding\n }\n },\n {\n id: \"primary-button\",\n type: \"Button\",\n props: {\n variant: \"default\",\n children: { __variableRef: \"buttonText\" }, // ← Button text binding\n disabled: { __variableRef: \"isLoading\" } // ← Boolean binding\n }\n }\n ]\n};\n\n// Variables that match the bindings\nconst variables: Variable[] = [\n {\n id: \"pageTitle\",\n name: \"Page Title\",\n type: \"string\",\n defaultValue: \"UI Builder Variables Demo\"\n },\n {\n id: \"userName\",\n name: \"User Name\", \n type: \"string\",\n defaultValue: \"John Doe\"\n },\n {\n id: \"buttonText\",\n name: \"Primary Button Text\",\n type: \"string\", \n defaultValue: \"Click Me!\"\n },\n {\n id: \"isLoading\",\n name: \"Loading State\",\n type: \"boolean\",\n defaultValue: false\n }\n];\n```\n\n## Automatic Variable Binding\n\n### Default Variable Bindings\n\nComponents can automatically bind to variables when added to the canvas:\n\n```tsx\nconst componentRegistry = {\n UserProfile: {\n component: UserProfile,\n schema: z.object({\n userId: z.string().default(\"user_123\"),\n displayName: z.string().default(\"John Doe\"),\n email: z.string().email().default(\"john@example.com\")\n }),\n from: \"@/components/ui/user-profile\",\n defaultVariableBindings: [\n {\n propName: \"userId\",\n variableId: \"current_user_id\",\n immutable: true // Cannot be unbound\n },\n {\n propName: \"displayName\", \n variableId: \"current_user_name\",\n immutable: false // Can be changed\n }\n ]\n }\n};\n```\n\n### Immutable Bindings\n\nImmutable bindings prevent accidental unbinding of critical data:\n\n- **System data**: User IDs, tenant IDs, session info\n- **Security**: Permissions, access levels, authentication state\n- **Branding**: Company logos, colors, brand consistency\n- **Template integrity**: Essential bindings in white-label scenarios\n\n```tsx\n// Example: Brand-consistent component with locked bindings\nconst BrandedButton = {\n component: Button,\n schema: z.object({\n text: z.string().default(\"Click Me\"),\n brandColor: z.string().default(\"#3b82f6\"),\n companyName: z.string().default(\"Acme Corp\")\n }),\n defaultVariableBindings: [\n {\n propName: \"brandColor\",\n variableId: \"company_brand_color\",\n immutable: true // 🔒 Locked to maintain brand consistency\n },\n {\n propName: \"companyName\",\n variableId: \"company_name\", \n immutable: true // 🔒 Company identity protected\n }\n // text prop left unbound for content flexibility\n ]\n};\n```\n\n## Variable Resolution\n\nAt runtime, variable references are resolved to actual values:\n\n```tsx\n// Variable reference in component props\nconst buttonProps = {\n children: { __variableRef: 'welcome-message' },\n disabled: { __variableRef: 'is-loading' }\n};\n\n// Variables definition\nconst variables = [\n {\n id: 'welcome-message',\n name: 'welcomeMessage',\n type: 'string',\n defaultValue: 'Welcome!'\n },\n {\n id: 'is-loading',\n name: 'isLoading', \n type: 'boolean',\n defaultValue: false\n }\n];\n\n// Runtime values override defaults\nconst variableValues = {\n 'welcome-message': 'Hello, Jane!',\n 'is-loading': true\n};\n\n// Resolution process:\n// 1. Find variable by ID → 'welcome-message'\n// 2. Use runtime value if provided → 'Hello, Jane!'\n// 3. Fall back to default if no runtime value → 'Welcome!'\n// 4. Final resolved props: { children: 'Hello, Jane!', disabled: true }\n```\n\n> 📚 **See More**: Learn about [Data Binding](/docs/data-binding) for external data integration and [Rendering Pages](/docs/rendering-pages) for LayerRenderer usage.\n\n## Managing Bindings Programmatically\n\n### Using Layer Store Methods\n\n```tsx\nimport { useLayerStore } from '@/lib/ui-builder/store/layer-store';\n\nfunction CustomBindingControl() {\n const bindPropToVariable = useLayerStore((state) => state.bindPropToVariable);\n const unbindPropFromVariable = useLayerStore((state) => state.unbindPropFromVariable);\n const isBindingImmutable = useLayerStore((state) => state.isBindingImmutable);\n\n const handleBind = () => {\n // Bind a component's 'title' prop to a variable\n bindPropToVariable('button-123', 'title', 'page-title-var');\n };\n\n const handleUnbind = () => {\n // Check if binding is immutable first\n if (!isBindingImmutable('button-123', 'title')) {\n unbindPropFromVariable('button-123', 'title');\n }\n };\n\n return (\n \n Bind Title \n Unbind Title \n
\n );\n}\n```\n\n### Variable Reference Detection\n\n```tsx\nimport { isVariableReference } from '@/lib/ui-builder/utils/variable-resolver';\n\n// Check if a prop value is a variable reference\nconst propValue = layer.props.children;\n\nif (isVariableReference(propValue)) {\n console.log('Bound to variable:', propValue.__variableRef);\n} else {\n console.log('Static value:', propValue);\n}\n```\n\n## Binding Best Practices\n\n### Design Patterns\n\n- **Use meaningful variable names** that clearly indicate their purpose\n- **Set appropriate default values** for better editor preview experience \n- **Use immutable bindings** for system-critical or brand-related data\n- **Group related variables** with consistent naming patterns\n- **Bind the same variable** to multiple components for consistency\n\n### UI/UX Considerations\n\n- **Visual indicators** help users understand which properties are bound\n- **Immutable bindings** should be clearly marked to avoid user confusion\n- **Unbinding** should revert to sensible default values from the schema\n- **Variable cards** provide clear information about bound variables\n\n### Performance Tips\n\n- **Variable resolution is optimized** through memoization in the rendering process\n- **Only bound properties** are processed during variable resolution\n- **Static values** pass through without processing overhead\n- **Variable updates** trigger efficient re-renders only for affected components\n\n## Troubleshooting Binding Issues\n\n### Variable Not Found\n- **Check variable ID** matches exactly in both definition and reference\n- **Verify variable exists** in the variables array\n- **Ensure variable scope** includes the needed variable in your context\n\n### Binding Not Working\n- **Confirm variable reference format** uses `{ __variableRef: 'variable-id' }`\n- **Check variable type compatibility** with component prop expectations\n- **Verify component schema** allows the property to be bound\n\n### Immutable Binding Issues\n- **Check defaultVariableBindings** configuration in component registry\n- **Verify immutable flag** is set correctly for auto-bound properties\n- **Use layer store methods** to check binding immutability programmatically\n\n```tsx\n// Debug variable bindings in browser dev tools\nconst layer = useLayerStore.getState().findLayerById('my-component');\nconsole.log('Layer props:', layer?.props);\n\n// Verify variable resolution\nimport { resolveVariableReferences } from '@/lib/ui-builder/utils/variable-resolver';\n\nconst resolved = resolveVariableReferences(\n layer.props,\n variables,\n variableValues\n);\nconsole.log('Resolved props:', resolved);\n```\n\n> 🔄 **Next Steps**: Now that you understand variable binding mechanics, explore [Data Binding](/docs/data-binding) to connect external data sources and [Variables](/docs/variables) for variable management fundamentals."
}
]
} as const satisfies ComponentLayer;
\ No newline at end of file
diff --git a/app/docs/docs-data/docs-page-layers/variables-panel.ts b/app/docs/docs-data/docs-page-layers/variables-panel.ts
deleted file mode 100644
index 230fb06..0000000
--- a/app/docs/docs-data/docs-page-layers/variables-panel.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { ComponentLayer } from "@/components/ui/ui-builder/types";
-
-export const VARIABLES_PANEL_LAYER = {
- "id": "variables-panel",
- "type": "div",
- "name": "Variables Panel",
- "props": {
- "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen",
- "data-group": "editor-features"
- },
- "children": [
- {
- "type": "span",
- "children": "Variables Panel",
- "id": "variables-panel-title",
- "name": "Text",
- "props": {
- "className": "text-4xl"
- }
- },
- {
- "id": "variables-panel-intro",
- "type": "Markdown",
- "name": "Markdown",
- "props": {},
- "children": "The variables panel provides a visual interface for creating and managing dynamic data in your layouts. Define variables that can be bound to component properties for data-driven, personalized interfaces."
- },
- {
- "id": "variables-panel-demo",
- "type": "div",
- "name": "div",
- "props": {},
- "children": [
- {
- "id": "variables-panel-badge",
- "type": "Badge",
- "name": "Badge",
- "props": {
- "variant": "default",
- "className": "rounded rounded-b-none"
- },
- "children": [
- {
- "id": "variables-panel-badge-text",
- "type": "span",
- "name": "span",
- "props": {},
- "children": "Live Example"
- }
- ]
- },
- {
- "id": "variables-panel-demo-frame",
- "type": "div",
- "name": "div",
- "props": {
- "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden"
- },
- "children": [
- {
- "id": "variables-panel-iframe",
- "type": "iframe",
- "name": "iframe",
- "props": {
- "src": "http://localhost:3000/examples/renderer/variables",
- "title": "Variables Panel Demo",
- "className": "aspect-square md:aspect-video w-full"
- },
- "children": []
- }
- ]
- }
- ]
- },
- {
- "id": "variables-panel-content",
- "type": "Markdown",
- "name": "Markdown",
- "props": {},
- "children": "## Variable Types\n\nUI Builder supports three core variable types:\n\n```tsx\n// String variable for text content\n{\n id: 'user-name',\n name: 'userName',\n type: 'string',\n defaultValue: 'John Doe'\n}\n\n// Number variable for counts, prices, etc.\n{\n id: 'product-count',\n name: 'productCount', \n type: 'number',\n defaultValue: 42\n}\n\n// Boolean variable for toggles, features flags\n{\n id: 'show-header',\n name: 'showHeader',\n type: 'boolean',\n defaultValue: true\n}\n```\n\n## Creating Variables\n\n### Through UI Builder Props\n\n```tsx\nconst initialVariables = [\n {\n id: 'welcome-msg',\n name: 'welcomeMessage',\n type: 'string',\n defaultValue: 'Welcome to our site!'\n },\n {\n id: 'user-age',\n name: 'userAge',\n type: 'number', \n defaultValue: 25\n },\n {\n id: 'is-premium',\n name: 'isPremiumUser',\n type: 'boolean',\n defaultValue: false\n }\n];\n\n {\n // Save variables to your backend\n saveVariables(variables);\n }}\n allowVariableEditing={true} // Allow users to edit variables\n/>\n```\n\n### Through the Variables Panel\n\nUsers can create variables directly in the editor:\n\n1. **Click \"Add Variable\"** in the variables panel\n2. **Choose variable type** (string, number, boolean)\n3. **Set name and default value**\n4. **Variable is immediately available** for binding\n\n## Variable Binding\n\n### Binding to Component Properties\n\nBind variables to any component property in the props panel:\n\n```tsx\n// Component with variable binding\nconst buttonComponent = {\n id: 'welcome-button',\n type: 'Button',\n props: {\n children: { __variableRef: 'welcome-msg' }, // Bound to variable\n disabled: { __variableRef: 'is-premium' }, // Boolean binding\n className: 'px-4 py-2' // Static value\n }\n};\n```\n\n### Variable Reference Format\n\nVariable bindings use a special reference format:\n\n```tsx\n// Variable reference object\n{ __variableRef: 'variable-id' }\n\n// Examples\nprops: {\n title: { __variableRef: 'page-title' }, // String variable\n count: { __variableRef: 'item-count' }, // Number variable\n visible: { __variableRef: 'show-banner' } // Boolean variable\n}\n```\n\n## Default Variable Bindings\n\nComponents can automatically bind to variables when added:\n\n```tsx\nconst UserProfile = {\n component: UserProfile,\n schema: z.object({\n name: z.string().default(''),\n email: z.string().default(''),\n avatar: z.string().optional()\n }),\n from: '@/components/user-profile',\n // Automatically bind these props to variables\n defaultVariableBindings: [\n { \n propName: 'name', \n variableId: 'current-user-name',\n immutable: false // Can be unbound by user\n },\n { \n propName: 'email', \n variableId: 'current-user-email',\n immutable: true // Cannot be unbound\n }\n ]\n};\n```\n\n## Variable Resolution\n\n### Runtime Values\n\nWhen rendering with `LayerRenderer`, override variable values:\n\n```tsx\n// Variables defined in editor\nconst editorVariables = [\n { id: 'user-name', name: 'userName', type: 'string', defaultValue: 'Guest' },\n { id: 'user-age', name: 'userAge', type: 'number', defaultValue: 0 }\n];\n\n// Runtime variable values\nconst runtimeValues = {\n 'user-name': 'Alice Johnson', // Override with real user data\n 'user-age': 28\n};\n\n \n```\n\n### Variable Resolution Process\n\n1. **Editor displays** default values during editing\n2. **Renderer uses** runtime values when provided\n3. **Falls back** to default values if runtime value missing\n4. **Type safety** ensures values match variable types\n\n## Use Cases\n\n### Personalized Content\n\n```tsx\n// User-specific variables\nconst userVariables = [\n { id: 'user-first-name', name: 'firstName', type: 'string', defaultValue: 'User' },\n { id: 'user-last-name', name: 'lastName', type: 'string', defaultValue: '' },\n { id: 'user-points', name: 'loyaltyPoints', type: 'number', defaultValue: 0 }\n];\n\n// Components bound to user data\nconst welcomeSection = {\n type: 'div',\n props: { className: 'welcome-section' },\n children: [\n {\n type: 'span',\n props: {\n children: { __variableRef: 'user-first-name' }\n }\n }\n ]\n};\n```\n\n### Feature Flags\n\n```tsx\n// Boolean variables for feature toggles\nconst featureFlags = [\n { id: 'show-beta-features', name: 'showBetaFeatures', type: 'boolean', defaultValue: false },\n { id: 'enable-notifications', name: 'enableNotifications', type: 'boolean', defaultValue: true }\n];\n\n// Conditionally show components\nconst betaFeature = {\n type: 'div',\n props: {\n className: 'beta-feature',\n style: { \n display: { __variableRef: 'show-beta-features' } ? 'block' : 'none'\n }\n }\n};\n```\n\n### Multi-Tenant Branding\n\n```tsx\n// Brand-specific variables\nconst brandVariables = [\n { id: 'company-name', name: 'companyName', type: 'string', defaultValue: 'Your Company' },\n { id: 'brand-color', name: 'primaryColor', type: 'string', defaultValue: '#3b82f6' },\n { id: 'logo-url', name: 'logoUrl', type: 'string', defaultValue: '/default-logo.png' }\n];\n\n// Components using brand variables\nconst header = {\n type: 'header',\n children: [\n {\n type: 'img',\n props: {\n src: { __variableRef: 'logo-url' },\n alt: { __variableRef: 'company-name' }\n }\n }\n ]\n};\n```\n\n## Variable Management\n\n### Panel Controls\n\n- **Add Variable** - Create new variables\n- **Edit Variable** - Modify name, type, or default value\n- **Delete Variable** - Remove unused variables\n- **Search Variables** - Find variables by name\n\n### Variable Validation\n\n- **Unique names** - Prevent duplicate variable names\n- **Type checking** - Ensure values match declared types\n- **Usage tracking** - Show which components use each variable\n- **Orphan detection** - Identify unused variables\n\n### Variable Panel Configuration\n\n```tsx\n { // Handle variable changes\n console.log('Variables updated:', vars);\n }}\n/>\n```\n\n## Best Practices\n\n### Naming Conventions\n- **Use camelCase** for variable names (`userName`, not `user_name`)\n- **Be descriptive** (`currentUserEmail` vs `email`)\n- **Group related variables** (`user*`, `brand*`, `feature*`)\n\n### Type Selection\n- **Use strings** for text, URLs, IDs, and enum-like values\n- **Use numbers** for counts, measurements, and calculations \n- **Use booleans** for flags, toggles, and conditional display\n\n### Organization\n- **Start with core variables** that many components will use\n- **Group by purpose** (user data, branding, features)\n- **Document variable purpose** in your codebase\n- **Plan for runtime data** structure when designing variables\n\n### Performance\n- **Avoid excessive variables** that aren't actually needed\n- **Use immutable bindings** for system-level data\n- **Cache runtime values** when possible to reduce re-renders"
- }
- ]
- } as const satisfies ComponentLayer;
\ No newline at end of file
diff --git a/app/docs/docs-data/docs-page-layers/variables.ts b/app/docs/docs-data/docs-page-layers/variables.ts
index e2f4198..841b01e 100644
--- a/app/docs/docs-data/docs-page-layers/variables.ts
+++ b/app/docs/docs-data/docs-page-layers/variables.ts
@@ -23,7 +23,7 @@ export const VARIABLES_LAYER = {
"type": "Markdown",
"name": "Markdown",
"props": {},
- "children": "Variables are typed data containers that enable dynamic, data-driven interfaces in UI Builder. They allow you to bind component properties to values that can change at runtime, enabling personalization, theming, and reusable templates."
+ "children": "**Variables are the key to creating dynamic, data-driven interfaces with UI Builder.** Instead of hardcoding static values into your components, variables allow you to bind component properties to dynamic data that can change at runtime.\n\nThis transforms static designs into powerful applications with:\n- **Personalized content** that adapts to user data\n- **Reusable templates** that work across different contexts \n- **Multi-tenant applications** with customized branding per client\n- **A/B testing** and feature flags through boolean variables"
},
{
"id": "variables-demo",
@@ -62,7 +62,7 @@ export const VARIABLES_LAYER = {
"type": "iframe",
"name": "iframe",
"props": {
- "src": "http://localhost:3000/examples/renderer/variables",
+ "src": "/examples/renderer/variables",
"title": "Variables Demo",
"className": "aspect-square md:aspect-video w-full"
},
@@ -77,7 +77,7 @@ export const VARIABLES_LAYER = {
"type": "Markdown",
"name": "Markdown",
"props": {},
- "children": "## Variable Types\n\nUI Builder supports three primitive variable types:\n\n```tsx\n// String variable for text content\nconst userNameVar: Variable = {\n id: 'user-name-var',\n name: 'userName',\n type: 'string',\n defaultValue: 'John Doe'\n};\n\n// Number variable for counts, prices, quantities\nconst itemCountVar: Variable = {\n id: 'item-count-var',\n name: 'itemCount', \n type: 'number',\n defaultValue: 42\n};\n\n// Boolean variable for feature flags, toggles\nconst showBannerVar: Variable = {\n id: 'show-banner-var',\n name: 'showBanner',\n type: 'boolean',\n defaultValue: true\n};\n```\n\n## Creating Variables\n\n### Through UIBuilder Props\n\nDefine initial variables when initializing the editor:\n\n```tsx\nimport UIBuilder from '@/components/ui/ui-builder';\nimport { Variable } from '@/components/ui/ui-builder/types';\n\nconst initialVariables: Variable[] = [\n {\n id: 'welcome-msg',\n name: 'welcomeMessage',\n type: 'string',\n defaultValue: 'Welcome to our site!'\n },\n {\n id: 'user-age',\n name: 'userAge',\n type: 'number', \n defaultValue: 25\n },\n {\n id: 'is-premium',\n name: 'isPremiumUser',\n type: 'boolean',\n defaultValue: false\n }\n];\n\nfunction App() {\n return (\n {\n // Save variables to your backend\n console.log('Variables updated:', variables);\n }}\n allowVariableEditing={true} // Allow users to edit variables\n componentRegistry={myComponentRegistry}\n />\n );\n}\n```\n\n### Through the Variables Panel\n\nUsers can create and manage variables directly in the editor:\n\n1. **Navigate to the \"Data\" tab** in the left panel\n2. **Click \"Add Variable\"** to create a new variable\n3. **Choose variable type** (string, number, boolean)\n4. **Set name and default value**\n5. **Variable is immediately available** for binding in the props panel\n\n## Variable Binding\n\n### Binding Through Props Panel\n\nVariables are bound to component properties through the UI:\n\n1. **Select a component** in the editor\n2. **Open the props panel** (right panel)\n3. **Click the link icon** next to any property field\n4. **Choose a variable** from the dropdown menu\n5. **The property is now bound** to the variable\n\n### Binding Structure\n\nWhen bound, component props store a variable reference:\n\n```tsx\n// Internal structure when a prop is bound to a variable\nconst buttonLayer: ComponentLayer = {\n id: 'my-button',\n type: 'Button',\n props: {\n children: { __variableRef: 'welcome-msg' }, // Bound to variable\n disabled: { __variableRef: 'is-loading' }, // Bound to variable\n variant: 'default' // Static value\n },\n children: []\n};\n```\n\n### Immutable Bindings\n\nUse `defaultVariableBindings` to automatically bind variables when components are added:\n\n```tsx\nconst componentRegistry = {\n UserProfile: {\n component: UserProfile,\n schema: z.object({\n userId: z.string(),\n displayName: z.string(),\n }),\n from: '@/components/ui/user-profile',\n // Automatically bind user data when component is added\n defaultVariableBindings: [\n { \n propName: 'userId', \n variableId: 'current-user-id', \n immutable: true // Cannot be unbound in UI\n },\n { \n propName: 'displayName', \n variableId: 'current-user-name', \n immutable: false // Can be changed by users\n }\n ]\n }\n};\n```\n\n## Runtime Variable Resolution\n\n### In LayerRenderer\n\nVariables are resolved when rendering pages:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\nimport { Variable } from '@/components/ui/ui-builder/types';\n\n// Define variables\nconst variables: Variable[] = [\n {\n id: 'user-name',\n name: 'userName',\n type: 'string',\n defaultValue: 'Anonymous'\n },\n {\n id: 'user-age',\n name: 'userAge',\n type: 'number',\n defaultValue: 0\n }\n];\n\n// Override variable values at runtime\nconst variableValues = {\n 'user-name': 'Jane Smith',\n 'user-age': 30\n};\n\nfunction MyPage() {\n return (\n \n );\n}\n```\n\n### Variable Resolution Process\n\n1. **Component props are scanned** for variable references\n2. **Variable references are resolved** using provided `variableValues` or defaults\n3. **Resolved values are passed** to React components\n4. **Components render** with dynamic data\n\n## Managing Variables\n\n### Read-Only Variables\n\nControl whether users can edit variables in the UI:\n\n```tsx\n\n```\n\n### Variable Change Handling\n\nRespond to variable changes in the editor:\n\n```tsx\nfunction App() {\n const handleVariablesChange = (variables: Variable[]) => {\n // Persist to backend\n fetch('/api/variables', {\n method: 'POST',\n body: JSON.stringify(variables)\n });\n };\n\n return (\n \n );\n}\n```\n\n## Code Generation\n\nVariables are included in generated React code:\n\n```tsx\n// Generated component with variables\ninterface PageProps {\n variables: {\n userName: string;\n userAge: number;\n showWelcome: boolean;\n };\n}\n\nconst Page = ({ variables }: PageProps) => {\n return (\n \n \n {variables.userName}\n \n Age: {variables.userAge} \n
\n );\n};\n```\n\n## Use Cases\n\n### Personalization\n\n```tsx\n// Variables for user-specific content\nconst userVariables = [\n { id: 'user-name', name: 'userName', type: 'string', defaultValue: 'User' },\n { id: 'user-avatar', name: 'userAvatar', type: 'string', defaultValue: '/default-avatar.png' },\n { id: 'is-premium', name: 'isPremiumUser', type: 'boolean', defaultValue: false }\n];\n```\n\n### Feature Flags\n\n```tsx\n// Variables for conditional features\nconst featureFlags = [\n { id: 'show-beta-feature', name: 'showBetaFeature', type: 'boolean', defaultValue: false },\n { id: 'enable-dark-mode', name: 'enableDarkMode', type: 'boolean', defaultValue: true }\n];\n```\n\n### Multi-tenant Branding\n\n```tsx\n// Variables for client-specific branding\nconst brandingVariables = [\n { id: 'company-name', name: 'companyName', type: 'string', defaultValue: 'Acme Corp' },\n { id: 'primary-color', name: 'primaryColor', type: 'string', defaultValue: '#3b82f6' },\n { id: 'logo-url', name: 'logoUrl', type: 'string', defaultValue: '/default-logo.png' }\n];\n```\n\n### Dynamic Content\n\n```tsx\n// Variables for content management\nconst contentVariables = [\n { id: 'page-title', name: 'pageTitle', type: 'string', defaultValue: 'Welcome' },\n { id: 'product-count', name: 'productCount', type: 'number', defaultValue: 0 },\n { id: 'show-special-offer', name: 'showSpecialOffer', type: 'boolean', defaultValue: false }\n];\n```\n\n## Best Practices\n\n- **Use descriptive names** for variables (e.g., `userName` not `u`)\n- **Choose appropriate types** for your data\n- **Set meaningful default values** for better preview experience\n- **Use immutable bindings** for system-critical data\n- **Group related variables** with consistent naming patterns\n- **Document variable purposes** in your component registry"
+ "children": "## Variable Types\n\nUI Builder supports three typed variables:\n\n```tsx\ninterface Variable {\n id: string; // Unique identifier\n name: string; // Display name (becomes property name in generated code)\n type: 'string' | 'number' | 'boolean';\n defaultValue: string | number | boolean; // Must match the type\n}\n\n// Examples:\nconst stringVar: Variable = {\n id: 'page-title',\n name: 'pageTitle',\n type: 'string',\n defaultValue: 'Welcome to UI Builder'\n};\n\nconst numberVar: Variable = {\n id: 'user-age',\n name: 'userAge', \n type: 'number',\n defaultValue: 25\n};\n\nconst booleanVar: Variable = {\n id: 'is-loading',\n name: 'isLoading',\n type: 'boolean',\n defaultValue: false\n};\n```\n\n💡 **See it in action**: The demo above shows all three types with real-time variable binding and runtime value overrides.\n\n## Creating Variables\n\n### Via Initial Variables Prop\n\nSet up variables when initializing the UIBuilder:\n\n```tsx\nimport UIBuilder from '@/components/ui/ui-builder';\nimport { Variable } from '@/components/ui/ui-builder/types';\n\nconst initialVariables: Variable[] = [\n {\n id: 'welcome-msg',\n name: 'welcomeMessage',\n type: 'string',\n defaultValue: 'Welcome to our site!'\n },\n {\n id: 'user-count',\n name: 'userCount',\n type: 'number', \n defaultValue: 0\n },\n {\n id: 'show-banner',\n name: 'showBanner',\n type: 'boolean',\n defaultValue: true\n }\n];\n\nfunction App() {\n return (\n {\n // Persist variable definitions to your backend\n console.log('Variables updated:', variables);\n }}\n />\n );\n}\n```\n\n### Via the Data Panel\n\nUsers can create variables directly in the editor:\n\n1. **Navigate to the \"Data\" tab** in the left panel\n2. **Click \"Add Variable\"** to create a new variable\n3. **Choose variable type** (string, number, boolean)\n4. **Set name and default value**\n5. **Variable is immediately available** for binding in the props panel\n\n## Using Variables\n\nVariables can be bound to component properties in two ways:\n\n### Manual Binding\nUsers can bind variables to component properties in the props panel by clicking the link icon next to any field.\n\n### Automatic Binding \nComponents can be configured to automatically bind to specific variables when added:\n\n```tsx\nconst componentRegistry = {\n UserProfile: {\n component: UserProfile,\n schema: z.object({\n userId: z.string(),\n displayName: z.string(),\n }),\n from: '@/components/ui/user-profile',\n // Automatically bind user data when component is added\n defaultVariableBindings: [\n { \n propName: 'userId', \n variableId: 'current-user-id', \n immutable: true // Cannot be unbound in UI\n },\n { \n propName: 'displayName', \n variableId: 'current-user-name', \n immutable: false // Can be changed by users\n }\n ]\n }\n};\n```\n\n**Immutable bindings** prevent users from unbinding critical variables for system data, branding consistency, and template integrity.\n\n> 💡 **Learn more**: See [Variable Binding](/docs/variable-binding) for detailed binding mechanics and [Data Binding](/docs/data-binding) for connecting to external data sources.\n\n## Variable Management\n\n### Controlling Variable Editing\n\nControl whether users can edit variables in the UI:\n\n```tsx\n\n```\n\nWhen `allowVariableEditing` is `false`:\n- Variables panel becomes read-only\n- \"Add Variable\" button is hidden\n- Edit/delete buttons on individual variables are hidden\n- Variable values can still be overridden at runtime during rendering\n\n### Variable Change Handling\n\nRespond to variable definition changes in the editor:\n\n```tsx\nfunction App() {\n const handleVariablesChange = (variables: Variable[]) => {\n // Persist variable definitions to backend\n fetch('/api/variables', {\n method: 'POST',\n body: JSON.stringify(variables)\n });\n };\n\n return (\n \n );\n}\n```\n\n## Common Use Cases\n\n### Personalization\n\n```tsx\n// Variables for user-specific content\nconst userVariables: Variable[] = [\n { id: 'user-name', name: 'userName', type: 'string', defaultValue: 'User' },\n { id: 'user-avatar', name: 'userAvatar', type: 'string', defaultValue: '/default-avatar.png' },\n { id: 'is-premium', name: 'isPremiumUser', type: 'boolean', defaultValue: false }\n];\n```\n\n### Feature Flags\n\n```tsx\n// Variables for conditional features\nconst featureFlags: Variable[] = [\n { id: 'show-beta-feature', name: 'showBetaFeature', type: 'boolean', defaultValue: false },\n { id: 'enable-dark-mode', name: 'enableDarkMode', type: 'boolean', defaultValue: true }\n];\n```\n\n### Multi-tenant Branding\n\n```tsx\n// Variables for client-specific branding\nconst brandingVariables: Variable[] = [\n { id: 'company-name', name: 'companyName', type: 'string', defaultValue: 'Acme Corp' },\n { id: 'primary-color', name: 'primaryColor', type: 'string', defaultValue: '#3b82f6' },\n { id: 'logo-url', name: 'logoUrl', type: 'string', defaultValue: '/default-logo.png' }\n];\n```\n\n## Best Practices\n\n- **Use descriptive names** for variables (e.g., `userName` not `u`)\n- **Choose appropriate types** for your data (string for text, number for counts, boolean for flags)\n- **Set meaningful default values** for better preview experience in the editor\n- **Use immutable bindings** for system-critical data that shouldn't be unbound\n- **Group related variables** with consistent naming patterns\n- **Keep variable names simple** - they become property names in generated code\n- **Separate variable definitions from values** - define structure in the editor, inject data at runtime\n\n## Variable Workflow Summary\n\n1. **Define Variables**: Create variable definitions in the editor or via `initialVariables`\n2. **Bind to Components**: Link component properties to variables in the props panel\n3. **Save Structure**: Store the page structure and variable definitions (via `onChange` and `onVariablesChange`)\n4. **Render with Data**: Use `LayerRenderer` with `variableValues` to inject real data at runtime\n\nThis workflow enables the separation of content structure from actual data, making your UI Builder pages truly dynamic and reusable.\n\n> 📚 **Next Steps**: Learn about [Variable Binding](/docs/variable-binding) for detailed binding mechanics, [Data Binding](/docs/data-binding) for external data integration, and [Rendering Pages](/docs/rendering-pages) for runtime usage."
}
]
} as const satisfies ComponentLayer;
\ No newline at end of file
diff --git a/app/docs/layout.tsx b/app/docs/layout.tsx
index 4ed24c6..354ee66 100644
--- a/app/docs/layout.tsx
+++ b/app/docs/layout.tsx
@@ -1,9 +1,43 @@
+import { Suspense } from "react";
import { ThemeProvider } from "next-themes";
+import { AppSidebar } from "@/app/platform/app-sidebar";
+import {
+ SidebarInset,
+ SidebarProvider,
+ SidebarTrigger,
+} from "@/components/ui/sidebar";
+import { DocBreadcrumbs } from "../platform/doc-breadcrumbs";
-export default function DocsLayout({ children }: { children: React.ReactNode }) {
+export const metadata = {
+ title: "Documentation - UI Builder",
+ description: "Everything you need to know about building UIs with our drag-and-drop builder.",
+};
+
+export default function DocsLayout({
+ children
+}: {
+ children: React.ReactNode,
+}) {
return (
- {children}
+
+
+
+
+ {children}
+
+
);
+}
+
+function DocsHeader() {
+ return (
+
+ );
}
\ No newline at end of file
diff --git a/app/docs/page.tsx b/app/docs/page.tsx
new file mode 100644
index 0000000..26deecb
--- /dev/null
+++ b/app/docs/page.tsx
@@ -0,0 +1,59 @@
+import Link from "next/link";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { MENU_DATA } from "@/app/docs/docs-data/data";
+import { ArrowRightIcon } from "lucide-react";
+
+export default function DocsPage() {
+ return (
+
+
+
+
Documentation
+
+ Everything you need to know about building UIs with our drag-and-drop builder.
+
+
+
+
+ {MENU_DATA.map((section) => (
+
+
+ {section.title}
+
+ {getCardDescription(section.title)}
+
+
+
+
+ {section.items?.map((item) => (
+
+
{item.title}
+
+
+ ))}
+
+
+
+ ))}
+
+
+
+ );
+}
+
+function getCardDescription(title: string): string {
+ const descriptions: Record = {
+ "Core": "Get started with the fundamentals of the UI builder",
+ "Component System": "Learn about components, customization, and configuration",
+ "Editor Features": "Explore the powerful editor panels and features",
+ "Data & Variables": "Master data binding and variable management",
+ "Layout & Persistence": "Understand structure and state management",
+ "Rendering": "Learn how to render and theme your pages"
+ };
+
+ return descriptions[title] || "Explore this section";
+}
\ No newline at end of file
diff --git a/app/examples/editor/panel-config/page.tsx b/app/examples/editor/panel-config/page.tsx
new file mode 100644
index 0000000..3c505e6
--- /dev/null
+++ b/app/examples/editor/panel-config/page.tsx
@@ -0,0 +1,14 @@
+import { PanelConfigDemo } from "app/platform/panel-config-demo";
+
+export const metadata = {
+ title: "Panel Configuration - UI Builder",
+ description: "Demonstrates custom panel configuration options"
+};
+
+export default function Page() {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/examples/editor/read-only-mode/page.tsx b/app/examples/editor/read-only-mode/page.tsx
new file mode 100644
index 0000000..b97c09c
--- /dev/null
+++ b/app/examples/editor/read-only-mode/page.tsx
@@ -0,0 +1,14 @@
+import { ReadOnlyDemo } from "@/app/platform/read-only-demo";
+
+export const metadata = {
+ title: "UI Builder - Read-Only Mode Demo",
+ description: "Interactive demonstration of UI Builder's read-only mode capabilities"
+};
+
+export default function ReadOnlyModePage() {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
index 7e48beb..da38c44 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,6 +1,11 @@
/* eslint-disable @next/next/no-sync-scripts */
import "@/styles/globals.css";
+export const metadata = {
+ title: "UI Builder",
+ description: "An open source UI builder for building complex UIs",
+};
+
export default function RootLayout({
children,
}: {
diff --git a/app/page.tsx b/app/page.tsx
index 7b4d4f8..7c791bc 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,13 +1,58 @@
-import { SimpleBuilder } from "./platform/simple-builder";
+import Link from "next/link";
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+} from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { GithubIcon } from "lucide-react";
+import Image from "next/image";
-export const metadata = {
- title: "UI Builder",
-};
export default function Page() {
return (
-
-
+
+
+
+
+
+ UI Builder
+
+
+ Get started by exploring the documentation or trying out a basic
+ example.
+
+
+
+
+ View Documentation
+
+
+ Try Basic Example
+
+
+
+
+ View on GitHub
+
+
+
+
);
}
diff --git a/app/platform/app-sidebar.tsx b/app/platform/app-sidebar.tsx
index fc4450f..90121e2 100644
--- a/app/platform/app-sidebar.tsx
+++ b/app/platform/app-sidebar.tsx
@@ -1,26 +1,42 @@
+"use client"
+
import * as React from "react"
import Link from "next/link"
+import { usePathname } from "next/navigation"
import {
Sidebar,
SidebarContent,
+ SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
+ SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
} from "@/components/ui/sidebar"
import { MENU_DATA } from "@/app/docs/docs-data/data"
+import Image from "next/image";
+import { GithubIcon } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { ThemeToggle } from "@/app/platform/theme-toggle"
interface AppSidebarProps extends React.ComponentProps {
currentPath?: string;
}
-export function AppSidebar({ currentPath, ...props }: AppSidebarProps) {
+function AppSidebarContent({ currentPath, ...props }: AppSidebarProps) {
return (
+
+
+
+ UI Builder
+
+
+
{MENU_DATA.map((section) => (
@@ -45,7 +61,19 @@ export function AppSidebar({ currentPath, ...props }: AppSidebarProps) {
))}
+
+
+
+
+
+
+
)
}
+
+export function AppSidebar(props: Omit) {
+ const pathname = usePathname()
+ return
+}
diff --git a/app/platform/builder-drag-drop-test.tsx b/app/platform/builder-drag-drop-test.tsx
index 6207143..8544268 100644
--- a/app/platform/builder-drag-drop-test.tsx
+++ b/app/platform/builder-drag-drop-test.tsx
@@ -806,6 +806,7 @@ export const BuilderDragDropTest = () => {
allowPagesCreation={true}
allowPagesDeletion={true}
allowVariableEditing={true}
+ persistLayerStore={false}
/>
);
};
\ No newline at end of file
diff --git a/app/platform/builder-with-immutable-bindings.tsx b/app/platform/builder-with-immutable-bindings.tsx
index 8dfbf55..c4c9400 100644
--- a/app/platform/builder-with-immutable-bindings.tsx
+++ b/app/platform/builder-with-immutable-bindings.tsx
@@ -363,6 +363,7 @@ export const BuilderWithImmutableBindings = () => {
allowPagesCreation={true}
allowPagesDeletion={true}
allowVariableEditing={false}
+ persistLayerStore={false}
/>
);
};
\ No newline at end of file
diff --git a/app/platform/builder-with-pages.tsx b/app/platform/builder-with-pages.tsx
index a9a2ea3..4c5ef74 100644
--- a/app/platform/builder-with-pages.tsx
+++ b/app/platform/builder-with-pages.tsx
@@ -990,6 +990,7 @@ export const BuilderWithPages = ({fixedPages = false}: {fixedPages?: boolean}) =
}}
allowPagesCreation={!fixedPages}
allowPagesDeletion={!fixedPages}
+ persistLayerStore={false}
panelConfig={getDefaultPanelConfigValues(defaultConfigTabsContent())}
/>;
};
\ No newline at end of file
diff --git a/app/platform/doc-breadcrumbs.tsx b/app/platform/doc-breadcrumbs.tsx
new file mode 100644
index 0000000..e10ac6f
--- /dev/null
+++ b/app/platform/doc-breadcrumbs.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbList,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+} from "@/components/ui/breadcrumb";
+import { getBreadcrumbsFromUrl } from "@/app/docs/docs-data/data";
+import { usePathname } from "next/navigation";
+import { Button } from "@/components/ui/button";
+import Link from "next/link";
+import { PencilIcon } from "lucide-react";
+
+export function DocBreadcrumbs() {
+ const pathname = usePathname();
+ const slug = pathname.replace("/docs/", "");
+
+ const currentPath = `/docs/${slug}`;
+ const breadcrumbs = getBreadcrumbsFromUrl(currentPath);
+ return (
+ <>
+
+
+
+
+ {breadcrumbs.category.title}
+
+
+
+
+ {breadcrumbs.page.title}
+
+
+
+ {pathname != "/docs" && (
+
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/app/platform/panel-config-demo.tsx b/app/platform/panel-config-demo.tsx
new file mode 100644
index 0000000..2ac40c4
--- /dev/null
+++ b/app/platform/panel-config-demo.tsx
@@ -0,0 +1,469 @@
+"use client"
+
+import React, { useState } from "react";
+import UIBuilder, { defaultConfigTabsContent, TabsContentConfig } from "@/components/ui/ui-builder";
+import { complexComponentDefinitions } from "@/lib/ui-builder/registry/complex-component-definitions";
+import { primitiveComponentDefinitions } from "@/lib/ui-builder/registry/primitive-component-definitions";
+import { ComponentLayer, Variable } from '@/components/ui/ui-builder/types';
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Settings, Database, Layout, Home, Code, Eye } from "lucide-react";
+import { useLayerStore } from "@/lib/ui-builder/store/layer-store";
+import { useEditorStore } from "@/lib/ui-builder/store/editor-store";
+import LayerRenderer from "@/components/ui/ui-builder/layer-renderer";
+
+// Super Simple Custom Nav Component
+const SimpleNav = () => {
+ const [showCodeDialog, setShowCodeDialog] = useState(false);
+ const [showPreviewDialog, setShowPreviewDialog] = useState(false);
+
+ const selectedPageId = useLayerStore((state: any) => state.selectedPageId);
+ const findLayerById = useLayerStore((state: any) => state.findLayerById);
+ const componentRegistry = useEditorStore((state: any) => state.registry);
+
+ const page = findLayerById(selectedPageId) as ComponentLayer;
+
+ return (
+ <>
+
+
+
+ UI Builder
+
+
+ setShowCodeDialog(true)}
+ >
+
+ Code
+
+ setShowPreviewDialog(true)}
+ >
+
+ Preview
+
+
+
+
+ {/* Simple Code Dialog */}
+ {showCodeDialog && (
+
+
+
+
Generated Code
+ setShowCodeDialog(false)}>
+ ×
+
+
+
+
{`// Code export functionality would go here`}
+
+
+
+ )}
+
+ {/* Simple Preview Dialog */}
+ {showPreviewDialog && (
+
+
+
+
Page Preview
+ setShowPreviewDialog(false)}>
+ ×
+
+
+
+ {page && (
+
+ )}
+
+
+
+ )}
+ >
+ );
+};
+
+// Sample template for panel configuration demonstrations
+const sampleTemplate: ComponentLayer[] = [{
+ "id": "panel-config-demo-page",
+ "type": "div",
+ "name": "Panel Config Demo Page",
+ "props": {
+ "className": "min-h-screen bg-gradient-to-br from-purple-50 to-pink-100 p-8",
+ },
+ "children": [
+ {
+ "id": "demo-header",
+ "type": "div",
+ "name": "Demo Header",
+ "props": {
+ "className": "max-w-4xl mx-auto text-center mb-8"
+ },
+ "children": [
+ {
+ "id": "demo-title",
+ "type": "span",
+ "name": "Demo Title",
+ "props": {
+ "className": "text-3xl font-bold text-gray-900 block mb-2",
+ "children": { __variableRef: "demoTitle" }
+ },
+ "children": []
+ },
+ {
+ "id": "demo-description",
+ "type": "span",
+ "name": "Demo Description",
+ "props": {
+ "className": "text-lg text-gray-600 block",
+ "children": { __variableRef: "demoDescription" }
+ },
+ "children": []
+ }
+ ]
+ },
+ {
+ "id": "demo-content",
+ "type": "div",
+ "name": "Demo Content",
+ "props": {
+ "className": "max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-6"
+ },
+ "children": [
+ {
+ "id": "card-1",
+ "type": "Card",
+ "name": "Custom Panel Card 1",
+ "props": {
+ "className": "hover:shadow-lg transition-shadow"
+ },
+ "children": [
+ {
+ "id": "card-1-header",
+ "type": "CardHeader",
+ "name": "Card 1 Header",
+ "props": {},
+ "children": [
+ {
+ "id": "card-1-title",
+ "type": "CardTitle",
+ "name": "Card 1 Title",
+ "props": {
+ "children": { __variableRef: "card1Title" }
+ },
+ "children": []
+ }
+ ]
+ },
+ {
+ "id": "card-1-content",
+ "type": "CardContent",
+ "name": "Card 1 Content",
+ "props": {},
+ "children": [
+ {
+ "id": "card-1-text",
+ "type": "span",
+ "name": "Card 1 Text",
+ "props": {
+ "className": "text-gray-600",
+ "children": { __variableRef: "card1Content" }
+ },
+ "children": []
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "card-2",
+ "type": "Card",
+ "name": "Custom Panel Card 2",
+ "props": {
+ "className": "hover:shadow-lg transition-shadow"
+ },
+ "children": [
+ {
+ "id": "card-2-header",
+ "type": "CardHeader",
+ "name": "Card 2 Header",
+ "props": {},
+ "children": [
+ {
+ "id": "card-2-title",
+ "type": "CardTitle",
+ "name": "Card 2 Title",
+ "props": {
+ "children": { __variableRef: "card2Title" }
+ },
+ "children": []
+ }
+ ]
+ },
+ {
+ "id": "card-2-content",
+ "type": "CardContent",
+ "name": "Card 2 Content",
+ "props": {},
+ "children": [
+ {
+ "id": "card-2-text",
+ "type": "span",
+ "name": "Card 2 Text",
+ "props": {
+ "className": "text-gray-600",
+ "children": { __variableRef: "card2Content" }
+ },
+ "children": []
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}];
+
+// Sample variables for the demo
+const sampleVariables: Variable[] = [
+ {
+ id: "demoTitle",
+ name: "demoTitle",
+ type: "string",
+ defaultValue: "Panel Configuration Demo"
+ },
+ {
+ id: "demoDescription",
+ name: "demoDescription",
+ type: "string",
+ defaultValue: "This demo showcases custom panel configurations"
+ },
+ {
+ id: "card1Title",
+ name: "card1Title",
+ type: "string",
+ defaultValue: "Custom Panel Feature"
+ },
+ {
+ id: "card1Content",
+ name: "card1Content",
+ type: "string",
+ defaultValue: "Customize the editor interface to match your workflow"
+ },
+ {
+ id: "card2Title",
+ name: "card2Title",
+ type: "string",
+ defaultValue: "Advanced Configuration"
+ },
+ {
+ id: "card2Content",
+ name: "card2Content",
+ type: "string",
+ defaultValue: "Override default panels with your own components"
+ }
+];
+
+type PanelConfigMode = 'default' | 'custom-tabs' | 'custom-content' | 'minimal' | 'simple-nav';
+
+// Custom appearance panel component
+const CustomAppearancePanel = () => (
+
+
🎨 Custom Design Panel
+
+
Brand Colors
+
+ {['#3b82f6', '#ef4444', '#10b981', '#f59e0b'].map((color) => (
+
+ ))}
+
+
+
+
Typography Scale
+
+
Headings
+
Body Text
+
Captions
+
+
+
+);
+
+// Custom data panel component
+const CustomDataPanel = () => (
+
+
+
+ Data Sources
+
+
+
+
User Database
+
Connected • 1,247 users
+
+
+
Content API
+
Connected • 89 articles
+
+
+
Analytics
+
Connected • Live data
+
+
+
+ Add Data Source
+
+
+);
+
+// Custom settings panel component
+const CustomSettingsPanel = () => (
+
+
+
+ Project Settings
+
+
+
+
Environment
+
Development
+
+
+
Framework
+
Next.js 14
+
+
+
Deploy Target
+
Vercel
+
+
+
+);
+
+export const PanelConfigDemo = () => {
+ const [mode, setMode] = useState('default');
+
+ const modeConfigs = {
+ 'default': {
+ title: 'Default Config',
+ description: 'Standard panels with default content',
+ panelConfig: undefined,
+ },
+ 'custom-tabs': {
+ title: 'Custom Tab Names',
+ description: 'Same content with custom tab labels',
+ panelConfig: {
+ pageConfigPanelTabsContent: {
+ layers: { title: "Structure", content: defaultConfigTabsContent().layers.content },
+ appearance: { title: "Design", content: defaultConfigTabsContent().appearance?.content },
+ data: { title: "Variables", content: defaultConfigTabsContent().data?.content }
+ } as TabsContentConfig
+ },
+ },
+ 'custom-content': {
+ title: 'Custom Panel Content',
+ description: 'Custom components in each panel tab',
+ panelConfig: {
+ pageConfigPanelTabsContent: {
+ layers: { title: "Layers", content: defaultConfigTabsContent().layers.content },
+ appearance: { title: "Theme", content: },
+ data: { title: "Data", content: },
+ settings: { title: "Settings", content: }
+ } as TabsContentConfig
+ },
+ },
+ 'minimal': {
+ title: 'Minimal',
+ description: 'Only essential panels shown',
+ panelConfig: {
+ pageConfigPanelTabsContent: {
+ layers: { title: "Structure", content: defaultConfigTabsContent().layers.content }
+ } as TabsContentConfig
+ },
+ },
+ 'simple-nav': {
+ title: 'Custom Nav',
+ description: 'A super simple custom navigation bar',
+ panelConfig: {
+ navBar: ,
+ pageConfigPanelTabsContent: {
+ layers: { title: "Layers", content: defaultConfigTabsContent().layers.content },
+ appearance: { title: "Appearance", content: defaultConfigTabsContent().appearance?.content },
+ data: { title: "Data", content: defaultConfigTabsContent().data?.content }
+ } as TabsContentConfig
+ },
+ }
+ };
+
+ const currentConfig = modeConfigs[mode];
+
+ return (
+
+ {/* Mode Controls */}
+
+
+
+
+
+
+ {currentConfig.title}
+
+
{currentConfig.description}
+
+
+ {(Object.keys(modeConfigs) as PanelConfigMode[]).map((modeKey) => (
+ setMode(modeKey)}
+ >
+ {modeConfigs[modeKey].title}
+
+ ))}
+
+
+
+
+
+ {/* UI Builder */}
+
+ {
+ console.log(`[${mode}] Pages updated:`, updatedPages);
+ }}
+ onVariablesChange={(updatedVariables) => {
+ console.log(`[${mode}] Variables updated:`, updatedVariables);
+ }}
+ />
+
+
+ );
+};
\ No newline at end of file
diff --git a/app/platform/read-only-demo.tsx b/app/platform/read-only-demo.tsx
new file mode 100644
index 0000000..7ce8d52
--- /dev/null
+++ b/app/platform/read-only-demo.tsx
@@ -0,0 +1,325 @@
+"use client"
+
+import React, { useState } from "react";
+import UIBuilder from "@/components/ui/ui-builder";
+import { complexComponentDefinitions } from "@/lib/ui-builder/registry/complex-component-definitions";
+import { primitiveComponentDefinitions } from "@/lib/ui-builder/registry/primitive-component-definitions";
+import { ComponentLayer, Variable } from '@/components/ui/ui-builder/types';
+import { Button } from "@/components/ui/button";
+
+// Sample template for read-only demonstrations
+const sampleTemplate: ComponentLayer[] = [{
+ "id": "read-only-demo-page",
+ "type": "div",
+ "name": "Read-Only Demo Page",
+ "props": {
+ "className": "min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-8",
+ },
+ "children": [
+ {
+ "id": "header-section",
+ "type": "div",
+ "name": "Header",
+ "props": {
+ "className": "max-w-4xl mx-auto text-center mb-12"
+ },
+ "children": [
+ {
+ "id": "main-title",
+ "type": "span",
+ "name": "Main Title",
+ "props": {
+ "className": "text-4xl font-bold text-gray-900 block mb-4",
+ "children": { __variableRef: "pageTitle" }
+ },
+ "children": []
+ },
+ {
+ "id": "subtitle",
+ "type": "span",
+ "name": "Subtitle",
+ "props": {
+ "className": "text-lg text-gray-600 block",
+ "children": { __variableRef: "pageSubtitle" }
+ },
+ "children": []
+ }
+ ]
+ },
+ {
+ "id": "content-section",
+ "type": "div",
+ "name": "Content Section",
+ "props": {
+ "className": "max-w-4xl mx-auto grid md:grid-cols-2 gap-8"
+ },
+ "children": [
+ {
+ "id": "info-card",
+ "type": "Card",
+ "name": "Info Card",
+ "props": {
+ "className": "p-6"
+ },
+ "children": [
+ {
+ "id": "info-title",
+ "type": "CardHeader",
+ "name": "Card Header",
+ "props": {},
+ "children": [
+ {
+ "id": "info-card-title",
+ "type": "CardTitle",
+ "name": "Card Title",
+ "props": {},
+ "children": [
+ {
+ "type": "span",
+ "id": "info-title-text",
+ "name": "Title Text",
+ "props": {
+ "children": { __variableRef: "cardTitle" }
+ },
+ "children": []
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "info-content",
+ "type": "CardContent",
+ "name": "Card Content",
+ "props": {},
+ "children": [
+ {
+ "type": "span",
+ "id": "info-content-text",
+ "name": "Content Text",
+ "props": {
+ "children": { __variableRef: "cardContent" }
+ },
+ "children": []
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "action-card",
+ "type": "Card",
+ "name": "Action Card",
+ "props": {
+ "className": "p-6"
+ },
+ "children": [
+ {
+ "id": "action-header",
+ "type": "CardHeader",
+ "name": "Action Header",
+ "props": {},
+ "children": [
+ {
+ "id": "action-card-title",
+ "type": "CardTitle",
+ "name": "Action Title",
+ "props": {},
+ "children": [
+ {
+ "type": "span",
+ "children": "Take Action",
+ "id": "action-title-text",
+ "name": "Action Title Text",
+ "props": {}
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "action-content",
+ "type": "CardContent",
+ "name": "Action Content",
+ "props": {
+ "className": "space-y-4"
+ },
+ "children": [
+ {
+ "id": "primary-button",
+ "type": "Button",
+ "name": "Primary Button",
+ "props": {
+ "variant": "default",
+ "className": "w-full"
+ },
+ "children": [
+ {
+ "type": "span",
+ "id": "primary-btn-text",
+ "name": "Primary Button Text",
+ "props": {
+ "children": { __variableRef: "primaryButtonText" }
+ },
+ "children": []
+ }
+ ]
+ },
+ {
+ "id": "secondary-button",
+ "type": "Button",
+ "name": "Secondary Button",
+ "props": {
+ "variant": "outline",
+ "className": "w-full"
+ },
+ "children": [
+ {
+ "type": "span",
+ "id": "secondary-btn-text",
+ "name": "Secondary Button Text",
+ "props": {
+ "children": { __variableRef: "secondaryButtonText" }
+ },
+ "children": []
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}];
+
+// Sample variables bound to the template
+const sampleVariables: Variable[] = [
+ {
+ id: "pageTitle",
+ name: "Page Title",
+ type: "string",
+ defaultValue: "Read-Only Mode Demo"
+ },
+ {
+ id: "pageSubtitle",
+ name: "Page Subtitle",
+ type: "string",
+ defaultValue: "Demonstrating different levels of editing restrictions"
+ },
+ {
+ id: "cardTitle",
+ name: "Card Title",
+ type: "string",
+ defaultValue: "System Information"
+ },
+ {
+ id: "cardContent",
+ name: "Card Content",
+ type: "string",
+ defaultValue: "This content is bound to variables that may be restricted based on your permissions."
+ },
+ {
+ id: "primaryButtonText",
+ name: "Primary Button Text",
+ type: "string",
+ defaultValue: "Get Started"
+ },
+ {
+ id: "secondaryButtonText",
+ name: "Secondary Button Text",
+ type: "string",
+ defaultValue: "Learn More"
+ }
+];
+
+type ReadOnlyMode = 'full-edit' | 'content-only' | 'no-variables' | 'full-readonly';
+
+export const ReadOnlyDemo = () => {
+ const [mode, setMode] = useState('full-edit');
+
+ const modeConfigs = {
+ 'full-edit': {
+ title: 'Full Editing Mode',
+ description: 'All editing capabilities enabled',
+ allowVariableEditing: true,
+ allowPagesCreation: true,
+ allowPagesDeletion: true,
+ },
+ 'content-only': {
+ title: 'Content-Only Mode',
+ description: 'Variables editable, but page structure locked',
+ allowVariableEditing: true,
+ allowPagesCreation: false,
+ allowPagesDeletion: false,
+ },
+ 'no-variables': {
+ title: 'No Variable Editing',
+ description: 'Page structure editable, but variables locked',
+ allowVariableEditing: false,
+ allowPagesCreation: true,
+ allowPagesDeletion: true,
+ },
+ 'full-readonly': {
+ title: 'Full Read-Only Mode',
+ description: 'All structural changes disabled',
+ allowVariableEditing: false,
+ allowPagesCreation: false,
+ allowPagesDeletion: false,
+ }
+ };
+
+ const currentConfig = modeConfigs[mode];
+
+ return (
+
+ {/* Mode Controls */}
+
+
+
+
+
{currentConfig.title}
+
{currentConfig.description}
+
+
+ {(Object.keys(modeConfigs) as ReadOnlyMode[]).map((modeKey) => (
+ setMode(modeKey)}
+ >
+ {modeConfigs[modeKey].title}
+
+ ))}
+
+
+
+
+
+ {/* UI Builder */}
+
+ {
+ console.log(`[${mode}] Pages updated:`, updatedPages);
+ }}
+ onVariablesChange={(updatedVariables) => {
+ console.log(`[${mode}] Variables updated:`, updatedVariables);
+ }}
+ />
+
+
+ );
+};
\ No newline at end of file
diff --git a/components/ui/ui-builder/components/codeblock.tsx b/components/ui/ui-builder/components/codeblock.tsx
index 2bcadd2..cad21ca 100644
--- a/components/ui/ui-builder/components/codeblock.tsx
+++ b/components/ui/ui-builder/components/codeblock.tsx
@@ -85,9 +85,9 @@ export const CodeBlock = memo(function CodeBlock({
);
return (
-
-
-
{language}
+
+
+
{language}
childrenAsTextareaFieldOverrides(layer)
+ },
+ defaultChildren: "Heading 1"
+ },
+ 'h2': {
+ schema: z.object({
+ className: z.string().optional(),
+ children: z.string().optional(),
+ }),
+ fieldOverrides: {
+ ...commonFieldOverrides(),
+ children: (layer) => childrenAsTextareaFieldOverrides(layer)
+ },
+ defaultChildren: "Heading 2"
+ },
+ 'h3': {
+ schema: z.object({
+ className: z.string().optional(),
+ children: z.string().optional(),
+ }),
+ fieldOverrides: {
+ ...commonFieldOverrides(),
+ children: (layer) => childrenAsTextareaFieldOverrides(layer)
+ },
+ defaultChildren: "Heading 3"
+ },
+ 'p': {
+ schema: z.object({
+ className: z.string().optional(),
+ children: z.string().optional(),
+ }),
+ fieldOverrides: {
+ ...commonFieldOverrides(),
+ children: (layer) => childrenAsTextareaFieldOverrides(layer)
+ },
+ defaultChildren: "Paragraph text"
+ },
+ 'li': {
+ schema: z.object({
+ className: z.string().optional(),
+ children: z.string().optional(),
+ }),
+ fieldOverrides: {
+ ...commonFieldOverrides(),
+ children: (layer) => childrenAsTextareaFieldOverrides(layer)
+ },
+ defaultChildren: "List item"
+ },
+ 'ul': {
+ schema: z.object({
+ className: z.string().optional(),
+ children: z.string().optional(),
+ }),
+ fieldOverrides: commonFieldOverrides()
+ },
+ 'ol': {
+ schema: z.object({
+ className: z.string().optional(),
+ children: z.string().optional(),
+ }),
+ fieldOverrides: commonFieldOverrides()
+ }
};
diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png
new file mode 100644
index 0000000000000000000000000000000000000000..05366b7cc13a80e42d98080aa6c9d50dce838eee
GIT binary patch
literal 10947
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