\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": "## 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
new file mode 100644
index 0000000..7fee5a7
--- /dev/null
+++ b/app/docs/docs-data/docs-page-layers/persistence.ts
@@ -0,0 +1,99 @@
+import { ComponentLayer } from "@/components/ui/ui-builder/types";
+
+export const PERSISTENCE_LAYER = {
+ "id": "persistence",
+ "type": "div",
+ "name": "State Management & Persistence",
+ "props": {
+ "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen",
+ "data-group": "layout-persistence"
+ },
+ "children": [
+ {
+ "type": "span",
+ "children": "State Management & Persistence",
+ "id": "persistence-title",
+ "name": "Text",
+ "props": {
+ "className": "text-4xl"
+ }
+ },
+ {
+ "id": "persistence-intro",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "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-state-overview",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "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/quick-start.ts b/app/docs/docs-data/docs-page-layers/quick-start.ts
new file mode 100644
index 0000000..4737978
--- /dev/null
+++ b/app/docs/docs-data/docs-page-layers/quick-start.ts
@@ -0,0 +1,172 @@
+import { ComponentLayer } from "@/components/ui/ui-builder/types";
+
+export const QUICK_START_LAYER = {
+ "id": "quick-start",
+ "type": "div",
+ "name": "Quick Start",
+ "props": {
+ "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen",
+ "data-group": "core"
+ },
+ "children": [
+ {
+ "type": "span",
+ "children": "Quick Start",
+ "id": "quick-start-title",
+ "name": "Text",
+ "props": {
+ "className": "text-4xl"
+ }
+ },
+ {
+ "id": "quick-start-intro",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "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, 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-setup",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "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",
+ "type": "div",
+ "name": "div",
+ "props": {},
+ "children": [
+ {
+ "id": "quick-start-badge",
+ "type": "Badge",
+ "name": "Badge",
+ "props": {
+ "variant": "default",
+ "className": "rounded rounded-b-none"
+ },
+ "children": [
+ {
+ "id": "quick-start-badge-text",
+ "type": "span",
+ "name": "span",
+ "props": {},
+ "children": "Try it now"
+ }
+ ]
+ },
+ {
+ "id": "quick-start-demo",
+ "type": "div",
+ "name": "div",
+ "props": {
+ "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden"
+ },
+ "children": [
+ {
+ "id": "quick-start-iframe",
+ "type": "iframe",
+ "name": "iframe",
+ "props": {
+ "src": "/examples/basic",
+ "title": "Quick Start Example",
+ "className": "aspect-square md:aspect-video"
+ },
+ "children": []
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "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
new file mode 100644
index 0000000..9bedcaf
--- /dev/null
+++ b/app/docs/docs-data/docs-page-layers/read-only-mode.ts
@@ -0,0 +1,57 @@
+import { ComponentLayer } from "@/components/ui/ui-builder/types";
+
+export const READ_ONLY_MODE_LAYER = {
+ "id": "read-only-mode",
+ "type": "div",
+ "name": "Editing Restrictions",
+ "props": {
+ "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen",
+ "data-group": "data-variables"
+ },
+ "children": [
+ {
+ "type": "span",
+ "children": "Editing Restrictions",
+ "id": "read-only-mode-title",
+ "name": "Text",
+ "props": {
+ "className": "text-4xl"
+ }
+ },
+ {
+ "id": "read-only-mode-intro",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "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": "## 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
new file mode 100644
index 0000000..d9b8ccb
--- /dev/null
+++ b/app/docs/docs-data/docs-page-layers/rendering-pages.ts
@@ -0,0 +1,130 @@
+import { ComponentLayer } from "@/components/ui/ui-builder/types";
+
+export const RENDERING_PAGES_LAYER = {
+ "id": "rendering-pages",
+ "type": "div",
+ "name": "Rendering Pages",
+ "props": {
+ "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen",
+ "data-group": "rendering"
+ },
+ "children": [
+ {
+ "type": "span",
+ "children": "Rendering Pages",
+ "id": "rendering-pages-title",
+ "name": "Text",
+ "props": {
+ "className": "text-4xl"
+ }
+ },
+ {
+ "id": "rendering-pages-intro",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "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",
+ "type": "div",
+ "name": "div",
+ "props": {},
+ "children": [
+ {
+ "id": "rendering-pages-badge",
+ "type": "Badge",
+ "name": "Badge",
+ "props": {
+ "variant": "default",
+ "className": "rounded rounded-b-none"
+ },
+ "children": [
+ {
+ "id": "rendering-pages-badge-text",
+ "type": "span",
+ "name": "span",
+ "props": {},
+ "children": "Basic Rendering Demo"
+ }
+ ]
+ },
+ {
+ "id": "rendering-pages-demo-frame",
+ "type": "div",
+ "name": "div",
+ "props": {
+ "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden"
+ },
+ "children": [
+ {
+ "id": "rendering-pages-iframe",
+ "type": "iframe",
+ "name": "iframe",
+ "props": {
+ "src": "/examples/renderer",
+ "title": "Page Rendering Demo",
+ "className": "aspect-square md:aspect-video w-full"
+ },
+ "children": []
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "rendering-pages-variables-demo",
+ "type": "div",
+ "name": "div",
+ "props": {},
+ "children": [
+ {
+ "id": "rendering-pages-variables-badge",
+ "type": "Badge",
+ "name": "Badge",
+ "props": {
+ "variant": "outline",
+ "className": "rounded rounded-b-none"
+ },
+ "children": [
+ {
+ "id": "rendering-pages-variables-badge-text",
+ "type": "span",
+ "name": "span",
+ "props": {},
+ "children": "Variables Demo"
+ }
+ ]
+ },
+ {
+ "id": "rendering-pages-variables-demo-frame",
+ "type": "div",
+ "name": "div",
+ "props": {
+ "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden"
+ },
+ "children": [
+ {
+ "id": "rendering-pages-variables-iframe",
+ "type": "iframe",
+ "name": "iframe",
+ "props": {
+ "src": "/examples/renderer/variables",
+ "title": "Variables Rendering Demo",
+ "className": "aspect-square md:aspect-video w-full"
+ },
+ "children": []
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "rendering-pages-content",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "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
new file mode 100644
index 0000000..bbfe66c
--- /dev/null
+++ b/app/docs/docs-data/docs-page-layers/variable-binding.ts
@@ -0,0 +1,83 @@
+import { ComponentLayer } from "@/components/ui/ui-builder/types";
+
+export const VARIABLE_BINDING_LAYER = {
+ "id": "variable-binding",
+ "type": "div",
+ "name": "Variable Binding",
+ "props": {
+ "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen",
+ "data-group": "data-variables"
+ },
+ "children": [
+ {
+ "type": "span",
+ "children": "Variable Binding",
+ "id": "variable-binding-title",
+ "name": "Text",
+ "props": {
+ "className": "text-4xl"
+ }
+ },
+ {
+ "id": "variable-binding-intro",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "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 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.ts b/app/docs/docs-data/docs-page-layers/variables.ts
new file mode 100644
index 0000000..841b01e
--- /dev/null
+++ b/app/docs/docs-data/docs-page-layers/variables.ts
@@ -0,0 +1,83 @@
+import { ComponentLayer } from "@/components/ui/ui-builder/types";
+
+export const VARIABLES_LAYER = {
+ "id": "variables",
+ "type": "div",
+ "name": "Variables",
+ "props": {
+ "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen",
+ "data-group": "data-variables"
+ },
+ "children": [
+ {
+ "type": "span",
+ "children": "Variables",
+ "id": "variables-title",
+ "name": "Text",
+ "props": {
+ "className": "text-4xl"
+ }
+ },
+ {
+ "id": "variables-intro",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "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",
+ "type": "div",
+ "name": "div",
+ "props": {},
+ "children": [
+ {
+ "id": "variables-badge",
+ "type": "Badge",
+ "name": "Badge",
+ "props": {
+ "variant": "default",
+ "className": "rounded rounded-b-none"
+ },
+ "children": [
+ {
+ "id": "variables-badge-text",
+ "type": "span",
+ "name": "span",
+ "props": {},
+ "children": "Live Variables Example"
+ }
+ ]
+ },
+ {
+ "id": "variables-demo-frame",
+ "type": "div",
+ "name": "div",
+ "props": {
+ "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden"
+ },
+ "children": [
+ {
+ "id": "variables-iframe",
+ "type": "iframe",
+ "name": "iframe",
+ "props": {
+ "src": "/examples/renderer/variables",
+ "title": "Variables Demo",
+ "className": "aspect-square md:aspect-video w-full"
+ },
+ "children": []
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "variables-content",
+ "type": "Markdown",
+ "name": "Markdown",
+ "props": {},
+ "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
new file mode 100644
index 0000000..354ee66
--- /dev/null
+++ b/app/docs/layout.tsx
@@ -0,0 +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 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}
+
+
+
+ );
+}
+
+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/basic/page.tsx b/app/examples/basic/page.tsx
new file mode 100644
index 0000000..24321ad
--- /dev/null
+++ b/app/examples/basic/page.tsx
@@ -0,0 +1,13 @@
+import { SimpleBuilder } from "@/app/platform/simple-builder";
+
+export const metadata = {
+ title: "UI Builder",
+};
+
+export default function Page() {
+ return (
+
+
+
+ );
+}
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
new file mode 100644
index 0000000..90121e2
--- /dev/null
+++ b/app/platform/app-sidebar.tsx
@@ -0,0 +1,79 @@
+"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;
+}
+
+function AppSidebarContent({ currentPath, ...props }: AppSidebarProps) {
+ return (
+
+
+
+
+ UI Builder
+
+
+
+
+ {MENU_DATA.map((section) => (
+
+ {section.title}
+
+
+ {section.items?.map((item) => {
+ const isActive = currentPath === item.url;
+ return (
+
+
+ {item.title}
+
+
+ );
+ })}
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+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/doc-editor.tsx b/app/platform/doc-editor.tsx
new file mode 100644
index 0000000..263b950
--- /dev/null
+++ b/app/platform/doc-editor.tsx
@@ -0,0 +1,22 @@
+"use client";
+
+import React 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 } from "@/components/ui/ui-builder/types";
+
+
+
+export const DocEditor = ({page}: {page: ComponentLayer}) => {
+ return (
+
+ );
+}
diff --git a/app/platform/doc-renderer.tsx b/app/platform/doc-renderer.tsx
new file mode 100644
index 0000000..54f7c0c
--- /dev/null
+++ b/app/platform/doc-renderer.tsx
@@ -0,0 +1,14 @@
+"use client";
+
+import LayerRenderer from "@/components/ui/ui-builder/layer-renderer";
+import { complexComponentDefinitions } from "@/lib/ui-builder/registry/complex-component-definitions";
+import { primitiveComponentDefinitions } from "@/lib/ui-builder/registry/primitive-component-definitions";
+
+import { ComponentLayer } from "@/components/ui/ui-builder/types";
+const COMPONENT_REGISTRY = {
+ ...complexComponentDefinitions,
+ ...primitiveComponentDefinitions,
+}
+export const DocRenderer = ({page, className}: {page: ComponentLayer, className?: string}) => {
+ return ;
+};
\ No newline at end of file
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/app/platform/renderer-with-vars.tsx b/app/platform/renderer-with-vars.tsx
index 1dc2053..8286593 100644
--- a/app/platform/renderer-with-vars.tsx
+++ b/app/platform/renderer-with-vars.tsx
@@ -4,79 +4,360 @@ import LayerRenderer from "@/components/ui/ui-builder/layer-renderer";
import { ComponentLayer } from "@/components/ui/ui-builder/types";
import { ComponentRegistry } from "@/components/ui/ui-builder/types";
import { Variable } from '@/components/ui/ui-builder/types';
-import SimpleComponent from "app/platform/simple-component";
import { primitiveComponentDefinitions } from "@/lib/ui-builder/registry/primitive-component-definitions";
-import z from "zod";
-
+import { complexComponentDefinitions } from "@/lib/ui-builder/registry/complex-component-definitions";
const myComponentRegistry: ComponentRegistry = {
- SimpleComponent: {
- component: SimpleComponent,
- schema: z.object({
- someString: z.string(),
- someNumber: z.number(),
- someBoolean: z.boolean()
- }),
- from: "app/platform/simple-component"
- },
- ...primitiveComponentDefinitions
+ ...primitiveComponentDefinitions,
+ ...complexComponentDefinitions
};
+// Page structure with actual variable bindings
const page: ComponentLayer = {
- id: "example-page",
+ id: "variables-demo-page",
type: "div",
props: {
- className: "flex flex-col items-center justify-center gap-4 p-4 bg-gray-100"
+ className: "max-w-4xl mx-auto p-8 space-y-8 bg-gradient-to-br from-blue-50 to-purple-50 min-h-screen"
},
children: [
+ // Header with variable binding
+ {
+ id: "header",
+ type: "div",
+ props: {
+ className: "text-center space-y-4"
+ },
+ children: [
+ {
+ id: "page-title",
+ type: "h1",
+ props: {
+ className: "text-4xl font-bold text-gray-900",
+ children: { __variableRef: "pageTitle" } // Bound to variable
+ },
+ children: []
+ },
+ {
+ id: "page-subtitle",
+ type: "p",
+ props: {
+ className: "text-xl text-gray-600",
+ children: { __variableRef: "pageSubtitle" } // Bound to variable
+ },
+ children: []
+ }
+ ]
+ },
+ // User info card with variable bindings
+ {
+ id: "user-card",
+ type: "div",
+ props: {
+ className: "bg-white rounded-lg shadow-lg p-6 border border-gray-200"
+ },
+ children: [
+ {
+ id: "user-card-title",
+ type: "h2",
+ props: {
+ className: "text-2xl font-semibold text-gray-800 mb-4",
+ children: "User Information"
+ },
+ children: []
+ },
+ {
+ id: "user-info",
+ type: "div",
+ props: {
+ className: "space-y-3"
+ },
+ children: [
+ {
+ id: "user-name-row",
+ type: "div",
+ props: {
+ className: "flex items-center gap-2"
+ },
+ children: [
+ {
+ id: "name-label",
+ type: "span",
+ props: {
+ className: "font-medium text-gray-700 w-20",
+ children: "Name:"
+ },
+ children: []
+ },
+ {
+ id: "user-name",
+ type: "span",
+ props: {
+ className: "text-gray-900",
+ children: { __variableRef: "userName" } // Bound to variable
+ },
+ children: []
+ }
+ ]
+ },
+ {
+ id: "user-age-row",
+ type: "div",
+ props: {
+ className: "flex items-center gap-2"
+ },
+ children: [
+ {
+ id: "age-label",
+ type: "span",
+ props: {
+ className: "font-medium text-gray-700 w-20",
+ children: "Age:"
+ },
+ children: []
+ },
+ {
+ id: "user-age",
+ type: "span",
+ props: {
+ className: "text-gray-900",
+ children: { __variableRef: "userAge" } // Bound to variable
+ },
+ children: []
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ // Interactive buttons with variable bindings
+ {
+ id: "buttons-section",
+ type: "div",
+ props: {
+ className: "bg-white rounded-lg shadow-lg p-6 border border-gray-200"
+ },
+ children: [
+ {
+ id: "buttons-title",
+ type: "h2",
+ props: {
+ className: "text-2xl font-semibold text-gray-800 mb-4",
+ children: "Dynamic Buttons"
+ },
+ children: []
+ },
+ {
+ id: "buttons-container",
+ type: "div",
+ props: {
+ className: "flex flex-wrap gap-4"
+ },
+ children: [
+ {
+ id: "primary-button",
+ type: "Button",
+ props: {
+ variant: "default",
+ className: "flex-1 min-w-fit",
+ children: { __variableRef: "buttonText" }, // Bound to variable
+ disabled: { __variableRef: "isLoading" } // Bound to variable
+ },
+ children: []
+ },
+ {
+ id: "secondary-button",
+ type: "Button",
+ props: {
+ variant: "outline",
+ className: "flex-1 min-w-fit",
+ children: { __variableRef: "secondaryButtonText" } // Bound to variable
+ },
+ children: []
+ }
+ ]
+ }
+ ]
+ },
+ // Feature flags section
{
- id: "simple-component",
- type: "SimpleComponent",
+ id: "features-section",
+ type: "div",
props: {
- someString: "Hello",
- someNumber: 123,
- someBoolean: true
+ className: "bg-white rounded-lg shadow-lg p-6 border border-gray-200"
},
- children: []
+ children: [
+ {
+ id: "features-title",
+ type: "h2",
+ props: {
+ className: "text-2xl font-semibold text-gray-800 mb-4",
+ children: "Feature Toggles"
+ },
+ children: []
+ },
+ {
+ id: "features-list",
+ type: "div",
+ props: {
+ className: "space-y-3"
+ },
+ children: [
+ {
+ id: "dark-mode-feature",
+ type: "div",
+ props: {
+ className: "flex items-center justify-between p-3 bg-gray-50 rounded"
+ },
+ children: [
+ {
+ id: "dark-mode-label",
+ type: "span",
+ props: {
+ className: "font-medium text-gray-700",
+ children: "Dark Mode"
+ },
+ children: []
+ },
+ {
+ id: "dark-mode-badge",
+ type: "Badge",
+ props: {
+ variant: { __variableRef: "darkModeEnabled" }, // Bound to variable (will resolve to "default" or "secondary")
+ children: { __variableRef: "darkModeStatus" } // Bound to variable
+ },
+ children: []
+ }
+ ]
+ },
+ {
+ id: "notifications-feature",
+ type: "div",
+ props: {
+ className: "flex items-center justify-between p-3 bg-gray-50 rounded"
+ },
+ children: [
+ {
+ id: "notifications-label",
+ type: "span",
+ props: {
+ className: "font-medium text-gray-700",
+ children: "Notifications"
+ },
+ children: []
+ },
+ {
+ id: "notifications-badge",
+ type: "Badge",
+ props: {
+ variant: { __variableRef: "notificationsEnabled" }, // Bound to variable
+ children: { __variableRef: "notificationsStatus" } // Bound to variable
+ },
+ children: []
+ }
+ ]
+ }
+ ]
+ }
+ ]
}
]
-}
+};
-// Define variables for the page
+// Define variables that match the bindings
const variables: Variable[] = [
{
- id: "someString",
- name: "String Variable",
+ id: "pageTitle",
+ name: "Page Title",
+ type: "string",
+ defaultValue: "UI Builder Variables Demo"
+ },
+ {
+ id: "pageSubtitle",
+ name: "Page Subtitle",
+ type: "string",
+ defaultValue: "See how variables make your content dynamic"
+ },
+ {
+ id: "userName",
+ name: "User Name",
type: "string",
- defaultValue: "Hello"
+ defaultValue: "John Doe"
},
{
- id: "someNumber",
- name: "Number Variable",
+ id: "userAge",
+ name: "User Age",
type: "number",
- defaultValue: 123
+ defaultValue: 25
},
{
- id: "someBoolean",
- name: "Boolean Variable",
+ id: "buttonText",
+ name: "Primary Button Text",
+ type: "string",
+ defaultValue: "Click Me!"
+ },
+ {
+ id: "secondaryButtonText",
+ name: "Secondary Button Text",
+ type: "string",
+ defaultValue: "Learn More"
+ },
+ {
+ id: "isLoading",
+ name: "Loading State",
type: "boolean",
- defaultValue: true
+ defaultValue: false
+ },
+ {
+ id: "darkModeEnabled",
+ name: "Dark Mode Enabled",
+ type: "string",
+ defaultValue: "secondary"
+ },
+ {
+ id: "darkModeStatus",
+ name: "Dark Mode Status",
+ type: "string",
+ defaultValue: "Disabled"
+ },
+ {
+ id: "notificationsEnabled",
+ name: "Notifications Enabled",
+ type: "string",
+ defaultValue: "default"
+ },
+ {
+ id: "notificationsStatus",
+ name: "Notifications Status",
+ type: "string",
+ defaultValue: "Enabled"
}
];
+// Override some variable values to show dynamic behavior
const variableValues = {
- someString: "Hello",
- someNumber: 123,
- someBoolean: true
+ pageTitle: "🚀 Dynamic Variables in Action",
+ pageSubtitle: "Values injected at runtime - try changing them!",
+ userName: "Jane Smith",
+ userAge: 30,
+ buttonText: "Get Started Now",
+ secondaryButtonText: "View Documentation",
+ isLoading: false,
+ darkModeEnabled: "default",
+ darkModeStatus: "Enabled",
+ notificationsEnabled: "secondary",
+ notificationsStatus: "Disabled"
};
export function RendererWithVars() {
return (
-
+
+
+
);
}
\ No newline at end of file
diff --git a/app/platform/simple-builder.tsx b/app/platform/simple-builder.tsx
index 6185b36..08a53b9 100644
--- a/app/platform/simple-builder.tsx
+++ b/app/platform/simple-builder.tsx
@@ -11,7 +11,7 @@ const initialLayers = [
type: "div",
name: "Page",
props: {
- className: "bg-gray-200 flex flex-col justify-center items-center gap-4 p-2 w-full h-96",
+ className: "bg-gray-200 flex flex-col justify-center items-center gap-4 p-2 w-full h-screen",
},
children: [
{
@@ -19,7 +19,7 @@ const initialLayers = [
type: "div",
name: "Box A",
props: {
- className: "bg-red-300 p-2 w-1/2 h-1/3 text-center",
+ className: "flex flex-row justify-center items-center bg-red-300 p-2 w-full md:w-1/2 h-1/3 text-center",
},
children: [
{
@@ -27,7 +27,7 @@ const initialLayers = [
type: "span",
name: "Text",
props: {
- className: "text-4xl font-bold text-white",
+ className: "text-4xl font-bold text-secondary",
},
children: "A",
}
@@ -38,7 +38,7 @@ const initialLayers = [
type: "div",
name: "Box B",
props: {
- className: "bg-green-300 p-2 w-1/2 h-1/3 text-center",
+ className: "flex flex-row justify-center items-center bg-green-300 p-2 w-full md:w-1/2 h-1/3 text-center",
},
children: [
{
@@ -46,7 +46,7 @@ const initialLayers = [
type: "span",
name: "Text",
props: {
- className: "text-4xl font-bold text-white",
+ className: "text-4xl font-bold text-secondary",
},
children: "B",
}
@@ -57,7 +57,7 @@ const initialLayers = [
type: "div",
name: "Box C",
props: {
- className: "flex flex-row justify-center items-center bg-blue-300 p-2 w-1/2 h-1/3 p-2 w-1/2 h-1/3 text-center",
+ className: "flex flex-row justify-center items-center bg-blue-300 p-2 w-full md:w-1/2 h-1/3 p-2 w-1/2 h-1/3 text-center",
},
children: [
{
@@ -65,19 +65,21 @@ const initialLayers = [
type: "div",
name: "Inner Box D",
props: {
- className: "bg-yellow-300 p-2 w-1/2 h-1/3 p-2 w-1/2 h-1/3 text-center",
+ className: "bg-yellow-300 p-2 w-1/2 p-2 w-1/2 h-auto text-center",
},
- children: [],
+ children: [
+ {
+ id: "7",
+ type: "span",
+ name: "Text",
+ props: {
+ className: "text-4xl font-bold text-secondary-foreground",
+ },
+ children: "C",
+ }
+ ],
},
- {
- id: "7",
- type: "span",
- name: "Text",
- props: {
- className: "text-4xl font-bold text-white",
- },
- children: "C",
- }
+
],
},
],
diff --git a/app/platform/theme-toggle.tsx b/app/platform/theme-toggle.tsx
new file mode 100644
index 0000000..b305dc9
--- /dev/null
+++ b/app/platform/theme-toggle.tsx
@@ -0,0 +1,46 @@
+"use client";
+import { useTheme } from "next-themes"
+import { useCallback } from "react"
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
+import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
+import { Button } from "@/components/ui/button"
+import { SunIcon, MoonIcon } from "lucide-react"
+
+export function ThemeToggle() {
+ const { setTheme } = useTheme();
+
+
+ const handleSetLightTheme = useCallback(() => {
+ setTheme("light");
+ }, [setTheme]);
+ const handleSetDarkTheme = useCallback(() => {
+ setTheme("dark");
+ }, [setTheme]);
+ const handleSetSystemTheme = useCallback(() => {
+ setTheme("system");
+ }, [setTheme]);
+
+ return (
+
+
+
+
+
+
+
+ Toggle theme
+
+
+
+ Toggle theme
+
+
+ Light
+ Dark
+
+ System
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..60e6c96
--- /dev/null
+++ b/components/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode
+ }
+>(({ ...props }, ref) => )
+Breadcrumb.displayName = "Breadcrumb"
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbList.displayName = "BreadcrumbList"
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbItem.displayName = "BreadcrumbItem"
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+})
+BreadcrumbLink.displayName = "BreadcrumbLink"
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbPage.displayName = "BreadcrumbPage"
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+ svg]:w-3.5 [&>svg]:h-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+)
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+)
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/components/ui/sheet.tsx b/components/ui/sheet.tsx
new file mode 100644
index 0000000..a37f17b
--- /dev/null
+++ b/components/ui/sheet.tsx
@@ -0,0 +1,140 @@
+"use client"
+
+import * as React from "react"
+import * as SheetPrimitive from "@radix-ui/react-dialog"
+import { cva, type VariantProps } from "class-variance-authority"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Sheet = SheetPrimitive.Root
+
+const SheetTrigger = SheetPrimitive.Trigger
+
+const SheetClose = SheetPrimitive.Close
+
+const SheetPortal = SheetPrimitive.Portal
+
+const SheetOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
+
+const sheetVariants = cva(
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
+ {
+ variants: {
+ side: {
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
+ bottom:
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
+ right:
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
+ },
+ },
+ defaultVariants: {
+ side: "right",
+ },
+ }
+)
+
+interface SheetContentProps
+ extends React.ComponentPropsWithoutRef,
+ VariantProps {}
+
+const SheetContent = React.forwardRef<
+ React.ElementRef,
+ SheetContentProps
+>(({ side = "right", className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+))
+SheetContent.displayName = SheetPrimitive.Content.displayName
+
+const SheetHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+SheetHeader.displayName = "SheetHeader"
+
+const SheetFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+SheetFooter.displayName = "SheetFooter"
+
+const SheetTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetTitle.displayName = SheetPrimitive.Title.displayName
+
+const SheetDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetDescription.displayName = SheetPrimitive.Description.displayName
+
+export {
+ Sheet,
+ SheetPortal,
+ SheetOverlay,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+}
diff --git a/components/ui/sidebar.tsx b/components/ui/sidebar.tsx
new file mode 100644
index 0000000..a8a9d4c
--- /dev/null
+++ b/components/ui/sidebar.tsx
@@ -0,0 +1,780 @@
+/* eslint-disable react-perf/jsx-no-new-function-as-prop */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+"use client"
+
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { VariantProps, cva } from "class-variance-authority"
+import { PanelLeft } from "lucide-react"
+
+import { useIsMobile } from "@/hooks/use-mobile"
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Separator } from "@/components/ui/separator"
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet"
+import { Skeleton } from "@/components/ui/skeleton"
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip"
+
+const SIDEBAR_COOKIE_NAME = "sidebar_state"
+const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
+const SIDEBAR_WIDTH = "16rem"
+const SIDEBAR_WIDTH_MOBILE = "18rem"
+const SIDEBAR_WIDTH_ICON = "3rem"
+const SIDEBAR_KEYBOARD_SHORTCUT = "b"
+
+type SidebarContextProps = {
+ state: "expanded" | "collapsed"
+ open: boolean
+ setOpen: (open: boolean) => void
+ openMobile: boolean
+ setOpenMobile: (open: boolean) => void
+ isMobile: boolean
+ toggleSidebar: () => void
+}
+
+const SidebarContext = React.createContext(null)
+
+function useSidebar() {
+ const context = React.useContext(SidebarContext)
+ if (!context) {
+ throw new Error("useSidebar must be used within a SidebarProvider.")
+ }
+
+ return context
+}
+
+const SidebarProvider = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ defaultOpen?: boolean
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+ }
+>(
+ (
+ {
+ defaultOpen = true,
+ open: openProp,
+ onOpenChange: setOpenProp,
+ className,
+ style,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const isMobile = useIsMobile()
+ const [openMobile, setOpenMobile] = React.useState(false)
+
+ // This is the internal state of the sidebar.
+ // We use openProp and setOpenProp for control from outside the component.
+ const [_open, _setOpen] = React.useState(defaultOpen)
+ const open = openProp ?? _open
+ const setOpen = React.useCallback(
+ (value: boolean | ((value: boolean) => boolean)) => {
+ const openState = typeof value === "function" ? value(open) : value
+ if (setOpenProp) {
+ setOpenProp(openState)
+ } else {
+ _setOpen(openState)
+ }
+
+ // This sets the cookie to keep the sidebar state.
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
+ },
+ [setOpenProp, open]
+ )
+
+ // Helper to toggle the sidebar.
+ const toggleSidebar = React.useCallback(() => {
+ return isMobile
+ ? setOpenMobile((open) => !open)
+ : setOpen((open) => !open)
+ }, [isMobile, setOpen, setOpenMobile])
+
+ // Adds a keyboard shortcut to toggle the sidebar.
+ React.useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
+ (event.metaKey || event.ctrlKey)
+ ) {
+ event.preventDefault()
+ toggleSidebar()
+ }
+ }
+
+ window.addEventListener("keydown", handleKeyDown)
+ return () => window.removeEventListener("keydown", handleKeyDown)
+ }, [toggleSidebar])
+
+ // We add a state so that we can do data-state="expanded" or "collapsed".
+ // This makes it easier to style the sidebar with Tailwind classes.
+ const state = open ? "expanded" : "collapsed"
+
+ const contextValue = React.useMemo(
+ () => ({
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ }),
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
+ )
+
+ return (
+
+
+
+ {children}
+
+
+
+ )
+ }
+)
+SidebarProvider.displayName = "SidebarProvider"
+
+const Sidebar = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ side?: "left" | "right"
+ variant?: "sidebar" | "floating" | "inset"
+ collapsible?: "offcanvas" | "icon" | "none"
+ }
+>(
+ (
+ {
+ side = "left",
+ variant = "sidebar",
+ collapsible = "offcanvas",
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
+
+ if (collapsible === "none") {
+ return (
+
+ {children}
+
+ )
+ }
+
+ if (isMobile) {
+ return (
+
+
+
+ Sidebar
+ Displays the mobile sidebar.
+
+ {children}
+
+
+ )
+ }
+
+ return (
+
+ {/* This is what handles the sidebar gap on desktop */}
+
+
+
+ )
+ }
+)
+Sidebar.displayName = "Sidebar"
+
+const SidebarTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, onClick, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+ {
+ onClick?.(event)
+ toggleSidebar()
+ }}
+ {...props}
+ >
+
+ Toggle Sidebar
+
+ )
+})
+SidebarTrigger.displayName = "SidebarTrigger"
+
+const SidebarRail = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button">
+>(({ className, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+
+ )
+})
+SidebarRail.displayName = "SidebarRail"
+
+const SidebarInset = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"main">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarInset.displayName = "SidebarInset"
+
+const SidebarInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarInput.displayName = "SidebarInput"
+
+const SidebarHeader = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarHeader.displayName = "SidebarHeader"
+
+const SidebarFooter = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarFooter.displayName = "SidebarFooter"
+
+const SidebarSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarSeparator.displayName = "SidebarSeparator"
+
+const SidebarContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarContent.displayName = "SidebarContent"
+
+const SidebarGroup = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarGroup.displayName = "SidebarGroup"
+
+const SidebarGroupLabel = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "div"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarGroupLabel.displayName = "SidebarGroupLabel"
+
+const SidebarGroupAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 after:md:hidden",
+ "group-data-[collapsible=icon]:hidden",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarGroupAction.displayName = "SidebarGroupAction"
+
+const SidebarGroupContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarGroupContent.displayName = "SidebarGroupContent"
+
+const SidebarMenu = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenu.displayName = "SidebarMenu"
+
+const SidebarMenuItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenuItem.displayName = "SidebarMenuItem"
+
+const sidebarMenuButtonVariants = cva(
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+ outline:
+ "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+ },
+ size: {
+ default: "h-8 text-sm",
+ sm: "h-7 text-xs",
+ lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+const SidebarMenuButton = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean
+ isActive?: boolean
+ tooltip?: string | React.ComponentProps
+ } & VariantProps
+>(
+ (
+ {
+ asChild = false,
+ isActive = false,
+ variant = "default",
+ size = "default",
+ tooltip,
+ className,
+ ...props
+ },
+ ref
+ ) => {
+ const Comp = asChild ? Slot : "button"
+ const { isMobile, state } = useSidebar()
+
+ const button = (
+
+ )
+
+ if (!tooltip) {
+ return button
+ }
+
+ if (typeof tooltip === "string") {
+ tooltip = {
+ children: tooltip,
+ }
+ }
+
+ return (
+
+ {button}
+
+
+ )
+ }
+)
+SidebarMenuButton.displayName = "SidebarMenuButton"
+
+const SidebarMenuAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean
+ showOnHover?: boolean
+ }
+>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 after:md:hidden",
+ "peer-data-[size=sm]/menu-button:top-1",
+ "peer-data-[size=default]/menu-button:top-1.5",
+ "peer-data-[size=lg]/menu-button:top-2.5",
+ "group-data-[collapsible=icon]:hidden",
+ showOnHover &&
+ "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarMenuAction.displayName = "SidebarMenuAction"
+
+const SidebarMenuBadge = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenuBadge.displayName = "SidebarMenuBadge"
+
+const SidebarMenuSkeleton = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ showIcon?: boolean
+ }
+>(({ className, showIcon = false, ...props }, ref) => {
+ // Random width between 50 to 90%.
+ const width = React.useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`
+ }, [])
+
+ return (
+
+ {showIcon && (
+
+ )}
+
+
+ )
+})
+SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
+
+const SidebarMenuSub = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenuSub.displayName = "SidebarMenuSub"
+
+const SidebarMenuSubItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ ...props }, ref) => )
+SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
+
+const SidebarMenuSubButton = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentProps<"a"> & {
+ asChild?: boolean
+ size?: "sm" | "md"
+ isActive?: boolean
+ }
+>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+ span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
+ "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
+ size === "sm" && "text-xs",
+ size === "md" && "text-sm",
+ "group-data-[collapsible=icon]:hidden",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
+
+export {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupAction,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarInput,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSkeleton,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarProvider,
+ SidebarRail,
+ SidebarSeparator,
+ SidebarTrigger,
+ useSidebar,
+}
diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx
new file mode 100644
index 0000000..01b8b6d
--- /dev/null
+++ b/components/ui/skeleton.tsx
@@ -0,0 +1,15 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ )
+}
+
+export { Skeleton }
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}
(undefined)
+
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ }
+ mql.addEventListener("change", onChange)
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ return () => mql.removeEventListener("change", onChange)
+ }, [])
+
+ return !!isMobile
+}
diff --git a/lib/ui-builder/registry/primitive-component-definitions.ts b/lib/ui-builder/registry/primitive-component-definitions.ts
index 27f3b11..2d18bf3 100644
--- a/lib/ui-builder/registry/primitive-component-definitions.ts
+++ b/lib/ui-builder/registry/primitive-component-definitions.ts
@@ -61,4 +61,73 @@ export const primitiveComponentDefinitions: ComponentRegistry = {
},
defaultChildren: "Text"
},
+ 'h1': {
+ schema: z.object({
+ className: z.string().optional(),
+ children: z.string().optional(),
+ }),
+ fieldOverrides: {
+ ...commonFieldOverrides(),
+ children: (layer) => 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/package-lock.json b/package-lock.json
index f399e09..51ba311 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -43,7 +43,7 @@
"@tiptap/react": "^2.12.0",
"@tiptap/starter-kit": "^2.12.0",
"@use-gesture/react": "^10.3.1",
- "class-variance-authority": "^0.7.0",
+ "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
diff --git a/package.json b/package.json
index cf60795..f079e22 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,7 @@
"@tiptap/react": "^2.12.0",
"@tiptap/starter-kit": "^2.12.0",
"@use-gesture/react": "^10.3.1",
- "class-variance-authority": "^0.7.0",
+ "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png
new file mode 100644
index 0000000..05366b7
Binary files /dev/null and b/public/android-chrome-192x192.png differ
diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png
new file mode 100644
index 0000000..306428a
Binary files /dev/null and b/public/android-chrome-512x512.png differ
diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png
new file mode 100644
index 0000000..8f8cbe2
Binary files /dev/null and b/public/apple-touch-icon.png differ
diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png
new file mode 100644
index 0000000..6bef5e8
Binary files /dev/null and b/public/favicon-16x16.png differ
diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png
new file mode 100644
index 0000000..b220a37
Binary files /dev/null and b/public/favicon-32x32.png differ
diff --git a/public/favicon.ico b/public/favicon.ico
index 4965832..47ec58d 100644
Binary files a/public/favicon.ico and b/public/favicon.ico differ
diff --git a/public/logo.svg b/public/logo.svg
new file mode 100644
index 0000000..5ff31ad
--- /dev/null
+++ b/public/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/site.webmanifest b/public/site.webmanifest
new file mode 100644
index 0000000..45dc8a2
--- /dev/null
+++ b/public/site.webmanifest
@@ -0,0 +1 @@
+{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
\ No newline at end of file
diff --git a/registry/block-registry.json b/registry/block-registry.json
index 9b2d8c8..93d83f9 100644
--- a/registry/block-registry.json
+++ b/registry/block-registry.json
@@ -155,7 +155,7 @@
},
{
"path": "components/ui/ui-builder/components/codeblock.tsx",
- "content": "\"use client\";\nimport React, { memo, useCallback, useMemo } from \"react\";\nimport { Prism as SyntaxHighlighter } from \"react-syntax-highlighter\";\nimport { coldarkDark } from \"react-syntax-highlighter/dist/cjs/styles/prism\";\nimport { CopyIcon, CheckIcon } from \"lucide-react\";\nimport { useCopyToClipboard } from \"@/hooks/use-copy-to-clipboard\";\nimport { Button } from \"@/components/ui/button\";\n\ninterface CodeBlockProps {\n language: string;\n value: string;\n}\n\ninterface languageMap {\n [key: string]: string | undefined;\n}\n\nexport const programmingLanguages: languageMap = {\n javascript: \".js\",\n python: \".py\",\n java: \".java\",\n c: \".c\",\n cpp: \".cpp\",\n \"c++\": \".cpp\",\n \"c#\": \".cs\",\n ruby: \".rb\",\n php: \".php\",\n swift: \".swift\",\n \"objective-c\": \".m\",\n kotlin: \".kt\",\n typescript: \".ts\",\n go: \".go\",\n perl: \".pl\",\n rust: \".rs\",\n scala: \".scala\",\n haskell: \".hs\",\n lua: \".lua\",\n shell: \".sh\",\n sql: \".sql\",\n html: \".html\",\n css: \".css\",\n tsx: \".tsx\",\n // add more file extensions here\n};\n\nexport const CodeBlock = memo(function CodeBlock({\n language,\n value,\n}: CodeBlockProps) {\n const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });\n\n const onCopy = useCallback(() => {\n if (isCopied) return;\n copyToClipboard(value);\n }, [isCopied, copyToClipboard, value]);\n\n const customStyle = useMemo(() => ({\n margin: 0,\n width: \"100%\",\n background: \"transparent\",\n padding: \"1.5rem 1rem\",\n }), []);\n\n const codeTagProps = useMemo(() => ({\n style: {\n fontSize: \"0.9rem\",\n fontFamily: \"var(--font-mono)\",\n },\n }), []);\n\n const highlighted = useMemo(\n () => (\n \n {value}\n \n ),\n [language, value, customStyle, codeTagProps]\n );\n\n return (\n \n
\n
{language} \n
\n \n {isCopied ? : }\n Copy code \n \n
\n
\n {highlighted}\n
\n );\n});\n",
+ "content": "\"use client\";\nimport React, { memo, useCallback, useMemo } from \"react\";\nimport { Prism as SyntaxHighlighter } from \"react-syntax-highlighter\";\nimport { coldarkDark } from \"react-syntax-highlighter/dist/cjs/styles/prism\";\nimport { CopyIcon, CheckIcon } from \"lucide-react\";\nimport { useCopyToClipboard } from \"@/hooks/use-copy-to-clipboard\";\nimport { Button } from \"@/components/ui/button\";\n\ninterface CodeBlockProps {\n language: string;\n value: string;\n}\n\ninterface languageMap {\n [key: string]: string | undefined;\n}\n\nexport const programmingLanguages: languageMap = {\n javascript: \".js\",\n python: \".py\",\n java: \".java\",\n c: \".c\",\n cpp: \".cpp\",\n \"c++\": \".cpp\",\n \"c#\": \".cs\",\n ruby: \".rb\",\n php: \".php\",\n swift: \".swift\",\n \"objective-c\": \".m\",\n kotlin: \".kt\",\n typescript: \".ts\",\n go: \".go\",\n perl: \".pl\",\n rust: \".rs\",\n scala: \".scala\",\n haskell: \".hs\",\n lua: \".lua\",\n shell: \".sh\",\n sql: \".sql\",\n html: \".html\",\n css: \".css\",\n tsx: \".tsx\",\n // add more file extensions here\n};\n\nexport const CodeBlock = memo(function CodeBlock({\n language,\n value,\n}: CodeBlockProps) {\n const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });\n\n const onCopy = useCallback(() => {\n if (isCopied) return;\n copyToClipboard(value);\n }, [isCopied, copyToClipboard, value]);\n\n const customStyle = useMemo(() => ({\n margin: 0,\n width: \"100%\",\n background: \"transparent\",\n padding: \"1.5rem 1rem\",\n }), []);\n\n const codeTagProps = useMemo(() => ({\n style: {\n fontSize: \"0.9rem\",\n fontFamily: \"var(--font-mono)\",\n },\n }), []);\n\n const highlighted = useMemo(\n () => (\n \n {value}\n \n ),\n [language, value, customStyle, codeTagProps]\n );\n\n return (\n \n
\n
{language} \n
\n \n {isCopied ? : }\n Copy code \n \n
\n
\n {highlighted}\n
\n );\n});\n",
"type": "registry:ui",
"target": "components/ui/ui-builder/components/codeblock.tsx"
},
@@ -363,24 +363,6 @@
"type": "registry:lib",
"target": "lib/ui-builder/utils/get-scroll-parent.tsx"
},
- {
- "path": "lib/ui-builder/registry/primitive-component-definitions.ts",
- "content": "import { ComponentRegistry } from '@/components/ui/ui-builder/types';\nimport { z } from 'zod';\nimport { childrenAsTextareaFieldOverrides, classNameFieldOverrides, commonFieldOverrides } from \"@/lib/ui-builder/registry/form-field-overrides\";\n\nexport const primitiveComponentDefinitions: ComponentRegistry = {\n 'a': {\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n href: z.string().optional(),\n target: z.enum(['_blank', '_self', '_parent', '_top']).optional().default('_self'),\n rel: z.enum(['noopener', 'noreferrer', 'nofollow']).optional(),\n title: z.string().optional(),\n download: z.boolean().optional().default(false),\n }),\n fieldOverrides: commonFieldOverrides()\n },\n 'img': {\n schema: z.object({\n className: z.string().optional(),\n src: z.string().default(\"https://placehold.co/200\"),\n alt: z.string().optional(),\n width: z.coerce.number().optional(),\n height: z.coerce.number().optional(),\n }),\n fieldOverrides: {\n className:(layer)=> classNameFieldOverrides(layer)\n }\n },\n 'div': {\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n fieldOverrides: commonFieldOverrides()\n },\n 'iframe': {\n schema: z.object({\n className: z.string().optional(),\n src: z.string().default(\"https://www.youtube.com/embed/dQw4w9WgXcQ?si=oc74qTYUBuCsOJwL\"),\n title: z.string().optional(),\n width: z.coerce.number().optional(),\n height: z.coerce.number().optional(),\n frameBorder: z.number().optional(),\n allowFullScreen: z.boolean().optional(),\n allow: z.string().optional(),\n referrerPolicy: z.enum(['no-referrer', 'no-referrer-when-downgrade', 'origin', 'origin-when-cross-origin', 'same-origin', 'strict-origin', 'strict-origin-when-cross-origin', 'unsafe-url']).optional(),\n }),\n fieldOverrides: {\n className:(layer)=> classNameFieldOverrides(layer)\n }\n },\n 'span': {\n schema: z.object({\n className: z.string().optional(),\n children: z.string().optional(),\n }),\n fieldOverrides: {\n className:(layer)=> classNameFieldOverrides(layer),\n children: (layer)=> childrenAsTextareaFieldOverrides(layer)\n },\n defaultChildren: \"Text\"\n },\n};\n",
- "type": "registry:lib",
- "target": "lib/ui-builder/registry/primitive-component-definitions.ts"
- },
- {
- "path": "lib/ui-builder/registry/form-field-overrides.tsx",
- "content": "import React from \"react\";\nimport {\n FormControl,\n FormDescription,\n FormItem,\n FormLabel,\n} from \"@/components/ui/form\";\nimport { ChildrenSearchableSelect } from \"@/components/ui/ui-builder/internal/form-fields/children-searchable-select\";\nimport {\n AutoFormInputComponentProps,\n ComponentLayer,\n FieldConfigFunction,\n} from \"@/components/ui/ui-builder/types\";\nimport IconNameField from \"@/components/ui/ui-builder/internal/form-fields/iconname-field\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { MinimalTiptapEditor } from \"@/components/ui/minimal-tiptap\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { useLayerStore } from \"../store/layer-store\";\nimport { isVariableReference } from \"../utils/variable-resolver\";\nimport { Link, LockKeyhole, Unlink } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { Input } from \"@/components/ui/input\";\nimport { useEditorStore } from \"../store/editor-store\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport BreakpointClassNameControl from \"@/components/ui/ui-builder/internal/form-fields/classname-control\";\nimport { Label } from \"@/components/ui/label\";\nimport { Badge } from \"@/components/ui/badge\";\n\nexport const classNameFieldOverrides: FieldConfigFunction = (\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n layer,\n) => {\n return {\n fieldType: ({\n label,\n isRequired,\n field,\n fieldConfigItem,\n }: AutoFormInputComponentProps) => (\n \n \n \n ),\n };\n};\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport const childrenFieldOverrides: FieldConfigFunction = (\n layer,\n) => {\n return {\n fieldType: ({\n label,\n isRequired,\n fieldConfigItem,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n \n \n ),\n };\n};\n\nexport const iconNameFieldOverrides: FieldConfigFunction = (layer) => {\n return {\n fieldType: ({\n label,\n isRequired,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n ),\n };\n};\n\nexport const childrenAsTextareaFieldOverrides: FieldConfigFunction = (\n layer\n) => {\n return {\n fieldType: ({\n label,\n isRequired,\n fieldConfigItem,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n \n \n ),\n };\n};\n\nexport const childrenAsTipTapFieldOverrides: FieldConfigFunction = (\n layer,\n) => {\n return {\n fieldType: ({\n label,\n isRequired,\n fieldConfigItem,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n {\n //if string call field.onChange\n if (typeof content === \"string\") {\n field.onChange(content);\n } else {\n console.warn(\"Tiptap content is not a string\");\n }\n }}\n {...fieldProps}\n />\n \n ),\n };\n};\n\n// Memoized common field overrides to avoid recreating objects\nconst memoizedCommonFieldOverrides = new Map>();\n\nexport const commonFieldOverrides = (allowBinding = false) => {\n if (memoizedCommonFieldOverrides.has(allowBinding)) {\n return memoizedCommonFieldOverrides.get(allowBinding)!;\n }\n \n const overrides = {\n className: (layer: ComponentLayer) => classNameFieldOverrides(layer),\n children: (layer: ComponentLayer) => childrenFieldOverrides(layer),\n };\n \n memoizedCommonFieldOverrides.set(allowBinding, overrides);\n return overrides;\n};\n\nexport const commonVariableRenderParentOverrides = (propName: string) => {\n return {\n renderParent: ({ children }: { children: React.ReactNode }) => (\n {children} \n ),\n };\n};\n\nexport const textInputFieldOverrides = (\n layer: ComponentLayer,\n allowVariableBinding = false,\n propName: string\n) => {\n return {\n renderParent: allowVariableBinding\n ? ({ children }: { children: React.ReactNode }) => (\n \n {children}\n \n )\n : undefined,\n fieldType: ({\n label,\n isRequired,\n fieldConfigItem,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n field.onChange(e.target.value)}\n {...fieldProps}\n />\n \n ),\n };\n};\n\nexport function VariableBindingWrapper({\n propName,\n children,\n}: {\n propName: string;\n children: React.ReactNode;\n}) {\n const variables = useLayerStore((state) => state.variables);\n const selectedLayerId = useLayerStore((state) => state.selectedLayerId);\n const findLayerById = useLayerStore((state) => state.findLayerById);\n const isBindingImmutable = useLayerStore((state) => state.isBindingImmutable);\n const incrementRevision = useEditorStore((state) => state.incrementRevision);\n const unbindPropFromVariable = useLayerStore(\n (state) => state.unbindPropFromVariable\n );\n const bindPropToVariable = useLayerStore((state) => state.bindPropToVariable);\n\n const selectedLayer = findLayerById(selectedLayerId);\n\n // If variable binding is not allowed or no propName provided, just render the form wrapper\n if (!selectedLayer) {\n return <>{children}>;\n }\n\n const currentValue = selectedLayer.props[propName];\n const isCurrentlyBound = isVariableReference(currentValue);\n const boundVariable = isCurrentlyBound\n ? variables.find((v) => v.id === currentValue.__variableRef)\n : null;\n const isImmutable = isBindingImmutable(selectedLayer.id, propName);\n\n const handleBindToVariable = (variableId: string) => {\n bindPropToVariable(selectedLayer.id, propName, variableId);\n incrementRevision();\n };\n\n // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop\n const handleUnbind = () => {\n // Use the new unbind function which sets default value from schema\n unbindPropFromVariable(selectedLayer.id, propName);\n incrementRevision();\n };\n\n return (\n \n {isCurrentlyBound && boundVariable ? (\n // Bound state - show variable info and unbind button\n
\n
{propName.charAt(0).toUpperCase() + propName.slice(1)} \n
\n
\n \n \n
\n
\n
\n {boundVariable.name} \n \n {boundVariable.type}\n \n {isImmutable && (\n \n \n \n )}\n
\n
\n {String(boundVariable.defaultValue)}\n \n
\n
\n \n \n {!isImmutable && (\n
\n \n \n \n \n \n Unbind Variable \n \n )}\n
\n
\n ) : (\n // Unbound state - show normal field with bind button\n <>\n
{children}
\n
\n
\n \n \n \n \n \n \n \n \n Bind Variable \n \n \n \n Bind to Variable\n
\n {variables.length > 0 ? (\n variables.map((variable) => (\n handleBindToVariable(variable.id)}\n className=\"flex flex-col items-start p-3\"\n >\n \n
\n
\n
\n {variable.name} \n \n {variable.type}\n \n
\n
\n {String(variable.defaultValue)}\n \n
\n
\n \n ))\n ) : (\n \n No variables defined\n
\n )}\n \n \n
\n >\n )}\n
\n );\n}\n\nexport function FormFieldWrapper({\n label,\n isRequired,\n fieldConfigItem,\n children,\n}: {\n label: string;\n isRequired?: boolean;\n fieldConfigItem?: { description?: React.ReactNode };\n children: React.ReactNode;\n}) {\n return (\n \n \n {label}\n {isRequired && * }\n \n {children} \n {fieldConfigItem?.description && (\n {fieldConfigItem.description} \n )}\n \n );\n}\n",
- "type": "registry:lib",
- "target": "lib/ui-builder/registry/form-field-overrides.tsx"
- },
- {
- "path": "lib/ui-builder/registry/complex-component-definitions.ts",
- "content": "import { ComponentRegistry } from '@/components/ui/ui-builder/types';\nimport { z } from 'zod';\nimport { Button } from '@/components/ui/button';\nimport { Badge } from '@/components/ui/badge';\nimport { Flexbox } from '@/components/ui/ui-builder/components/flexbox';\nimport { Grid } from '@/components/ui/ui-builder/components/grid';\nimport { CodePanel } from '@/components/ui/ui-builder/components/code-panel';\nimport { Markdown } from \"@/components/ui/ui-builder/components/markdown\";\nimport { Icon, iconNames } from \"@/components/ui/ui-builder/components/icon\";\nimport { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from \"@/components/ui/accordion\";\nimport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from \"@/components/ui/card\";\nimport { classNameFieldOverrides, childrenFieldOverrides, iconNameFieldOverrides, commonFieldOverrides, childrenAsTipTapFieldOverrides } from \"@/lib/ui-builder/registry/form-field-overrides\";\nimport { ComponentLayer } from '@/components/ui/ui-builder/types';\n\nexport const complexComponentDefinitions: ComponentRegistry = {\n Button: {\n component: Button,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n asChild: z.boolean().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: \"Button\",\n } satisfies ComponentLayer,\n ],\n fieldOverrides: commonFieldOverrides()\n },\n Badge: {\n component: Badge,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n variant: z\n .enum([\"default\", \"secondary\", \"destructive\", \"outline\"])\n .default(\"default\"),\n }),\n from: \"@/components/ui/badge\",\n defaultChildren: [\n {\n id: \"badge-text\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Badge\",\n } satisfies ComponentLayer,\n ],\n fieldOverrides: commonFieldOverrides()\n },\n Flexbox: {\n component: Flexbox,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n direction: z\n .enum([\"row\", \"column\", \"rowReverse\", \"columnReverse\"])\n .default(\"row\"),\n justify: z\n .enum([\"start\", \"end\", \"center\", \"between\", \"around\", \"evenly\"])\n .default(\"start\"),\n align: z\n .enum([\"start\", \"end\", \"center\", \"baseline\", \"stretch\"])\n .default(\"start\"),\n wrap: z.enum([\"wrap\", \"nowrap\", \"wrapReverse\"]).default(\"nowrap\"),\n gap: z\n .preprocess(\n (val) => (typeof val === 'number' ? String(val) : val),\n z.enum([\"0\", \"1\", \"2\", \"4\", \"8\"]).default(\"1\")\n )\n .transform(Number),\n }),\n from: \"@/components/ui/ui-builder/flexbox\",\n fieldOverrides: commonFieldOverrides()\n },\n Grid: {\n component: Grid,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n columns: z\n .enum([\"auto\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\"])\n .default(\"1\"),\n autoRows: z.enum([\"none\", \"min\", \"max\", \"fr\"]).default(\"none\"),\n justify: z\n .enum([\"start\", \"end\", \"center\", \"between\", \"around\", \"evenly\"])\n .default(\"start\"),\n align: z\n .enum([\"start\", \"end\", \"center\", \"baseline\", \"stretch\"])\n .default(\"start\"),\n templateRows: z\n .enum([\"none\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\"])\n .default(\"none\")\n .transform(val => (val === \"none\" ? val : Number(val))),\n gap: z\n .preprocess(\n (val) => (typeof val === 'number' ? String(val) : val),\n z.enum([\"0\", \"1\", \"2\", \"4\", \"8\"]).default(\"0\")\n )\n .transform(Number),\n }),\n from: \"@/components/ui/ui-builder/grid\",\n fieldOverrides: commonFieldOverrides()\n },\n CodePanel: {\n component: CodePanel,\n schema: z.object({\n className: z.string().optional(),\n }),\n from: \"@/components/ui/ui-builder/code-panel\",\n fieldOverrides: {\n className:(layer)=> classNameFieldOverrides(layer)\n }\n },\n Markdown: {\n component: Markdown,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: \"@/components/ui/ui-builder/markdown\",\n fieldOverrides: {\n className:(layer)=> classNameFieldOverrides(layer),\n children: (layer)=> childrenAsTipTapFieldOverrides(layer)\n }\n },\n Icon: {\n component: Icon,\n schema: z.object({\n className: z.string().optional(),\n iconName: z.enum([...iconNames]).default(\"Image\"),\n size: z.enum([\"small\", \"medium\", \"large\"]).default(\"medium\"),\n color: z\n .enum([\n \"accent\",\n \"accentForeground\",\n \"primary\",\n \"primaryForeground\",\n \"secondary\",\n \"secondaryForeground\",\n \"destructive\",\n \"destructiveForeground\",\n \"muted\",\n \"mutedForeground\",\n \"background\",\n \"foreground\",\n ])\n .optional(),\n rotate: z.enum([\"none\", \"90\", \"180\", \"270\"]).default(\"none\"),\n }),\n from: \"@/components/ui/ui-builder/icon\",\n fieldOverrides: {\n className:(layer)=> classNameFieldOverrides(layer),\n iconName: (layer)=> iconNameFieldOverrides(layer)\n }\n },\n\n //Accordion\n Accordion: {\n component: Accordion,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n type: z.enum([\"single\", \"multiple\"]).default(\"single\"),\n collapsible: z.boolean().optional(),\n }),\n from: \"@/components/ui/accordion\",\n defaultChildren: [\n {\n id: \"acc-item-1\",\n type: \"AccordionItem\",\n name: \"AccordionItem\",\n props: {\n value: \"item-1\",\n },\n children: [\n {\n id: \"acc-trigger-1\",\n type: \"AccordionTrigger\",\n name: \"AccordionTrigger\",\n props: {},\n children: [\n {\n id: \"WEz8Yku\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Accordion Item #1\",\n } satisfies ComponentLayer,\n ],\n },\n {\n id: \"acc-content-1\",\n type: \"AccordionContent\",\n name: \"AccordionContent\",\n props: {},\n children: [\n {\n id: \"acc-content-1-text-1\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Accordion Content Text\",\n } satisfies ComponentLayer,\n ],\n },\n ],\n },\n {\n id: \"acc-item-2\",\n type: \"AccordionItem\",\n name: \"AccordionItem\",\n props: {\n value: \"item-2\",\n },\n children: [\n {\n id: \"acc-trigger-2\",\n type: \"AccordionTrigger\",\n name: \"AccordionTrigger\",\n props: {},\n children: [\n {\n id: \"acc-trigger-2-text-1\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Accordion Item #2\",\n } satisfies ComponentLayer,\n ],\n },\n {\n id: \"acc-content-2\",\n type: \"AccordionContent\",\n name: \"AccordionContent (Copy)\",\n props: {},\n children: [\n {\n id: \"acc-content-2-text-1\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Accordion Content Text\",\n } satisfies ComponentLayer,\n ],\n },\n ],\n },\n ],\n fieldOverrides: commonFieldOverrides()\n },\n AccordionItem: {\n component: AccordionItem,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n value: z.string(),\n }),\n from: \"@/components/ui/accordion\",\n defaultChildren: [\n {\n id: \"acc-trigger-1\",\n type: \"AccordionTrigger\",\n name: \"AccordionTrigger\",\n props: {},\n children: [\n {\n id: \"WEz8Yku\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Accordion Item #1\",\n } satisfies ComponentLayer,\n ],\n },\n {\n id: \"acc-content-1\",\n type: \"AccordionContent\",\n name: \"AccordionContent\",\n props: {},\n children: [\n {\n id: \"acc-content-1-text-1\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Accordion Content Text\",\n } satisfies ComponentLayer,\n ],\n },\n ],\n fieldOverrides: commonFieldOverrides()\n },\n AccordionTrigger: {\n component: AccordionTrigger,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: \"@/components/ui/accordion\",\n fieldOverrides: {\n className:(layer)=> classNameFieldOverrides(layer),\n children: (layer)=> childrenFieldOverrides(layer)\n }\n },\n AccordionContent: {\n component: AccordionContent,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: \"@/components/ui/accordion\",\n fieldOverrides: commonFieldOverrides()\n },\n\n //Card\n Card: {\n component: Card,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: '@/components/ui/card',\n defaultChildren: [\n {\n id: \"card-header\",\n type: \"CardHeader\",\n name: \"CardHeader\",\n props: {},\n children: [\n {\n id: \"card-title\",\n type: \"CardTitle\",\n name: \"CardTitle\",\n props: {},\n children: [\n {\n id: \"card-title-text\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Card Title\",\n } satisfies ComponentLayer,\n ],\n },\n {\n id: \"card-description\",\n type: \"CardDescription\",\n name: \"CardDescription\",\n props: {},\n children: [\n {\n id: \"card-description-text\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Card Description\",\n } satisfies ComponentLayer,\n ],\n },\n ],\n },\n {\n id: \"card-content\",\n type: \"CardContent\",\n name: \"CardContent\",\n props: {},\n children: [\n {\n id: \"card-content-paragraph\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Card Content\",\n } satisfies ComponentLayer,\n ],\n },\n {\n id: \"card-footer\",\n type: \"CardFooter\",\n name: \"CardFooter\",\n props: {},\n children: [\n {\n id: \"card-footer-paragraph\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Card Footer\",\n } satisfies ComponentLayer,\n ],\n },\n ],\n fieldOverrides: commonFieldOverrides()\n },\n CardHeader: {\n component: CardHeader,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: '@/components/ui/card',\n fieldOverrides: commonFieldOverrides()\n },\n CardFooter: {\n component: CardFooter,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: '@/components/ui/card',\n fieldOverrides: commonFieldOverrides()\n },\n CardTitle: {\n component: CardTitle,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: '@/components/ui/card',\n fieldOverrides: commonFieldOverrides()\n },\n CardDescription: {\n component: CardDescription,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: '@/components/ui/card',\n fieldOverrides: commonFieldOverrides()\n },\n CardContent: {\n component: CardContent,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: '@/components/ui/card',\n fieldOverrides: commonFieldOverrides()\n },\n};",
- "type": "registry:lib",
- "target": "lib/ui-builder/registry/complex-component-definitions.ts"
- },
{
"path": "lib/ui-builder/store/schema-utils.ts",
"content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { z, ZodObject, ZodTypeAny, ZodDate, ZodNumber, ZodEnum, ZodOptional, ZodNullable, ZodDefault, ZodArray, ZodRawShape, ZodLiteral, ZodUnion, ZodTuple, ZodString, ZodAny } from 'zod';\n\n/**\n * Generates default props based on the provided Zod schema.\n * Supports boolean, date, number, string, enum, objects composed of these primitives, and arrays of these primitives.\n * Logs a warning for unsupported types.\n *\n * @param schema - The Zod schema object.\n * @returns An object containing default values for the schema.\n */\nexport function getDefaultProps(schema: ZodObject): Record {\n const shape = schema.shape;\n const defaultProps: Record = {};\n\n for (const key in shape) {\n if (Object.prototype.hasOwnProperty.call(shape, key)) {\n const fieldSchema = shape[key];\n const value = getDefaultValue(fieldSchema, key);\n if(value !== undefined){\n defaultProps[key] = value;\n }\n }\n }\n\n return defaultProps;\n}\n\n/**\n * Determines the default value for a given Zod schema.\n * Handles nullable and coerced fields appropriately.\n *\n * @param schema - The Zod schema for the field.\n * @param fieldName - The name of the field (used for logging).\n * @returns The default value for the field.\n */\nfunction getDefaultValue(schema: ZodTypeAny, fieldName: string): any {\n // Handle ZodDefault to return the specified default value\n if (schema instanceof ZodDefault) {\n return schema._def.defaultValue();\n }\n\n if (!schema.isOptional()){\n console.warn(`No default value set for required field \"${fieldName}\".`);\n }\n return undefined;\n}\n\n/**\n * Patches the given Zod object schema by transforming unions of literals to enums,\n * coercing number and date types, and adding an optional `className` property.\n *\n * @param schema - The original Zod object schema to be patched.\n * @returns A new Zod object schema with the specified transformations applied.\n */\n\nexport function patchSchema(schema: ZodObject): ZodObject {\n const schemaWithFixedEnums = transformUnionToEnum(schema);\n const schemaWithCoercedTypes = addCoerceToNumberAndDate(schemaWithFixedEnums);\n const schemaWithCommon = addCommon(schemaWithCoercedTypes);\n\n return schemaWithCommon;\n}\n\n/**\n * Extends the given Zod object schema by adding an optional `className` property.\n *\n * @param schema - The original Zod object schema.\n * @returns A new Zod object schema with the `className` property added.\n */\nfunction addCommon(\n schema: ZodObject\n) {\n return schema.extend({\n className: z.string().optional(),\n });\n}\n\n/**\n * Transforms a ZodUnion of ZodLiterals into a ZodEnum with a default value.\n * If the schema is nullable or optional, it recursively applies the transformation to the inner schema.\n *\n * @param schema - The original Zod schema, which can be a ZodUnion, ZodNullable, ZodOptional, or ZodObject.\n * @returns A transformed Zod schema with unions of literals converted to enums, or the original schema if no transformation is needed.\n */\nfunction transformUnionToEnum(schema: T): T {\n // Handle ZodUnion of string literals\n if (schema instanceof ZodUnion) {\n const options = schema.options;\n\n // Check if all options are ZodLiteral instances with string values\n if (\n options.every(\n (option: any) => option instanceof ZodLiteral && typeof option._def.value === 'string'\n )\n ) {\n const enumValues = options.map(\n (option: ZodLiteral) => option.value\n ).reverse();\n\n // Ensure there is at least one value to create an enum\n if (enumValues.length === 0) {\n throw new Error(\"Cannot create enum with no values.\");\n }\n\n // Create a ZodEnum from the string literals\n const enumSchema = z.enum(enumValues as [string, ...string[]]);\n\n // Determine if the original schema was nullable or optional\n let transformedSchema: ZodTypeAny = enumSchema;\n\n // Apply default before adding modifiers to ensure it doesn't get overridden\n transformedSchema = enumSchema.default(enumValues[0]);\n\n\n if (schema.isNullable()) {\n transformedSchema = transformedSchema.nullable();\n }\n\n if (schema.isOptional()) {\n transformedSchema = transformedSchema.optional();\n }\n\n return transformedSchema as unknown as T;\n }\n }\n\n // Recursively handle nullable and optional schemas\n if (schema instanceof ZodNullable) {\n const inner = schema.unwrap();\n const transformedInner = transformUnionToEnum(inner);\n return transformedInner.nullable() as any;\n }\n\n if (schema instanceof ZodOptional) {\n const inner = schema.unwrap();\n const transformedInner = transformUnionToEnum(inner);\n return transformedInner.optional() as any;\n }\n\n // Recursively handle ZodObjects by transforming their shape\n if (schema instanceof ZodObject) {\n const transformedShape: Record = {};\n\n for (const [key, value] of Object.entries(schema.shape)) {\n transformedShape[key] = transformUnionToEnum(value as ZodTypeAny);\n }\n\n return z.object(transformedShape) as unknown as T;\n }\n\n // Handle ZodArrays by transforming their element type\n if (schema instanceof ZodArray) {\n const transformedElement = transformUnionToEnum(schema.element);\n return z.array(transformedElement) as unknown as T;\n }\n\n // Handle ZodTuples by transforming each element type\n if (schema instanceof ZodTuple) {\n const transformedItems = schema.items.map((item: any) => transformUnionToEnum(item));\n return z.tuple(transformedItems) as unknown as T;\n }\n\n // If none of the above, return the schema unchanged\n return schema;\n}\n\n/**\n * Recursively applies coercion to number and date fields within the given Zod schema.\n * Handles nullable, optional, objects, arrays, unions, and enums appropriately to ensure type safety.\n *\n * @param schema - The original Zod schema to transform.\n * @returns A new Zod schema with coercions applied where necessary.\n */\nfunction addCoerceToNumberAndDate(schema: T): T {\n // Handle nullable schemas\n if (schema instanceof ZodNullable) {\n const inner = schema.unwrap();\n return addCoerceToNumberAndDate(inner).nullable() as any;\n }\n\n // Handle optional schemas\n if (schema instanceof ZodOptional) {\n const inner = schema.unwrap();\n return addCoerceToNumberAndDate(inner).optional() as any;\n }\n\n // Handle objects by recursively applying the transformation to each property\n if (schema instanceof ZodObject) {\n const shape: ZodRawShape = schema.shape;\n const transformedShape: ZodRawShape = {};\n\n for (const [key, value] of Object.entries(shape)) {\n transformedShape[key] = addCoerceToNumberAndDate(value);\n }\n\n return z.object(transformedShape) as any;\n }\n\n // Handle arrays by applying the transformation to the array's element type\n if (schema instanceof ZodArray) {\n const innerType = schema.element;\n return z.array(addCoerceToNumberAndDate(innerType)) as any;\n }\n\n // Apply coercion to number fields\n if (schema instanceof ZodNumber) {\n return z.coerce.number().optional() as any; // Adjust `.optional()` based on your schema requirements\n }\n\n // Apply coercion to date fields\n if (schema instanceof ZodDate) {\n return z.coerce.date().optional() as any; // Adjust `.optional()` based on your schema requirements\n }\n\n // Handle unions by applying the transformation to each option\n if (schema instanceof ZodUnion) {\n const transformedOptions = schema.options.map((option: any) => addCoerceToNumberAndDate(option));\n return z.union(transformedOptions) as any;\n }\n\n // Handle enums by returning them as-is\n if (schema instanceof ZodEnum) {\n return schema;\n }\n\n // If none of the above, return the schema unchanged\n return schema;\n}\n\n// patch for autoform to respect existing values, specifically for enums\nexport function addDefaultValues>(\n schema: T,\n defaultValues: Partial>\n): T {\n const shape = schema.shape;\n\n const updatedShape = { ...shape };\n\n for (const key in defaultValues) {\n if (updatedShape[key]) {\n // Apply the default value to the existing schema field\n updatedShape[key] = updatedShape[key].default(defaultValues[key]);\n } else if (process.env.NODE_ENV !== \"production\") {\n console.warn(\n `Key \"${key}\" does not exist in the schema and will be ignored.`\n );\n }\n }\n\n return z.object(updatedShape) as T;\n}\n\n/**\n * Checks if a Zod schema has a children field of type ANY\n */\nexport function hasAnyChildrenField(schema: ZodObject): boolean {\n const shape = schema.shape;\n if (!shape.children) {\n return false;\n }\n \n // Unwrap optional and nullable wrappers to get the inner type\n let childrenSchema = shape.children;\n while (childrenSchema instanceof ZodOptional || childrenSchema instanceof ZodNullable) {\n childrenSchema = childrenSchema.unwrap();\n }\n \n return childrenSchema instanceof ZodAny;\n}\n\n/**\n* Checks if a Zod schema has a children field of type String\n*/\nexport function hasChildrenFieldOfTypeString(schema: ZodObject): boolean {\n const shape = schema.shape;\n if (!shape.children) {\n return false;\n }\n \n // Unwrap optional and nullable wrappers to get the inner type\n let childrenSchema = shape.children;\n while (childrenSchema instanceof ZodOptional || childrenSchema instanceof ZodNullable) {\n childrenSchema = childrenSchema.unwrap();\n }\n \n return childrenSchema instanceof ZodString;\n}",
@@ -412,34 +394,22 @@
"target": "lib/ui-builder/store/editor-store.ts"
},
{
- "path": "lib/ui-builder/hooks/use-keyboard-shortcuts-dnd.ts",
- "content": "import { useEffect } from 'react';\n\nexport const useKeyboardShortcutsDnd = (activeLayerId: string | null, handleDragCancel: () => void) => {\n // Handle escape key to cancel drag operations\n useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape' && activeLayerId) {\n event.preventDefault();\n event.stopPropagation();\n handleDragCancel();\n }\n };\n\n // Only add the listener when actively dragging\n if (activeLayerId) {\n document.addEventListener('keydown', handleKeyDown);\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n };\n }\n }, [activeLayerId, handleDragCancel]);\n}; ",
- "type": "registry:lib",
- "target": "lib/ui-builder/hooks/use-keyboard-shortcuts-dnd.ts"
- },
- {
- "path": "lib/ui-builder/hooks/use-drop-validation.ts",
- "content": "import { useCallback } from 'react';\nimport { useLayerStore } from '@/lib/ui-builder/store/layer-store';\nimport { useEditorStore } from '@/lib/ui-builder/store/editor-store';\nimport { canLayerAcceptChildren } from '@/lib/ui-builder/store/layer-utils';\n\nexport const useDropValidation = (activeLayerId: string | null, isLayerDescendantOf: (childId: string, parentId: string) => boolean) => {\n const findLayerById = useLayerStore((state) => state.findLayerById);\n const componentRegistry = useEditorStore((state) => state.registry);\n\n const canDropOnLayer = useCallback((layerId: string): boolean => {\n const targetLayer = findLayerById(layerId);\n if (!targetLayer) return false;\n\n // If no active drag, check if layer can accept children\n if (!activeLayerId) {\n return canLayerAcceptChildren(targetLayer, componentRegistry);\n }\n \n // Don't allow dropping onto self or descendants\n if (isLayerDescendantOf(layerId, activeLayerId)) {\n return false;\n }\n\n const draggedLayer = findLayerById(activeLayerId);\n if (!draggedLayer) return false;\n\n return canLayerAcceptChildren(targetLayer, componentRegistry);\n }, [activeLayerId, isLayerDescendantOf, findLayerById, componentRegistry]);\n\n return { canDropOnLayer };\n}; ",
- "type": "registry:lib",
- "target": "lib/ui-builder/hooks/use-drop-validation.ts"
- },
- {
- "path": "lib/ui-builder/hooks/use-dnd-sensors.ts",
- "content": "import { useSensors, useSensor, MouseSensor, TouchSensor } from '@dnd-kit/core';\n\nexport const useDndSensors = () => {\n return useSensors(\n useSensor(MouseSensor, {\n activationConstraint: {\n distance: 4,\n },\n }),\n useSensor(TouchSensor, {\n activationConstraint: {\n delay: 100,\n tolerance: 8,\n },\n })\n );\n}; ",
+ "path": "lib/ui-builder/registry/primitive-component-definitions.ts",
+ "content": "import { ComponentRegistry } from '@/components/ui/ui-builder/types';\nimport { z } from 'zod';\nimport { childrenAsTextareaFieldOverrides, classNameFieldOverrides, commonFieldOverrides } from \"@/lib/ui-builder/registry/form-field-overrides\";\n\nexport const primitiveComponentDefinitions: ComponentRegistry = {\n 'a': {\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n href: z.string().optional(),\n target: z.enum(['_blank', '_self', '_parent', '_top']).optional().default('_self'),\n rel: z.enum(['noopener', 'noreferrer', 'nofollow']).optional(),\n title: z.string().optional(),\n download: z.boolean().optional().default(false),\n }),\n fieldOverrides: commonFieldOverrides()\n },\n 'img': {\n schema: z.object({\n className: z.string().optional(),\n src: z.string().default(\"https://placehold.co/200\"),\n alt: z.string().optional(),\n width: z.coerce.number().optional(),\n height: z.coerce.number().optional(),\n }),\n fieldOverrides: {\n className:(layer)=> classNameFieldOverrides(layer)\n }\n },\n 'div': {\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n fieldOverrides: commonFieldOverrides()\n },\n 'iframe': {\n schema: z.object({\n className: z.string().optional(),\n src: z.string().default(\"https://www.youtube.com/embed/dQw4w9WgXcQ?si=oc74qTYUBuCsOJwL\"),\n title: z.string().optional(),\n width: z.coerce.number().optional(),\n height: z.coerce.number().optional(),\n frameBorder: z.number().optional(),\n allowFullScreen: z.boolean().optional(),\n allow: z.string().optional(),\n referrerPolicy: z.enum(['no-referrer', 'no-referrer-when-downgrade', 'origin', 'origin-when-cross-origin', 'same-origin', 'strict-origin', 'strict-origin-when-cross-origin', 'unsafe-url']).optional(),\n }),\n fieldOverrides: {\n className:(layer)=> classNameFieldOverrides(layer)\n }\n },\n 'span': {\n schema: z.object({\n className: z.string().optional(),\n children: z.string().optional(),\n }),\n fieldOverrides: {\n className:(layer)=> classNameFieldOverrides(layer),\n children: (layer)=> childrenAsTextareaFieldOverrides(layer)\n },\n defaultChildren: \"Text\"\n },\n 'h1': {\n schema: z.object({\n className: z.string().optional(),\n children: z.string().optional(),\n }),\n fieldOverrides: {\n ...commonFieldOverrides(),\n children: (layer) => childrenAsTextareaFieldOverrides(layer)\n },\n defaultChildren: \"Heading 1\"\n },\n 'h2': {\n schema: z.object({\n className: z.string().optional(),\n children: z.string().optional(),\n }),\n fieldOverrides: {\n ...commonFieldOverrides(),\n children: (layer) => childrenAsTextareaFieldOverrides(layer)\n },\n defaultChildren: \"Heading 2\"\n },\n 'h3': {\n schema: z.object({\n className: z.string().optional(),\n children: z.string().optional(),\n }),\n fieldOverrides: {\n ...commonFieldOverrides(),\n children: (layer) => childrenAsTextareaFieldOverrides(layer)\n },\n defaultChildren: \"Heading 3\"\n },\n 'p': {\n schema: z.object({\n className: z.string().optional(),\n children: z.string().optional(),\n }),\n fieldOverrides: {\n ...commonFieldOverrides(),\n children: (layer) => childrenAsTextareaFieldOverrides(layer)\n },\n defaultChildren: \"Paragraph text\"\n },\n 'li': {\n schema: z.object({\n className: z.string().optional(),\n children: z.string().optional(),\n }),\n fieldOverrides: {\n ...commonFieldOverrides(),\n children: (layer) => childrenAsTextareaFieldOverrides(layer)\n },\n defaultChildren: \"List item\"\n },\n 'ul': {\n schema: z.object({\n className: z.string().optional(),\n children: z.string().optional(),\n }),\n fieldOverrides: commonFieldOverrides()\n },\n 'ol': {\n schema: z.object({\n className: z.string().optional(),\n children: z.string().optional(),\n }),\n fieldOverrides: commonFieldOverrides()\n }\n};\n",
"type": "registry:lib",
- "target": "lib/ui-builder/hooks/use-dnd-sensors.ts"
+ "target": "lib/ui-builder/registry/primitive-component-definitions.ts"
},
{
- "path": "lib/ui-builder/hooks/use-dnd-event-handlers.ts",
- "content": "import { useCallback } from 'react';\nimport { DragStartEvent, DragEndEvent } from '@dnd-kit/core';\nimport { useLayerStore } from '@/lib/ui-builder/store/layer-store';\nimport { findAllParentLayersRecursive } from '@/lib/ui-builder/store/layer-utils';\n\ninterface UseDndEventHandlersProps {\n stopAutoScroll: () => void;\n setActiveLayerId: (layerId: string | null) => void;\n}\n\nexport const useDndEventHandlers = ({ stopAutoScroll, setActiveLayerId }: UseDndEventHandlersProps) => {\n const moveLayer = useLayerStore((state) => state.moveLayer);\n const pages = useLayerStore((state) => state.pages);\n\n // Helper function to check if a layer is a descendant of another layer\n const isLayerDescendantOf = useCallback((childId: string, parentId: string): boolean => {\n if (childId === parentId) return true; // A layer is considered its own descendant for drop prevention\n const parentLayers = findAllParentLayersRecursive(pages, childId);\n return parentLayers.some(parent => parent.id === parentId);\n }, [pages]);\n\n const handleDragStart = useCallback((event: DragStartEvent) => {\n const { active } = event;\n if (active.data.current?.type === 'layer') {\n setActiveLayerId(active.data.current.layerId);\n } else {\n console.log('Drag start: Non-layer drag detected', active.data.current?.type);\n }\n }, [setActiveLayerId]);\n\n const handleDragEnd = useCallback((event: DragEndEvent) => {\n const { active, over } = event;\n \n // Stop auto-scroll immediately\n stopAutoScroll();\n \n if (!over || !active.data.current?.layerId) {\n setActiveLayerId(null);\n return;\n }\n\n const activeLayerId = active.data.current.layerId;\n const overData = over.data.current;\n\n if (overData?.type === 'drop-zone') {\n const targetParentId = overData.parentId;\n const targetPosition = overData.position;\n \n // Don't allow dropping a layer onto itself or its descendants\n if (isLayerDescendantOf(targetParentId, activeLayerId)) {\n setActiveLayerId(null);\n return;\n }\n\n moveLayer(activeLayerId, targetParentId, targetPosition);\n }\n\n setActiveLayerId(null);\n }, [moveLayer, isLayerDescendantOf, stopAutoScroll, setActiveLayerId]);\n\n const handleDragCancel = useCallback(() => {\n stopAutoScroll();\n setActiveLayerId(null);\n }, [stopAutoScroll, setActiveLayerId]);\n\n return {\n handleDragStart,\n handleDragEnd,\n handleDragCancel,\n isLayerDescendantOf,\n };\n}; ",
+ "path": "lib/ui-builder/registry/form-field-overrides.tsx",
+ "content": "import React from \"react\";\nimport {\n FormControl,\n FormDescription,\n FormItem,\n FormLabel,\n} from \"@/components/ui/form\";\nimport { ChildrenSearchableSelect } from \"@/components/ui/ui-builder/internal/form-fields/children-searchable-select\";\nimport {\n AutoFormInputComponentProps,\n ComponentLayer,\n FieldConfigFunction,\n} from \"@/components/ui/ui-builder/types\";\nimport IconNameField from \"@/components/ui/ui-builder/internal/form-fields/iconname-field\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { MinimalTiptapEditor } from \"@/components/ui/minimal-tiptap\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { useLayerStore } from \"../store/layer-store\";\nimport { isVariableReference } from \"../utils/variable-resolver\";\nimport { Link, LockKeyhole, Unlink } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { Input } from \"@/components/ui/input\";\nimport { useEditorStore } from \"../store/editor-store\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport BreakpointClassNameControl from \"@/components/ui/ui-builder/internal/form-fields/classname-control\";\nimport { Label } from \"@/components/ui/label\";\nimport { Badge } from \"@/components/ui/badge\";\n\nexport const classNameFieldOverrides: FieldConfigFunction = (\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n layer,\n) => {\n return {\n fieldType: ({\n label,\n isRequired,\n field,\n fieldConfigItem,\n }: AutoFormInputComponentProps) => (\n \n \n \n ),\n };\n};\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport const childrenFieldOverrides: FieldConfigFunction = (\n layer,\n) => {\n return {\n fieldType: ({\n label,\n isRequired,\n fieldConfigItem,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n \n \n ),\n };\n};\n\nexport const iconNameFieldOverrides: FieldConfigFunction = (layer) => {\n return {\n fieldType: ({\n label,\n isRequired,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n ),\n };\n};\n\nexport const childrenAsTextareaFieldOverrides: FieldConfigFunction = (\n layer\n) => {\n return {\n fieldType: ({\n label,\n isRequired,\n fieldConfigItem,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n \n \n ),\n };\n};\n\nexport const childrenAsTipTapFieldOverrides: FieldConfigFunction = (\n layer,\n) => {\n return {\n fieldType: ({\n label,\n isRequired,\n fieldConfigItem,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n {\n //if string call field.onChange\n if (typeof content === \"string\") {\n field.onChange(content);\n } else {\n console.warn(\"Tiptap content is not a string\");\n }\n }}\n {...fieldProps}\n />\n \n ),\n };\n};\n\n// Memoized common field overrides to avoid recreating objects\nconst memoizedCommonFieldOverrides = new Map>();\n\nexport const commonFieldOverrides = (allowBinding = false) => {\n if (memoizedCommonFieldOverrides.has(allowBinding)) {\n return memoizedCommonFieldOverrides.get(allowBinding)!;\n }\n \n const overrides = {\n className: (layer: ComponentLayer) => classNameFieldOverrides(layer),\n children: (layer: ComponentLayer) => childrenFieldOverrides(layer),\n };\n \n memoizedCommonFieldOverrides.set(allowBinding, overrides);\n return overrides;\n};\n\nexport const commonVariableRenderParentOverrides = (propName: string) => {\n return {\n renderParent: ({ children }: { children: React.ReactNode }) => (\n {children} \n ),\n };\n};\n\nexport const textInputFieldOverrides = (\n layer: ComponentLayer,\n allowVariableBinding = false,\n propName: string\n) => {\n return {\n renderParent: allowVariableBinding\n ? ({ children }: { children: React.ReactNode }) => (\n \n {children}\n \n )\n : undefined,\n fieldType: ({\n label,\n isRequired,\n fieldConfigItem,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n field.onChange(e.target.value)}\n {...fieldProps}\n />\n \n ),\n };\n};\n\nexport function VariableBindingWrapper({\n propName,\n children,\n}: {\n propName: string;\n children: React.ReactNode;\n}) {\n const variables = useLayerStore((state) => state.variables);\n const selectedLayerId = useLayerStore((state) => state.selectedLayerId);\n const findLayerById = useLayerStore((state) => state.findLayerById);\n const isBindingImmutable = useLayerStore((state) => state.isBindingImmutable);\n const incrementRevision = useEditorStore((state) => state.incrementRevision);\n const unbindPropFromVariable = useLayerStore(\n (state) => state.unbindPropFromVariable\n );\n const bindPropToVariable = useLayerStore((state) => state.bindPropToVariable);\n\n const selectedLayer = findLayerById(selectedLayerId);\n\n // If variable binding is not allowed or no propName provided, just render the form wrapper\n if (!selectedLayer) {\n return <>{children}>;\n }\n\n const currentValue = selectedLayer.props[propName];\n const isCurrentlyBound = isVariableReference(currentValue);\n const boundVariable = isCurrentlyBound\n ? variables.find((v) => v.id === currentValue.__variableRef)\n : null;\n const isImmutable = isBindingImmutable(selectedLayer.id, propName);\n\n const handleBindToVariable = (variableId: string) => {\n bindPropToVariable(selectedLayer.id, propName, variableId);\n incrementRevision();\n };\n\n // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop\n const handleUnbind = () => {\n // Use the new unbind function which sets default value from schema\n unbindPropFromVariable(selectedLayer.id, propName);\n incrementRevision();\n };\n\n return (\n \n {isCurrentlyBound && boundVariable ? (\n // Bound state - show variable info and unbind button\n
\n
{propName.charAt(0).toUpperCase() + propName.slice(1)} \n
\n
\n \n \n
\n
\n
\n {boundVariable.name} \n \n {boundVariable.type}\n \n {isImmutable && (\n \n \n \n )}\n
\n
\n {String(boundVariable.defaultValue)}\n \n
\n
\n \n \n {!isImmutable && (\n
\n \n \n \n \n \n Unbind Variable \n \n )}\n
\n
\n ) : (\n // Unbound state - show normal field with bind button\n <>\n
{children}
\n
\n
\n \n \n \n \n \n \n \n \n Bind Variable \n \n \n \n Bind to Variable\n
\n {variables.length > 0 ? (\n variables.map((variable) => (\n handleBindToVariable(variable.id)}\n className=\"flex flex-col items-start p-3\"\n >\n \n
\n
\n
\n {variable.name} \n \n {variable.type}\n \n
\n
\n {String(variable.defaultValue)}\n \n
\n
\n \n ))\n ) : (\n \n No variables defined\n
\n )}\n \n \n
\n >\n )}\n
\n );\n}\n\nexport function FormFieldWrapper({\n label,\n isRequired,\n fieldConfigItem,\n children,\n}: {\n label: string;\n isRequired?: boolean;\n fieldConfigItem?: { description?: React.ReactNode };\n children: React.ReactNode;\n}) {\n return (\n \n \n {label}\n {isRequired && * }\n \n {children} \n {fieldConfigItem?.description && (\n {fieldConfigItem.description} \n )}\n \n );\n}\n",
"type": "registry:lib",
- "target": "lib/ui-builder/hooks/use-dnd-event-handlers.ts"
+ "target": "lib/ui-builder/registry/form-field-overrides.tsx"
},
{
- "path": "lib/ui-builder/hooks/use-auto-scroll.ts",
- "content": "import { useCallback, useRef } from 'react';\nimport { getIframeElements } from '@/lib/ui-builder/context/dnd-utils';\nimport { \n AutoScrollState, \n AUTO_SCROLL_THRESHOLD, \n calculateScrollSpeed \n} from '../context/auto-scroll-constants';\n\nexport const useAutoScroll = () => {\n // Auto-scroll state management\n const autoScrollStateRef = useRef({\n isScrolling: false,\n directions: { left: false, right: false, top: false, bottom: false },\n speeds: { horizontal: 0, vertical: 0 },\n });\n \n const mousePositionRef = useRef<{ x: number; y: number } | null>(null);\n const animationFrameRef = useRef(null);\n\n // Auto-scroll logic\n const performAutoScroll = useCallback(() => {\n const iframeElements = getIframeElements();\n if (!iframeElements || !mousePositionRef.current) {\n return;\n }\n\n const { iframe, window: iframeWindow } = iframeElements;\n \n // Get iframe bounds in viewport coordinates\n const iframeRect = iframe.getBoundingClientRect();\n \n // Convert mouse position to iframe-relative coordinates (without transform)\n const iframeMouseX = mousePositionRef.current.x - iframeRect.left;\n const iframeMouseY = mousePositionRef.current.y - iframeRect.top;\n \n // The scrollable area dimensions are the iframe dimensions\n const scrollableWidth = iframeRect.width;\n const scrollableHeight = iframeRect.height;\n \n const distanceFromLeft = iframeMouseX;\n const distanceFromRight = scrollableWidth - iframeMouseX;\n const distanceFromTop = iframeMouseY;\n const distanceFromBottom = scrollableHeight - iframeMouseY;\n\n let shouldScrollLeft = distanceFromLeft < AUTO_SCROLL_THRESHOLD;\n let shouldScrollRight = distanceFromRight < AUTO_SCROLL_THRESHOLD;\n let shouldScrollUp = distanceFromTop < AUTO_SCROLL_THRESHOLD;\n let shouldScrollDown = distanceFromBottom < AUTO_SCROLL_THRESHOLD;\n\n if (shouldScrollLeft && shouldScrollRight) {\n if (distanceFromLeft < distanceFromRight) {\n shouldScrollRight = false;\n } else {\n shouldScrollLeft = false;\n }\n }\n\n if (shouldScrollUp && shouldScrollDown) {\n if (distanceFromTop < distanceFromBottom) {\n shouldScrollDown = false;\n } else {\n shouldScrollUp = false;\n }\n }\n \n // Calculate scroll speeds\n const leftSpeed = shouldScrollLeft ? calculateScrollSpeed(Math.max(0, distanceFromLeft)) : 0;\n const rightSpeed = shouldScrollRight ? calculateScrollSpeed(Math.max(0, distanceFromRight)) : 0;\n const upSpeed = shouldScrollUp ? calculateScrollSpeed(Math.max(0, distanceFromTop)) : 0;\n const downSpeed = shouldScrollDown ? calculateScrollSpeed(Math.max(0, distanceFromBottom)) : 0;\n \n // Update auto-scroll state\n const state = autoScrollStateRef.current;\n state.directions = {\n left: shouldScrollLeft,\n right: shouldScrollRight,\n top: shouldScrollUp,\n bottom: shouldScrollDown,\n };\n state.speeds = {\n horizontal: leftSpeed || rightSpeed,\n vertical: upSpeed || downSpeed,\n };\n state.isScrolling = shouldScrollLeft || shouldScrollRight || shouldScrollUp || shouldScrollDown;\n \n // Perform the actual scrolling\n if (state.isScrolling) {\n let scrollX = 0;\n let scrollY = 0;\n \n if (shouldScrollLeft) scrollX = -leftSpeed;\n else if (shouldScrollRight) scrollX = rightSpeed;\n \n if (shouldScrollUp) scrollY = -upSpeed;\n else if (shouldScrollDown) scrollY = downSpeed;\n \n // Apply scroll to iframe content\n if (scrollX !== 0 || scrollY !== 0) {\n try {\n iframeWindow.scrollBy(scrollX, scrollY);\n } catch (error) {\n console.warn('Auto-scroll failed:', error);\n }\n }\n \n // Continue scrolling\n animationFrameRef.current = requestAnimationFrame(performAutoScroll);\n } else {\n // Stop scrolling\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n }\n }, []);\n\n // Mouse move handler for auto-scroll (parent document)\n const handleParentMouseMove = useCallback((event: MouseEvent, activeLayerId: string | null) => {\n if (!activeLayerId) return;\n \n mousePositionRef.current = { x: event.clientX, y: event.clientY };\n \n // Start auto-scroll if not already running\n if (!animationFrameRef.current) {\n animationFrameRef.current = requestAnimationFrame(performAutoScroll);\n }\n }, [performAutoScroll]);\n\n // Mouse move handler for auto-scroll (iframe content)\n const handleIframeMouseMove = useCallback((event: MouseEvent, activeLayerId: string | null) => {\n if (!activeLayerId) return;\n \n const iframeElements = getIframeElements();\n if (!iframeElements) return;\n \n const { iframe } = iframeElements;\n const iframeRect = iframe.getBoundingClientRect();\n \n // Convert iframe-relative mouse position to parent document coordinates\n const parentX = event.clientX + iframeRect.left;\n const parentY = event.clientY + iframeRect.top;\n \n mousePositionRef.current = { x: parentX, y: parentY };\n \n // Start auto-scroll if not already running\n if (!animationFrameRef.current) {\n animationFrameRef.current = requestAnimationFrame(performAutoScroll);\n }\n }, [performAutoScroll]);\n\n // Stop auto-scroll function\n const stopAutoScroll = useCallback(() => {\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n \n autoScrollStateRef.current = {\n isScrolling: false,\n directions: { left: false, right: false, top: false, bottom: false },\n speeds: { horizontal: 0, vertical: 0 },\n };\n \n mousePositionRef.current = null;\n }, []);\n\n return {\n handleParentMouseMove,\n handleIframeMouseMove,\n stopAutoScroll,\n autoScrollState: autoScrollStateRef.current,\n };\n}; ",
+ "path": "lib/ui-builder/registry/complex-component-definitions.ts",
+ "content": "import { ComponentRegistry } from '@/components/ui/ui-builder/types';\nimport { z } from 'zod';\nimport { Button } from '@/components/ui/button';\nimport { Badge } from '@/components/ui/badge';\nimport { Flexbox } from '@/components/ui/ui-builder/components/flexbox';\nimport { Grid } from '@/components/ui/ui-builder/components/grid';\nimport { CodePanel } from '@/components/ui/ui-builder/components/code-panel';\nimport { Markdown } from \"@/components/ui/ui-builder/components/markdown\";\nimport { Icon, iconNames } from \"@/components/ui/ui-builder/components/icon\";\nimport { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from \"@/components/ui/accordion\";\nimport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from \"@/components/ui/card\";\nimport { classNameFieldOverrides, childrenFieldOverrides, iconNameFieldOverrides, commonFieldOverrides, childrenAsTipTapFieldOverrides } from \"@/lib/ui-builder/registry/form-field-overrides\";\nimport { ComponentLayer } from '@/components/ui/ui-builder/types';\n\nexport const complexComponentDefinitions: ComponentRegistry = {\n Button: {\n component: Button,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n asChild: z.boolean().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: \"Button\",\n } satisfies ComponentLayer,\n ],\n fieldOverrides: commonFieldOverrides()\n },\n Badge: {\n component: Badge,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n variant: z\n .enum([\"default\", \"secondary\", \"destructive\", \"outline\"])\n .default(\"default\"),\n }),\n from: \"@/components/ui/badge\",\n defaultChildren: [\n {\n id: \"badge-text\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Badge\",\n } satisfies ComponentLayer,\n ],\n fieldOverrides: commonFieldOverrides()\n },\n Flexbox: {\n component: Flexbox,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n direction: z\n .enum([\"row\", \"column\", \"rowReverse\", \"columnReverse\"])\n .default(\"row\"),\n justify: z\n .enum([\"start\", \"end\", \"center\", \"between\", \"around\", \"evenly\"])\n .default(\"start\"),\n align: z\n .enum([\"start\", \"end\", \"center\", \"baseline\", \"stretch\"])\n .default(\"start\"),\n wrap: z.enum([\"wrap\", \"nowrap\", \"wrapReverse\"]).default(\"nowrap\"),\n gap: z\n .preprocess(\n (val) => (typeof val === 'number' ? String(val) : val),\n z.enum([\"0\", \"1\", \"2\", \"4\", \"8\"]).default(\"1\")\n )\n .transform(Number),\n }),\n from: \"@/components/ui/ui-builder/flexbox\",\n fieldOverrides: commonFieldOverrides()\n },\n Grid: {\n component: Grid,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n columns: z\n .enum([\"auto\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\"])\n .default(\"1\"),\n autoRows: z.enum([\"none\", \"min\", \"max\", \"fr\"]).default(\"none\"),\n justify: z\n .enum([\"start\", \"end\", \"center\", \"between\", \"around\", \"evenly\"])\n .default(\"start\"),\n align: z\n .enum([\"start\", \"end\", \"center\", \"baseline\", \"stretch\"])\n .default(\"start\"),\n templateRows: z\n .enum([\"none\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\"])\n .default(\"none\")\n .transform(val => (val === \"none\" ? val : Number(val))),\n gap: z\n .preprocess(\n (val) => (typeof val === 'number' ? String(val) : val),\n z.enum([\"0\", \"1\", \"2\", \"4\", \"8\"]).default(\"0\")\n )\n .transform(Number),\n }),\n from: \"@/components/ui/ui-builder/grid\",\n fieldOverrides: commonFieldOverrides()\n },\n CodePanel: {\n component: CodePanel,\n schema: z.object({\n className: z.string().optional(),\n }),\n from: \"@/components/ui/ui-builder/code-panel\",\n fieldOverrides: {\n className:(layer)=> classNameFieldOverrides(layer)\n }\n },\n Markdown: {\n component: Markdown,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: \"@/components/ui/ui-builder/markdown\",\n fieldOverrides: {\n className:(layer)=> classNameFieldOverrides(layer),\n children: (layer)=> childrenAsTipTapFieldOverrides(layer)\n }\n },\n Icon: {\n component: Icon,\n schema: z.object({\n className: z.string().optional(),\n iconName: z.enum([...iconNames]).default(\"Image\"),\n size: z.enum([\"small\", \"medium\", \"large\"]).default(\"medium\"),\n color: z\n .enum([\n \"accent\",\n \"accentForeground\",\n \"primary\",\n \"primaryForeground\",\n \"secondary\",\n \"secondaryForeground\",\n \"destructive\",\n \"destructiveForeground\",\n \"muted\",\n \"mutedForeground\",\n \"background\",\n \"foreground\",\n ])\n .optional(),\n rotate: z.enum([\"none\", \"90\", \"180\", \"270\"]).default(\"none\"),\n }),\n from: \"@/components/ui/ui-builder/icon\",\n fieldOverrides: {\n className:(layer)=> classNameFieldOverrides(layer),\n iconName: (layer)=> iconNameFieldOverrides(layer)\n }\n },\n\n //Accordion\n Accordion: {\n component: Accordion,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n type: z.enum([\"single\", \"multiple\"]).default(\"single\"),\n collapsible: z.boolean().optional(),\n }),\n from: \"@/components/ui/accordion\",\n defaultChildren: [\n {\n id: \"acc-item-1\",\n type: \"AccordionItem\",\n name: \"AccordionItem\",\n props: {\n value: \"item-1\",\n },\n children: [\n {\n id: \"acc-trigger-1\",\n type: \"AccordionTrigger\",\n name: \"AccordionTrigger\",\n props: {},\n children: [\n {\n id: \"WEz8Yku\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Accordion Item #1\",\n } satisfies ComponentLayer,\n ],\n },\n {\n id: \"acc-content-1\",\n type: \"AccordionContent\",\n name: \"AccordionContent\",\n props: {},\n children: [\n {\n id: \"acc-content-1-text-1\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Accordion Content Text\",\n } satisfies ComponentLayer,\n ],\n },\n ],\n },\n {\n id: \"acc-item-2\",\n type: \"AccordionItem\",\n name: \"AccordionItem\",\n props: {\n value: \"item-2\",\n },\n children: [\n {\n id: \"acc-trigger-2\",\n type: \"AccordionTrigger\",\n name: \"AccordionTrigger\",\n props: {},\n children: [\n {\n id: \"acc-trigger-2-text-1\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Accordion Item #2\",\n } satisfies ComponentLayer,\n ],\n },\n {\n id: \"acc-content-2\",\n type: \"AccordionContent\",\n name: \"AccordionContent (Copy)\",\n props: {},\n children: [\n {\n id: \"acc-content-2-text-1\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Accordion Content Text\",\n } satisfies ComponentLayer,\n ],\n },\n ],\n },\n ],\n fieldOverrides: commonFieldOverrides()\n },\n AccordionItem: {\n component: AccordionItem,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n value: z.string(),\n }),\n from: \"@/components/ui/accordion\",\n defaultChildren: [\n {\n id: \"acc-trigger-1\",\n type: \"AccordionTrigger\",\n name: \"AccordionTrigger\",\n props: {},\n children: [\n {\n id: \"WEz8Yku\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Accordion Item #1\",\n } satisfies ComponentLayer,\n ],\n },\n {\n id: \"acc-content-1\",\n type: \"AccordionContent\",\n name: \"AccordionContent\",\n props: {},\n children: [\n {\n id: \"acc-content-1-text-1\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Accordion Content Text\",\n } satisfies ComponentLayer,\n ],\n },\n ],\n fieldOverrides: commonFieldOverrides()\n },\n AccordionTrigger: {\n component: AccordionTrigger,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: \"@/components/ui/accordion\",\n fieldOverrides: {\n className:(layer)=> classNameFieldOverrides(layer),\n children: (layer)=> childrenFieldOverrides(layer)\n }\n },\n AccordionContent: {\n component: AccordionContent,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: \"@/components/ui/accordion\",\n fieldOverrides: commonFieldOverrides()\n },\n\n //Card\n Card: {\n component: Card,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: '@/components/ui/card',\n defaultChildren: [\n {\n id: \"card-header\",\n type: \"CardHeader\",\n name: \"CardHeader\",\n props: {},\n children: [\n {\n id: \"card-title\",\n type: \"CardTitle\",\n name: \"CardTitle\",\n props: {},\n children: [\n {\n id: \"card-title-text\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Card Title\",\n } satisfies ComponentLayer,\n ],\n },\n {\n id: \"card-description\",\n type: \"CardDescription\",\n name: \"CardDescription\",\n props: {},\n children: [\n {\n id: \"card-description-text\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Card Description\",\n } satisfies ComponentLayer,\n ],\n },\n ],\n },\n {\n id: \"card-content\",\n type: \"CardContent\",\n name: \"CardContent\",\n props: {},\n children: [\n {\n id: \"card-content-paragraph\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Card Content\",\n } satisfies ComponentLayer,\n ],\n },\n {\n id: \"card-footer\",\n type: \"CardFooter\",\n name: \"CardFooter\",\n props: {},\n children: [\n {\n id: \"card-footer-paragraph\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Card Footer\",\n } satisfies ComponentLayer,\n ],\n },\n ],\n fieldOverrides: commonFieldOverrides()\n },\n CardHeader: {\n component: CardHeader,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: '@/components/ui/card',\n fieldOverrides: commonFieldOverrides()\n },\n CardFooter: {\n component: CardFooter,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: '@/components/ui/card',\n fieldOverrides: commonFieldOverrides()\n },\n CardTitle: {\n component: CardTitle,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: '@/components/ui/card',\n fieldOverrides: commonFieldOverrides()\n },\n CardDescription: {\n component: CardDescription,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: '@/components/ui/card',\n fieldOverrides: commonFieldOverrides()\n },\n CardContent: {\n component: CardContent,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: '@/components/ui/card',\n fieldOverrides: commonFieldOverrides()\n },\n};",
"type": "registry:lib",
- "target": "lib/ui-builder/hooks/use-auto-scroll.ts"
+ "target": "lib/ui-builder/registry/complex-component-definitions.ts"
},
{
"path": "lib/ui-builder/context/drag-overlay.tsx",
@@ -483,12 +453,48 @@
"type": "registry:lib",
"target": "lib/ui-builder/context/auto-scroll-constants.ts"
},
+ {
+ "path": "lib/ui-builder/hooks/use-keyboard-shortcuts-dnd.ts",
+ "content": "import { useEffect } from 'react';\n\nexport const useKeyboardShortcutsDnd = (activeLayerId: string | null, handleDragCancel: () => void) => {\n // Handle escape key to cancel drag operations\n useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape' && activeLayerId) {\n event.preventDefault();\n event.stopPropagation();\n handleDragCancel();\n }\n };\n\n // Only add the listener when actively dragging\n if (activeLayerId) {\n document.addEventListener('keydown', handleKeyDown);\n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n };\n }\n }, [activeLayerId, handleDragCancel]);\n}; ",
+ "type": "registry:lib",
+ "target": "lib/ui-builder/hooks/use-keyboard-shortcuts-dnd.ts"
+ },
+ {
+ "path": "lib/ui-builder/hooks/use-drop-validation.ts",
+ "content": "import { useCallback } from 'react';\nimport { useLayerStore } from '@/lib/ui-builder/store/layer-store';\nimport { useEditorStore } from '@/lib/ui-builder/store/editor-store';\nimport { canLayerAcceptChildren } from '@/lib/ui-builder/store/layer-utils';\n\nexport const useDropValidation = (activeLayerId: string | null, isLayerDescendantOf: (childId: string, parentId: string) => boolean) => {\n const findLayerById = useLayerStore((state) => state.findLayerById);\n const componentRegistry = useEditorStore((state) => state.registry);\n\n const canDropOnLayer = useCallback((layerId: string): boolean => {\n const targetLayer = findLayerById(layerId);\n if (!targetLayer) return false;\n\n // If no active drag, check if layer can accept children\n if (!activeLayerId) {\n return canLayerAcceptChildren(targetLayer, componentRegistry);\n }\n \n // Don't allow dropping onto self or descendants\n if (isLayerDescendantOf(layerId, activeLayerId)) {\n return false;\n }\n\n const draggedLayer = findLayerById(activeLayerId);\n if (!draggedLayer) return false;\n\n return canLayerAcceptChildren(targetLayer, componentRegistry);\n }, [activeLayerId, isLayerDescendantOf, findLayerById, componentRegistry]);\n\n return { canDropOnLayer };\n}; ",
+ "type": "registry:lib",
+ "target": "lib/ui-builder/hooks/use-drop-validation.ts"
+ },
+ {
+ "path": "lib/ui-builder/hooks/use-dnd-sensors.ts",
+ "content": "import { useSensors, useSensor, MouseSensor, TouchSensor } from '@dnd-kit/core';\n\nexport const useDndSensors = () => {\n return useSensors(\n useSensor(MouseSensor, {\n activationConstraint: {\n distance: 4,\n },\n }),\n useSensor(TouchSensor, {\n activationConstraint: {\n delay: 100,\n tolerance: 8,\n },\n })\n );\n}; ",
+ "type": "registry:lib",
+ "target": "lib/ui-builder/hooks/use-dnd-sensors.ts"
+ },
+ {
+ "path": "lib/ui-builder/hooks/use-dnd-event-handlers.ts",
+ "content": "import { useCallback } from 'react';\nimport { DragStartEvent, DragEndEvent } from '@dnd-kit/core';\nimport { useLayerStore } from '@/lib/ui-builder/store/layer-store';\nimport { findAllParentLayersRecursive } from '@/lib/ui-builder/store/layer-utils';\n\ninterface UseDndEventHandlersProps {\n stopAutoScroll: () => void;\n setActiveLayerId: (layerId: string | null) => void;\n}\n\nexport const useDndEventHandlers = ({ stopAutoScroll, setActiveLayerId }: UseDndEventHandlersProps) => {\n const moveLayer = useLayerStore((state) => state.moveLayer);\n const pages = useLayerStore((state) => state.pages);\n\n // Helper function to check if a layer is a descendant of another layer\n const isLayerDescendantOf = useCallback((childId: string, parentId: string): boolean => {\n if (childId === parentId) return true; // A layer is considered its own descendant for drop prevention\n const parentLayers = findAllParentLayersRecursive(pages, childId);\n return parentLayers.some(parent => parent.id === parentId);\n }, [pages]);\n\n const handleDragStart = useCallback((event: DragStartEvent) => {\n const { active } = event;\n if (active.data.current?.type === 'layer') {\n setActiveLayerId(active.data.current.layerId);\n } else {\n console.log('Drag start: Non-layer drag detected', active.data.current?.type);\n }\n }, [setActiveLayerId]);\n\n const handleDragEnd = useCallback((event: DragEndEvent) => {\n const { active, over } = event;\n \n // Stop auto-scroll immediately\n stopAutoScroll();\n \n if (!over || !active.data.current?.layerId) {\n setActiveLayerId(null);\n return;\n }\n\n const activeLayerId = active.data.current.layerId;\n const overData = over.data.current;\n\n if (overData?.type === 'drop-zone') {\n const targetParentId = overData.parentId;\n const targetPosition = overData.position;\n \n // Don't allow dropping a layer onto itself or its descendants\n if (isLayerDescendantOf(targetParentId, activeLayerId)) {\n setActiveLayerId(null);\n return;\n }\n\n moveLayer(activeLayerId, targetParentId, targetPosition);\n }\n\n setActiveLayerId(null);\n }, [moveLayer, isLayerDescendantOf, stopAutoScroll, setActiveLayerId]);\n\n const handleDragCancel = useCallback(() => {\n stopAutoScroll();\n setActiveLayerId(null);\n }, [stopAutoScroll, setActiveLayerId]);\n\n return {\n handleDragStart,\n handleDragEnd,\n handleDragCancel,\n isLayerDescendantOf,\n };\n}; ",
+ "type": "registry:lib",
+ "target": "lib/ui-builder/hooks/use-dnd-event-handlers.ts"
+ },
+ {
+ "path": "lib/ui-builder/hooks/use-auto-scroll.ts",
+ "content": "import { useCallback, useRef } from 'react';\nimport { getIframeElements } from '@/lib/ui-builder/context/dnd-utils';\nimport { \n AutoScrollState, \n AUTO_SCROLL_THRESHOLD, \n calculateScrollSpeed \n} from '../context/auto-scroll-constants';\n\nexport const useAutoScroll = () => {\n // Auto-scroll state management\n const autoScrollStateRef = useRef({\n isScrolling: false,\n directions: { left: false, right: false, top: false, bottom: false },\n speeds: { horizontal: 0, vertical: 0 },\n });\n \n const mousePositionRef = useRef<{ x: number; y: number } | null>(null);\n const animationFrameRef = useRef(null);\n\n // Auto-scroll logic\n const performAutoScroll = useCallback(() => {\n const iframeElements = getIframeElements();\n if (!iframeElements || !mousePositionRef.current) {\n return;\n }\n\n const { iframe, window: iframeWindow } = iframeElements;\n \n // Get iframe bounds in viewport coordinates\n const iframeRect = iframe.getBoundingClientRect();\n \n // Convert mouse position to iframe-relative coordinates (without transform)\n const iframeMouseX = mousePositionRef.current.x - iframeRect.left;\n const iframeMouseY = mousePositionRef.current.y - iframeRect.top;\n \n // The scrollable area dimensions are the iframe dimensions\n const scrollableWidth = iframeRect.width;\n const scrollableHeight = iframeRect.height;\n \n const distanceFromLeft = iframeMouseX;\n const distanceFromRight = scrollableWidth - iframeMouseX;\n const distanceFromTop = iframeMouseY;\n const distanceFromBottom = scrollableHeight - iframeMouseY;\n\n let shouldScrollLeft = distanceFromLeft < AUTO_SCROLL_THRESHOLD;\n let shouldScrollRight = distanceFromRight < AUTO_SCROLL_THRESHOLD;\n let shouldScrollUp = distanceFromTop < AUTO_SCROLL_THRESHOLD;\n let shouldScrollDown = distanceFromBottom < AUTO_SCROLL_THRESHOLD;\n\n if (shouldScrollLeft && shouldScrollRight) {\n if (distanceFromLeft < distanceFromRight) {\n shouldScrollRight = false;\n } else {\n shouldScrollLeft = false;\n }\n }\n\n if (shouldScrollUp && shouldScrollDown) {\n if (distanceFromTop < distanceFromBottom) {\n shouldScrollDown = false;\n } else {\n shouldScrollUp = false;\n }\n }\n \n // Calculate scroll speeds\n const leftSpeed = shouldScrollLeft ? calculateScrollSpeed(Math.max(0, distanceFromLeft)) : 0;\n const rightSpeed = shouldScrollRight ? calculateScrollSpeed(Math.max(0, distanceFromRight)) : 0;\n const upSpeed = shouldScrollUp ? calculateScrollSpeed(Math.max(0, distanceFromTop)) : 0;\n const downSpeed = shouldScrollDown ? calculateScrollSpeed(Math.max(0, distanceFromBottom)) : 0;\n \n // Update auto-scroll state\n const state = autoScrollStateRef.current;\n state.directions = {\n left: shouldScrollLeft,\n right: shouldScrollRight,\n top: shouldScrollUp,\n bottom: shouldScrollDown,\n };\n state.speeds = {\n horizontal: leftSpeed || rightSpeed,\n vertical: upSpeed || downSpeed,\n };\n state.isScrolling = shouldScrollLeft || shouldScrollRight || shouldScrollUp || shouldScrollDown;\n \n // Perform the actual scrolling\n if (state.isScrolling) {\n let scrollX = 0;\n let scrollY = 0;\n \n if (shouldScrollLeft) scrollX = -leftSpeed;\n else if (shouldScrollRight) scrollX = rightSpeed;\n \n if (shouldScrollUp) scrollY = -upSpeed;\n else if (shouldScrollDown) scrollY = downSpeed;\n \n // Apply scroll to iframe content\n if (scrollX !== 0 || scrollY !== 0) {\n try {\n iframeWindow.scrollBy(scrollX, scrollY);\n } catch (error) {\n console.warn('Auto-scroll failed:', error);\n }\n }\n \n // Continue scrolling\n animationFrameRef.current = requestAnimationFrame(performAutoScroll);\n } else {\n // Stop scrolling\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n }\n }, []);\n\n // Mouse move handler for auto-scroll (parent document)\n const handleParentMouseMove = useCallback((event: MouseEvent, activeLayerId: string | null) => {\n if (!activeLayerId) return;\n \n mousePositionRef.current = { x: event.clientX, y: event.clientY };\n \n // Start auto-scroll if not already running\n if (!animationFrameRef.current) {\n animationFrameRef.current = requestAnimationFrame(performAutoScroll);\n }\n }, [performAutoScroll]);\n\n // Mouse move handler for auto-scroll (iframe content)\n const handleIframeMouseMove = useCallback((event: MouseEvent, activeLayerId: string | null) => {\n if (!activeLayerId) return;\n \n const iframeElements = getIframeElements();\n if (!iframeElements) return;\n \n const { iframe } = iframeElements;\n const iframeRect = iframe.getBoundingClientRect();\n \n // Convert iframe-relative mouse position to parent document coordinates\n const parentX = event.clientX + iframeRect.left;\n const parentY = event.clientY + iframeRect.top;\n \n mousePositionRef.current = { x: parentX, y: parentY };\n \n // Start auto-scroll if not already running\n if (!animationFrameRef.current) {\n animationFrameRef.current = requestAnimationFrame(performAutoScroll);\n }\n }, [performAutoScroll]);\n\n // Stop auto-scroll function\n const stopAutoScroll = useCallback(() => {\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n \n autoScrollStateRef.current = {\n isScrolling: false,\n directions: { left: false, right: false, top: false, bottom: false },\n speeds: { horizontal: 0, vertical: 0 },\n };\n \n mousePositionRef.current = null;\n }, []);\n\n return {\n handleParentMouseMove,\n handleIframeMouseMove,\n stopAutoScroll,\n autoScrollState: autoScrollStateRef.current,\n };\n}; ",
+ "type": "registry:lib",
+ "target": "lib/ui-builder/hooks/use-auto-scroll.ts"
+ },
{
"path": "hooks/use-store.ts",
"content": "import { useState, useEffect } from 'react';\n\n// prevents nextjs hydration error if using nextjs\nexport const useStore = (\n store: (callback: (state: T) => unknown) => unknown,\n callback: (state: T) => F,\n) => {\n const result = store(callback) as F\n const [data, setData] = useState()\n\n useEffect(() => {\n setData(result)\n }, [result])\n\n return data\n};\n",
"type": "registry:hook",
"target": "hooks/use-store.ts"
},
+ {
+ "path": "hooks/use-mobile.tsx",
+ "content": "import * as React from \"react\"\n\nconst MOBILE_BREAKPOINT = 768\n\nexport function useIsMobile() {\n const [isMobile, setIsMobile] = React.useState(undefined)\n\n React.useEffect(() => {\n const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)\n const onChange = () => {\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)\n }\n mql.addEventListener(\"change\", onChange)\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)\n return () => mql.removeEventListener(\"change\", onChange)\n }, [])\n\n return !!isMobile\n}\n",
+ "type": "registry:hook",
+ "target": "hooks/use-mobile.tsx"
+ },
{
"path": "hooks/use-keyboard-shortcuts.tsx",
"content": "import { useEffect } from \"react\";\n\nexport interface KeyCombination {\n keys: {\n ctrlKey?: boolean;\n metaKey?: boolean;\n shiftKey?: boolean;\n altKey?: boolean;\n };\n key: string;\n handler: (event: KeyboardEvent) => void;\n}\n\nexport function useKeyboardShortcuts(combinations: KeyCombination[]) {\n useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n combinations.forEach((combo) => {\n const { keys: { ctrlKey, metaKey, shiftKey, altKey }, key, handler } = combo;\n const isMatch =\n (ctrlKey === undefined || event.ctrlKey === ctrlKey) &&\n (metaKey === undefined || event.metaKey === metaKey) &&\n (shiftKey === undefined || event.shiftKey === shiftKey) &&\n (altKey === undefined || event.altKey === altKey) &&\n event.key.toLowerCase() === key.toLowerCase();\n\n if (isMatch) {\n event.preventDefault();\n handler(event);\n }\n });\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n\n // Cleanup event listener on unmount\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n };\n }, [combinations]);\n}",
diff --git a/styles/globals.css b/styles/globals.css
index 441e22b..0c744de 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -29,6 +29,14 @@
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
+ --sidebar-background: 0 0% 98%;
+ --sidebar-foreground: 240 5.3% 26.1%;
+ --sidebar-primary: 240 5.9% 10%;
+ --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-accent: 240 4.8% 95.9%;
+ --sidebar-accent-foreground: 240 5.9% 10%;
+ --sidebar-border: 220 13% 91%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
@@ -56,6 +64,14 @@
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
+ --sidebar-background: 240 5.9% 10%;
+ --sidebar-foreground: 240 4.8% 95.9%;
+ --sidebar-primary: 224.3 76.3% 48%;
+ --sidebar-primary-foreground: 0 0% 100%;
+ --sidebar-accent: 240 3.7% 15.9%;
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
+ --sidebar-border: 240 3.7% 15.9%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
}
}
diff --git a/tailwind.config.js b/tailwind.config.js
index 2345290..6fbdccb 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -50,6 +50,16 @@ module.exports = {
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
+ },
+ sidebar: {
+ DEFAULT: 'hsl(var(--sidebar-background))',
+ foreground: 'hsl(var(--sidebar-foreground))',
+ primary: 'hsl(var(--sidebar-primary))',
+ 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
+ accent: 'hsl(var(--sidebar-accent))',
+ 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
+ border: 'hsl(var(--sidebar-border))',
+ ring: 'hsl(var(--sidebar-ring))'
}
},
borderRadius: {
diff --git a/tsconfig.json b/tsconfig.json
index 65537d9..22513b9 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -19,7 +19,7 @@
"types": ["@testing-library/jest-dom"],
"paths": {
"@/components/*": ["components/*"],
- "@/pages/*": ["pages/*"],
+ "@/app/*": ["app/*"],
"@/styles/*": ["styles/*"],
"@/lib/*": ["lib/*"],
"@/hooks/*": ["hooks/*"]