From 7bb96c719db3fccde80e2687c0997f0bdc2dcddb Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Mon, 21 Jul 2025 17:03:52 -0400 Subject: [PATCH 1/5] feat: docs intro --- app/docs/[slug]/edit/page.tsx | 18 + app/docs/[slug]/page.tsx | 58 ++ app/docs/docs-data/data.ts | 210 +++++ .../docs-page-layers/introduction.ts | 83 ++ app/docs/layout.tsx | 9 + app/examples/basic/page.tsx | 13 + app/platform/app-sidebar.tsx | 44 + app/platform/doc-editor.tsx | 22 + app/platform/doc-renderer.tsx | 14 + app/platform/simple-builder.tsx | 36 +- app/platform/theme-toggle.tsx | 46 ++ components/ui/breadcrumb.tsx | 115 +++ components/ui/sheet.tsx | 140 ++++ components/ui/sidebar.tsx | 780 ++++++++++++++++++ components/ui/skeleton.tsx | 15 + hooks/use-mobile.tsx | 19 + package-lock.json | 2 +- package.json | 2 +- styles/globals.css | 16 + tailwind.config.js | 10 + tsconfig.json | 2 +- 21 files changed, 1634 insertions(+), 20 deletions(-) create mode 100644 app/docs/[slug]/edit/page.tsx create mode 100644 app/docs/[slug]/page.tsx create mode 100644 app/docs/docs-data/data.ts create mode 100644 app/docs/docs-data/docs-page-layers/introduction.ts create mode 100644 app/docs/layout.tsx create mode 100644 app/examples/basic/page.tsx create mode 100644 app/platform/app-sidebar.tsx create mode 100644 app/platform/doc-editor.tsx create mode 100644 app/platform/doc-renderer.tsx create mode 100644 app/platform/theme-toggle.tsx create mode 100644 components/ui/breadcrumb.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/sidebar.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 hooks/use-mobile.tsx diff --git a/app/docs/[slug]/edit/page.tsx b/app/docs/[slug]/edit/page.tsx new file mode 100644 index 0000000..8f038c1 --- /dev/null +++ b/app/docs/[slug]/edit/page.tsx @@ -0,0 +1,18 @@ + +import { notFound } from "next/navigation"; +import { DocEditor } from "@/app/platform/doc-editor"; +import { getDocPageForSlug } from "../../docs-data/data"; + +export default async function DocEditPage({ + params, + }: { + params: Promise<{ slug: string }>; + }){ + const { slug } = await params; + const page = getDocPageForSlug(slug); + if (!page) { + notFound(); + } + console.log({slug}); + return +} \ No newline at end of file diff --git a/app/docs/[slug]/page.tsx b/app/docs/[slug]/page.tsx new file mode 100644 index 0000000..ac3bad6 --- /dev/null +++ b/app/docs/[slug]/page.tsx @@ -0,0 +1,58 @@ +import { AppSidebar } from "@/app/platform/app-sidebar"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar"; +import { Suspense } from "react"; +import { DocRenderer } from "@/app/platform/doc-renderer"; +import { getBreadcrumbsFromUrl, getDocPageForSlug } from "@/app/docs/docs-data/data"; +import { ThemeToggle } from "@/app/platform/theme-toggle"; +import { notFound } from "next/navigation"; + +export default async function DocPage({ + params, + }: { + params: Promise<{ slug: string }>; + }){ + const { slug } = await params; + const page = getDocPageForSlug(slug); + if (!page) { + notFound(); + } + const breadcrumbs = getBreadcrumbsFromUrl(slug); + console.log({slug}); + return + + +
+ + + + + + {breadcrumbs.category.title} + + + + + {breadcrumbs.page.title} + + + + +
+ Loading...}> + + +
+
+} \ No newline at end of file diff --git a/app/docs/docs-data/data.ts b/app/docs/docs-data/data.ts new file mode 100644 index 0000000..13bee01 --- /dev/null +++ b/app/docs/docs-data/data.ts @@ -0,0 +1,210 @@ +import { INTRODUCTION_LAYER } from "@/app/docs/docs-data/docs-page-layers/introduction"; + +export const DOCS_PAGES = [ + INTRODUCTION_LAYER +] as const; + +type ExistingDocPageNames = `${Capitalize<(typeof DOCS_PAGES)[number]["name"]>}`; +type ExistingDocPageIds = (typeof DOCS_PAGES)[number]["id"]; +type ExistingDocGroupNames = `${Capitalize<(typeof DOCS_PAGES)[0]["props"]["data-group"]>}`; + + +type DocPageNavItem = { + title: ExistingDocGroupNames | string; + items: { + title: ExistingDocPageNames | string; + url: `/${ExistingDocPageIds}` | `/${string}`; + }[]; +} + +export const MENU_DATA: DocPageNavItem[] = [ + { + title: "Core", + items: [ + { + title: "Introduction", + url: "/introduction", + }, + { + title: "Quick Start", + url: "/quick-start", + }, + ], + }, + { + title: "Component System", + items: [ + { + title: "Component Registry", + url: "/component-registry", + }, + { + title: "Field Overrides", + url: "/field-overrides", + }, + { + title: "Default Children", + url: "/default-children", + }, + { + title: "Custom Components", + url: "/custom-components", + } + ], + }, + { + title: "Editor Features", + items: [ + { + title: "Canvas Editor", + url: "/canvas-editor", + }, + { + title: "Pages Panel", + url: "/pages-panel", + }, + { + title: "Immutable Pages", + url: "/immutable-pages", + }, + { + title: "Appearance Panel", + url: "/appearance-panel", + }, + { + title: "Props Panel", + url: "/props-panel", + }, + { + title: "Variables Panel", + url: "/variables-panel", + }, + { + title: "Panel Configuration", + url: "/panel-configuration", + }, + { + title: "Editor Panel Config", + url: "/editor-panel-config", + }, + { + title: "NavBar Customization", + url: "/navbar-customization", + }, + { + title: "Props Panel Customization", + url: "/props-panel-customization", + }, + ], + }, + { + title: "Data & Variables", + items: [ + { + title: "Variables", + url: "/variables", + }, + { + title: "Variable Binding", + url: "/variable-binding", + }, + { + title: "Read-Only Mode", + url: "/read-only-mode", + }, + { + title: "Data Binding", + url: "/data-binding", + }, + ], + }, + { + title: "Layout & Persistence", + items: [ + { + title: "Layer Structure", + url: "/layer-structure", + }, + { + title: "Persistence", + url: "/persistence", + }, + { + title: "Persist Layer Store", + url: "/persist-layer-store", + }, + ], + }, + { + title: "Rendering", + items: [ + { + title: "Rendering Pages", + url: "/rendering-pages", + }, + { + title: "Page Theming", + url: "/page-theming", + }, + ], + }, + { + title: "Code & Extensibility", + items: [ + { + title: "Code Generation", + url: "/code-generation", + }, + { + title: "Blocks (Planned)", + url: "/blocks-planned", + }, + ], + }, +] as const; + + + +// Utility function to generate breadcrumbs from navigation data +export function getBreadcrumbsFromUrl(url: string) { + // Remove leading slash if present + const cleanUrl = url.startsWith('/') ? url.substring(1) : url; + + // Find the category and item that matches the URL + for (const category of MENU_DATA) { + for (const item of category.items) { + // Remove leading slash from item URL for comparison + const itemUrl = item.url.startsWith('/') ? item.url.substring(1) : item.url; + + if (itemUrl === cleanUrl) { + return { + category: { + title: category.title, + // Create a category URL from the first item in the category + url: category.items[0]?.url || '#' + }, + page: { + title: item.title, + url: item.url + } + }; + } + } + } + + // Fallback if URL not found + return { + category: { + title: "Documentation", + url: "#" + }, + page: { + title: "Page", + url: url + } + }; +} + +export function getDocPageForSlug(slug: string) { + return DOCS_PAGES.find((page) => page.id === slug); +} diff --git a/app/docs/docs-data/docs-page-layers/introduction.ts b/app/docs/docs-data/docs-page-layers/introduction.ts new file mode 100644 index 0000000..c660094 --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/introduction.ts @@ -0,0 +1,83 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const INTRODUCTION_LAYER = { + "id": "introduction", + "type": "div", + "name": "Introduction", + "props": { + "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", + "data-group": "core" + }, + "children": [ + { + "type": "span", + "children": "Introduction", + "id": "1MnLSMe", + "name": "Text", + "props": { + "className": "text-4xl" + } + }, + { + "id": "JKiqXGV", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "Welcome to **UI Builder**, the drop‑in visual editor for your React app. With UI Builder, you can empower non‑developers to compose pages, emails, dashboards, and white‑label views using the exact React components you already ship—no rebuilding from scratch required." + }, + { + "id": "eR9CoTQ", + "type": "div", + "name": "div", + "props": {}, + "children": [ + { + "id": "1FmQvr5", + "type": "Badge", + "name": "Badge", + "props": { + "variant": "default", + "className": "rounded rounded-b-none" + }, + "children": [ + { + "id": "itgw5T6", + "type": "span", + "name": "span", + "props": {}, + "children": "Example" + } + ] + }, + { + "id": "3EYD3Jj", + "type": "div", + "name": "div", + "props": { + "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" + }, + "children": [ + { + "id": "h8a96fY", + "type": "iframe", + "name": "iframe", + "props": { + "src": "http://localhost:3000/examples/basic", + "title": "", + "className": "aspect-square md:aspect-video" + }, + "children": [] + } + ] + } + ] + }, + { + "id": "cUFUpBr", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "### Why UI Builder?\n\n- **Leverage your existing components** — no extra code or maintenance overhead.\n\n- **Enable true no‑code workflows** — marketing teams, product managers, or clients can edit layouts and content without engineering support.\n\n- **Human‑readable JSON layouts** — version and review designs just like code.\n\n- **Dynamic, data‑driven interfaces** — bind component props to variables for personalized experiences.\n\n### Key Benefits\n\n1. **One‑step installation**\\\n Get up and running with a single `npx shadcn@latest add …` command.\n\n2. **Figma‑style editing**\\\n Intuitive drag‑and‑drop canvas, properties panel, and live preview.\n\n3. **Full React code export**\\\n Generate clean, type‑safe React code that matches your project structure.\n\n4. **Runtime variable binding**\\\n Create dynamic templates with string, number, and boolean variables—perfect for personalization, A/B testing, or multi‑tenant branding." + } + ] + } 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..4ed24c6 --- /dev/null +++ b/app/docs/layout.tsx @@ -0,0 +1,9 @@ +import { ThemeProvider } from "next-themes"; + +export default function DocsLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} \ 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/platform/app-sidebar.tsx b/app/platform/app-sidebar.tsx new file mode 100644 index 0000000..f55b1c9 --- /dev/null +++ b/app/platform/app-sidebar.tsx @@ -0,0 +1,44 @@ +import * as React from "react" +import Link from "next/link" + +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, +} from "@/components/ui/sidebar" +import { MENU_DATA } from "@/app/docs/docs-data/data" + +export function AppSidebar({ ...props }: React.ComponentProps) { + return ( + + + {MENU_DATA.map((section) => ( + + {section.title} + + + {section.items?.map((item) => ( + + + {item.title} + + + ))} + + + + ))} + + + + ) +} 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/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 + + + 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) => \n );\n};\n\n\n }}\n/>\n```\n\n## Branded Navigation\n\nCreate a fully branded navigation experience:\n\n```tsx\nconst BrandedNavBar = () => {\n const hasUnsavedChanges = useLayerStore(state => state.hasUnsavedChanges);\n const saveLayout = async () => {\n // Your save logic\n const pages = useLayerStore.getState().pages;\n await saveToYourBackend(pages);\n };\n \n return (\n \n );\n};\n```\n\n## Role-Based Navigation\n\nCustomize the navigation based on user permissions:\n\n```tsx\nconst RoleBasedNavBar = ({ user }) => {\n const canPublish = user.role === 'admin' || user.role === 'editor';\n const canDeletePages = user.role === 'admin';\n \n return (\n \n );\n};\n\n\n }}\n/>\n```\n\n## Custom Editor Panel\n\nEnhance the editor panel with additional tools and features:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\nimport { useLayerStore } from '@/lib/ui-builder/store/layer-store';\nimport { useEditorStore } from '@/lib/ui-builder/store/editor-store';\n\nconst EnhancedEditorPanel = ({ className }) => {\n const selectedPageId = useLayerStore(state => state.selectedPageId);\n const findLayerById = useLayerStore(state => state.findLayerById);\n const componentRegistry = useEditorStore(state => state.registry);\n const variables = useLayerStore(state => state.variables);\n \n const currentPage = findLayerById(selectedPageId);\n const [zoom, setZoom] = useState(1);\n const [deviceMode, setDeviceMode] = useState('desktop');\n \n const deviceSizes = {\n mobile: { width: 375, height: 667 },\n tablet: { width: 768, height: 1024 },\n desktop: { width: '100%', height: '100%' }\n };\n \n return (\n
\n {/* Enhanced Toolbar */}\n
\n
\n {/* Device Selector */}\n \n \n \n \n \n \n \n \n \n \n \n \n {/* Zoom Control */}\n
\n \n \n {Math.round(zoom * 100)}%\n \n \n
\n
\n \n
\n {/* Grid Toggle */}\n \n \n {/* Undo/Redo */}\n
\n \n \n
\n
\n
\n \n {/* Canvas Area */}\n
\n
\n {currentPage && (\n \n )}\n
\n
\n \n {/* Status Bar */}\n
\n \n Page: {currentPage?.name || 'Untitled'}\n \n \n {variables.length} variables • {deviceMode} view\n \n
\n
\n );\n};\n\n\n }}\n/>\n```\n\n## Context-Aware Navigation\n\nMake the navigation respond to editor state:\n\n```tsx\nconst ContextAwareNavBar = () => {\n const selectedLayerId = useLayerStore(state => state.selectedLayerId);\n const findLayerById = useLayerStore(state => state.findLayerById);\n const duplicateLayer = useLayerStore(state => state.duplicateLayer);\n const removeLayer = useLayerStore(state => state.removeLayer);\n \n const selectedLayer = findLayerById(selectedLayerId);\n const isComponentSelected = selectedLayer && selectedLayerId !== selectedPageId;\n \n return (\n \n );\n};\n```\n\n## Integration with External Systems\n\nConnect your navigation to external services:\n\n```tsx\nconst IntegratedNavBar = () => {\n const [savingStatus, setSavingStatus] = useState('saved'); // 'saving', 'saved', 'error'\n const [collaborators, setCollaborators] = useState([]);\n \n const handleSave = async () => {\n setSavingStatus('saving');\n try {\n const pages = useLayerStore.getState().pages;\n await saveToYourCMS(pages);\n setSavingStatus('saved');\n } catch (error) {\n setSavingStatus('error');\n }\n };\n \n const handlePublish = async () => {\n const pages = useLayerStore.getState().pages;\n await publishToLiveSite(pages);\n showNotification('Published successfully!');\n };\n \n return (\n \n );\n};\n```\n\n## Mobile-Responsive Navigation\n\nCreate navigation that works well on mobile devices:\n\n```tsx\nconst ResponsiveNavBar = () => {\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const isMobile = useMediaQuery('(max-width: 768px)');\n \n if (isMobile) {\n return (\n \n );\n }\n \n // Desktop navigation\n return (\n \n );\n};\n```\n\n## Best Practices\n\n### Navigation Design\n- **Keep it simple** - Don't overcrowd the navigation\n- **Group related actions** - Use dropdowns for secondary actions\n- **Show context** - Display current page/component information\n- **Provide feedback** - Show save status and loading states\n\n### Performance\n- **Memoize components** - Use React.memo for nav components\n- **Debounce actions** - Avoid rapid save/publish calls\n- **Optimize re-renders** - Subscribe to specific store slices\n\n### Accessibility\n- **Keyboard navigation** - Ensure all actions are keyboard accessible\n- **ARIA labels** - Provide proper labels for screen readers\n- **Focus management** - Maintain logical focus order\n- **Color contrast** - Ensure sufficient contrast for all elements" + } + ] + } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/field-overrides.ts b/app/docs/docs-data/docs-page-layers/field-overrides.ts new file mode 100644 index 0000000..9f40fa4 --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/field-overrides.ts @@ -0,0 +1,36 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const FIELD_OVERRIDES_LAYER = { + "id": "field-overrides", + "type": "div", + "name": "Advanced Component Configuration", + "props": { + "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", + "data-group": "component-system" + }, + "children": [ + { + "type": "span", + "children": "Advanced Component Configuration", + "id": "field-overrides-title", + "name": "Text", + "props": { + "className": "text-4xl" + } + }, + { + "id": "field-overrides-intro", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "Master advanced component configuration techniques including field overrides, default children, and variable bindings to create sophisticated, user-friendly editing experiences." + }, + { + "id": "field-overrides-content", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Field Overrides\n\nField overrides replace auto-generated form fields with specialized input controls, providing better user experiences for complex data types.\n\n### How Field Overrides Work\n\nField overrides are defined within component definitions using the `fieldOverrides` property:\n\n```tsx\nimport { z } from 'zod';\nimport { classNameFieldOverrides, childrenFieldOverrides } from '@/lib/ui-builder/registry/form-field-overrides';\n\nconst componentRegistry = {\n MyComponent: {\n component: MyComponent,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n icon: z.string().default('home'),\n color: z.string().default('#000000'),\n }),\n from: '@/components/ui/my-component',\n fieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer), // Advanced Tailwind editor\n children: (layer) => childrenFieldOverrides(layer), // Component selector\n icon: (layer) => iconNameFieldOverrides(layer), // Icon picker\n // color prop uses auto-generated form field\n }\n }\n};\n```\n\n### Built-in Field Overrides\n\n#### `classNameFieldOverrides(layer)`\nAdvanced Tailwind CSS class editor:\n- Auto-complete for Tailwind classes\n- Responsive breakpoint controls \n- Visual class grouping\n- Theme-aware suggestions\n\n```tsx\nfieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer)\n}\n```\n\n#### `childrenFieldOverrides(layer)`\nSearchable component selector for child components:\n- Dropdown with available component types\n- Search and filter capabilities\n- Respects component hierarchy\n\n```tsx\nfieldOverrides: {\n children: (layer) => childrenFieldOverrides(layer)\n}\n```\n\n#### `childrenAsTextareaFieldOverrides(layer)`\nSimple textarea for text content:\n- Multi-line text editing\n- Perfect for span, p, and text elements\n\n```tsx\nfieldOverrides: {\n children: (layer) => childrenAsTextareaFieldOverrides(layer)\n}\n```\n\n#### `childrenAsTipTapFieldOverrides(layer)`\nRich text editor using TipTap:\n- WYSIWYG markdown editing\n- Formatting toolbar\n- Ideal for Markdown components\n\n```tsx\nfieldOverrides: {\n children: (layer) => childrenAsTipTapFieldOverrides(layer)\n}\n```\n\n#### `iconNameFieldOverrides(layer)`\nIcon picker with visual preview:\n- Grid of available icons\n- Search functionality\n- Live icon preview\n\n```tsx\nfieldOverrides: {\n iconName: (layer) => iconNameFieldOverrides(layer)\n}\n```\n\n#### `commonFieldOverrides()`\nConvenience function for standard className and children overrides:\n\n```tsx\nfieldOverrides: commonFieldOverrides()\n// Equivalent to:\n// fieldOverrides: {\n// className: (layer) => classNameFieldOverrides(layer),\n// children: (layer) => childrenFieldOverrides(layer)\n// }\n```\n\n### Creating Custom Field Overrides\n\nCreate specialized input controls for unique data types:\n\n```tsx\nimport { AutoFormInputComponentProps } from '@/components/ui/ui-builder/types';\nimport { FormItem, FormLabel, FormControl } from '@/components/ui/form';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';\n\nconst customColorFieldOverride = (layer) => ({\n fieldType: ({ label, field }: AutoFormInputComponentProps) => (\n \n {label}\n \n
\n field.onChange(e.target.value)}\n className=\"w-12 h-8 rounded border\"\n />\n field.onChange(e.target.value)}\n placeholder=\"#000000\"\n className=\"flex-1 px-2 border rounded\"\n />\n
\n
\n
\n )\n});\n\n// Use in component definition:\nfieldOverrides: {\n brandColor: customColorFieldOverride,\n className: (layer) => classNameFieldOverrides(layer)\n}\n```\n\n## Default Children\n\nConfigure default child components that appear when users add components to the canvas.\n\n### Simple Text Default Children\n\n```tsx\nspan: {\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n fieldOverrides: commonFieldOverrides(),\n defaultChildren: \"Default text content\"\n}\n```\n\n### Component Layer Default Children\n\nFor complex nested structures:\n\n```tsx\nButton: {\n component: Button,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n variant: z.enum(['default', 'destructive']).default('default'),\n }),\n from: '@/components/ui/button',\n defaultChildren: [\n {\n id: \"button-text\",\n type: \"span\",\n name: \"Button Text\",\n props: {},\n children: \"Click me\",\n }\n ],\n fieldOverrides: commonFieldOverrides()\n}\n```\n\n### Complex Nested Structures\n\n```tsx\nCard: {\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: \"div\",\n name: \"Header\",\n props: { className: \"p-6 pb-0\" },\n children: [\n {\n id: \"card-title\",\n type: \"span\",\n name: \"Title\",\n props: { className: \"text-2xl font-semibold\" },\n children: \"Card Title\"\n }\n ]\n },\n {\n id: \"card-content\",\n type: \"div\",\n name: \"Content\",\n props: { className: \"p-6\" },\n children: [\n {\n id: \"card-text\",\n type: \"span\",\n name: \"Text\",\n props: {},\n children: \"Card content goes here.\"\n }\n ]\n }\n ],\n fieldOverrides: commonFieldOverrides()\n}\n```\n\n## Default Variable Bindings\n\nAutomatically bind component properties to variables when components are added to the canvas.\n\n### Basic Variable Bindings\n\n```tsx\nUserProfile: {\n component: UserProfile,\n schema: z.object({\n userId: z.string().default(''),\n displayName: z.string().default('Anonymous'),\n email: z.string().optional(),\n }),\n from: '@/components/ui/user-profile',\n defaultVariableBindings: [\n {\n propName: 'userId',\n variableId: 'current-user-id',\n immutable: true // System data - cannot be unbound\n },\n {\n propName: 'displayName',\n variableId: 'current-user-name', \n immutable: false // Can be customized\n }\n ]\n}\n```\n\n### Immutable Bindings for Brand Consistency\n\n```tsx\nBrandedButton: {\n component: BrandedButton,\n schema: z.object({\n text: z.string().default('Click me'),\n brandColor: z.string().default('#000000'),\n companyName: z.string().default('Company'),\n }),\n from: '@/components/ui/branded-button',\n defaultVariableBindings: [\n {\n propName: 'brandColor',\n variableId: 'primary-brand-color',\n immutable: true // Prevents breaking brand guidelines\n },\n {\n propName: 'companyName',\n variableId: 'company-name',\n immutable: true // Consistent branding\n }\n // 'text' is not bound, allowing content customization\n ]\n}\n```\n\n## Advanced Patterns\n\n### Conditional Field Overrides\n\n```tsx\nconst conditionalFieldOverride = (layer) => ({\n isHidden: (currentValues) => currentValues.mode === 'simple',\n fieldType: ({ label, field }) => (\n \n {label}\n \n \n \n \n )\n});\n```\n\n### Dynamic Default Children\n\nWhile UI Builder doesn't support function-based default children, you can create multiple component variants:\n\n```tsx\nSimpleCard: {\n // ... basic card with minimal default children\n},\nRichCard: {\n // ... card with comprehensive default structure\n}\n```\n\n## Best Practices\n\n### Field Overrides\n- **Always override `className`** with `classNameFieldOverrides()` for consistent Tailwind editing\n- **Always override `children`** with appropriate children override based on content type\n- **Create specialized overrides** for domain-specific data types (colors, icons, etc.)\n- **Test thoroughly** to ensure overrides work as expected in the editor\n\n### Default Children\n- **Provide meaningful defaults** that demonstrate proper component usage\n- **Keep structures shallow** to avoid overwhelming new users\n- **Use unique IDs** to prevent conflicts when components are duplicated\n- **Include all dependencies** - ensure referenced component types are in your registry\n\n### Variable Bindings\n- **Use immutable bindings** for system data that shouldn't be modified\n- **Use immutable bindings** for brand consistency (colors, logos, names)\n- **Leave content unbound** so editors can customize text and messaging\n- **Group related variables** logically in your variable definitions\n\n### Performance\n- **Use `commonFieldOverrides()`** when you need standard className/children handling\n- **Memoize expensive field overrides** if they perform complex calculations\n- **Keep default children structures reasonable** to avoid slow initial renders" + } + ] + } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/immutable-pages.ts b/app/docs/docs-data/docs-page-layers/immutable-pages.ts new file mode 100644 index 0000000..2842c4f --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/immutable-pages.ts @@ -0,0 +1,83 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const IMMUTABLE_PAGES_LAYER = { + "id": "immutable-pages", + "type": "div", + "name": "Immutable Pages", + "props": { + "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", + "data-group": "editor-features" + }, + "children": [ + { + "type": "span", + "children": "Immutable Pages", + "id": "immutable-pages-title", + "name": "Text", + "props": { + "className": "text-4xl" + } + }, + { + "id": "immutable-pages-intro", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "Control page creation and deletion permissions to create template-based editing experiences. Perfect for maintaining approved layouts while allowing content customization, or creating read-only preview modes." + }, + { + "id": "immutable-pages-demo", + "type": "div", + "name": "div", + "props": {}, + "children": [ + { + "id": "immutable-pages-badge", + "type": "Badge", + "name": "Badge", + "props": { + "variant": "default", + "className": "rounded rounded-b-none" + }, + "children": [ + { + "id": "immutable-pages-badge-text", + "type": "span", + "name": "span", + "props": {}, + "children": "Controlled Editing" + } + ] + }, + { + "id": "immutable-pages-demo-frame", + "type": "div", + "name": "div", + "props": { + "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" + }, + "children": [ + { + "id": "immutable-pages-iframe", + "type": "iframe", + "name": "iframe", + "props": { + "src": "http://localhost:3000/examples/editor/immutable-pages", + "title": "Immutable Pages Demo", + "className": "aspect-square md:aspect-video w-full" + }, + "children": [] + } + ] + } + ] + }, + { + "id": "immutable-pages-content", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Page Control Props\n\nUI Builder provides props to control page creation and deletion permissions:\n\n```tsx\n\n```\n\n## Use Cases\n\n### Template-Based Editing\n\nProvide pre-built page templates that users can customize but not restructure:\n\n```tsx\n// Pre-defined page templates\nconst templatePages = [\n {\n id: 'homepage-template',\n type: 'div',\n name: 'Homepage',\n props: {\n className: 'min-h-screen bg-background'\n },\n children: [\n {\n id: 'hero-section',\n type: 'div',\n name: 'Hero Section',\n props: {\n className: 'py-20 text-center'\n },\n children: [\n {\n id: 'hero-title',\n type: 'span',\n name: 'Hero Title',\n props: {\n className: 'text-4xl font-bold',\n children: { __variableRef: 'hero-title-var' }\n },\n children: []\n }\n ]\n }\n ]\n },\n {\n id: 'about-template',\n type: 'div', \n name: 'About Page',\n props: {\n className: 'min-h-screen bg-background p-8'\n },\n children: [\n // Pre-structured about page content\n ]\n }\n];\n\n// Variables for customization\nconst templateVariables = [\n { id: 'hero-title-var', name: 'heroTitle', type: 'string', defaultValue: 'Welcome to Our Site' },\n { id: 'company-name-var', name: 'companyName', type: 'string', defaultValue: 'Your Company' }\n];\n\n\n```\n\n### White-Label Solutions\n\nCreate branded templates for different clients:\n\n```tsx\nconst ClientTemplateBuilder = ({ clientConfig }) => {\n const clientPages = [\n {\n id: 'client-homepage',\n type: 'div',\n name: 'Homepage',\n props: {\n className: 'min-h-screen',\n style: {\n '--brand-color': clientConfig.brandColor\n }\n },\n children: [\n // Client-specific template structure\n ]\n }\n ];\n \n const clientVariables = [\n { id: 'client-name', name: 'clientName', type: 'string', defaultValue: clientConfig.name },\n { id: 'client-logo', name: 'clientLogo', type: 'string', defaultValue: clientConfig.logoUrl },\n { id: 'contact-email', name: 'contactEmail', type: 'string', defaultValue: clientConfig.email }\n ];\n \n return (\n saveClientPages(clientConfig.id, pages)}\n onVariablesChange={(vars) => saveClientVariables(clientConfig.id, vars)}\n />\n );\n};\n```\n\n### Content-Only Editing\n\nAllow content updates without structural changes:\n\n```tsx\n// Blog template with fixed structure\nconst blogTemplate = [\n {\n id: 'blog-page',\n type: 'article',\n name: 'Blog Post',\n props: {\n className: 'max-w-4xl mx-auto py-8 px-4'\n },\n children: [\n {\n id: 'blog-header',\n type: 'header',\n name: 'Post Header',\n props: { className: 'mb-8' },\n children: [\n {\n id: 'blog-title',\n type: 'span',\n name: 'Post Title',\n props: {\n className: 'text-3xl font-bold mb-4 block',\n children: { __variableRef: 'post-title' }\n },\n children: []\n },\n {\n id: 'blog-meta',\n type: 'div',\n name: 'Post Meta',\n props: { className: 'text-sm text-muted-foreground' },\n children: [\n {\n id: 'publish-date',\n type: 'span',\n name: 'Publish Date',\n props: {\n children: { __variableRef: 'publish-date' }\n },\n children: []\n }\n ]\n }\n ]\n },\n {\n id: 'blog-content',\n type: 'div',\n name: 'Post Content',\n props: {\n className: 'prose prose-lg max-w-none',\n children: { __variableRef: 'post-content' }\n },\n children: []\n }\n ]\n }\n];\n\nconst contentVariables = [\n { id: 'post-title', name: 'postTitle', type: 'string', defaultValue: 'New Blog Post' },\n { id: 'publish-date', name: 'publishDate', type: 'string', defaultValue: 'January 1, 2024' },\n { id: 'post-content', name: 'postContent', type: 'string', defaultValue: 'Write your blog post content here...' }\n];\n\n\n```\n\n## Read-Only Preview Mode\n\nCreate completely read-only experiences for previewing:\n\n```tsx\nconst PreviewOnlyBuilder = ({ pageData, variables }) => {\n // Custom read-only props panel\n const ReadOnlyPropsPanel = ({ className }) => (\n
\n

Preview Mode

\n

\n This is a read-only preview. Changes cannot be made.\n

\n \n {/* Show selected component info */}\n \n \n {/* Show variable values */}\n
\n

Variables

\n
\n {variables.map(variable => (\n
\n {variable.name}:\n {variable.defaultValue}\n
\n ))}\n
\n
\n
\n );\n \n // Custom navigation for preview mode\n const PreviewNavBar = () => (\n \n );\n \n return (\n ,\n propsPanel: \n }}\n />\n );\n};\n```\n\n## Role-Based Permissions\n\nImplement different permission levels based on user roles:\n\n```tsx\nconst getRoleBasedConfig = (userRole) => {\n switch (userRole) {\n case 'admin':\n return {\n allowPagesCreation: true,\n allowPagesDeletion: true,\n allowVariableEditing: true\n };\n \n case 'editor':\n return {\n allowPagesCreation: false, // Can't change structure\n allowPagesDeletion: false,\n allowVariableEditing: true // Can edit content\n };\n \n case 'content-manager':\n return {\n allowPagesCreation: false,\n allowPagesDeletion: false,\n allowVariableEditing: true // Content-only editing\n };\n \n case 'viewer':\n return {\n allowPagesCreation: false,\n allowPagesDeletion: false,\n allowVariableEditing: false // Read-only\n };\n \n default:\n return {\n allowPagesCreation: false,\n allowPagesDeletion: false,\n allowVariableEditing: false\n };\n }\n};\n\nconst RoleBasedBuilder = ({ user, pages, variables }) => {\n const permissions = getRoleBasedConfig(user.role);\n \n return (\n \n );\n};\n```\n\n## Progressive Enhancement\n\nStart with limited permissions and unlock features based on user progression:\n\n```tsx\nconst ProgressiveBuilder = ({ userLevel, achievements }) => {\n const canCreatePages = userLevel >= 3 || achievements.includes('page-master');\n const canDeletePages = userLevel >= 5 || achievements.includes('admin');\n const canEditVariables = userLevel >= 1;\n \n return (\n \n
\n Level {userLevel} Builder\n
\n {achievements.map(achievement => (\n \n {achievement}\n \n ))}\n
\n
\n \n
\n {!canCreatePages && 'Unlock page creation at Level 3'}\n
\n \n )\n }}\n />\n );\n};\n```\n\n## Best Practices\n\n### Template Design\n- **Create comprehensive templates** that cover all necessary content areas\n- **Use meaningful variable names** that content editors will understand\n- **Provide sensible defaults** for all variables\n- **Test templates** with real content scenarios\n\n### Permission Strategy\n- **Start restrictive** and gradually unlock features\n- **Clearly communicate** what users can and cannot do\n- **Provide upgrade paths** for users who need more permissions\n- **Document permission levels** for team understanding\n\n### User Experience\n- **Show permission status** clearly in the UI\n- **Provide helpful messages** when actions are restricted\n- **Focus on enabled capabilities** rather than disabled ones\n- **Offer alternative paths** for restricted actions\n\n### Content Management\n- **Separate structure from content** using variables effectively\n- **Version control templates** separately from content\n- **Provide content guidelines** for variable editing\n- **Monitor content quality** with validation and review processes" + } + ] + } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/introduction.ts b/app/docs/docs-data/docs-page-layers/introduction.ts index c660094..6578a1e 100644 --- a/app/docs/docs-data/docs-page-layers/introduction.ts +++ b/app/docs/docs-data/docs-page-layers/introduction.ts @@ -23,7 +23,7 @@ export const INTRODUCTION_LAYER = { "type": "Markdown", "name": "Markdown", "props": {}, - "children": "Welcome to **UI Builder**, the drop‑in visual editor for your React app. With UI Builder, you can empower non‑developers to compose pages, emails, dashboards, and white‑label views using the exact React components you already ship—no rebuilding from scratch required." + "children": "**UI Builder solves the fundamental problem of UI creation tools: they ignore your existing React component library and force you to rebuild from scratch.**\n\nUI Builder is a shadcn/ui package that adds a Figma‑style editor to your own product, letting non‑developers compose pages, emails, dashboards, and white‑label views with the exact React components you already ship.\n\nLayouts are saved as plain JSON for easy versioning and can be rendered instantly with dynamic data, allowing:\n\n- your marketing team to update a landing page without waiting on engineering\n- a customer to tweak a branded portal with their own content and branding \n- a product manager to modify email templates but parts of the content is dynamic for each user\n- add a visual \"head\" to your headless CMS, connecting your content API with your component library" }, { "id": "eR9CoTQ", @@ -77,7 +77,7 @@ export const INTRODUCTION_LAYER = { "type": "Markdown", "name": "Markdown", "props": {}, - "children": "### Why UI Builder?\n\n- **Leverage your existing components** — no extra code or maintenance overhead.\n\n- **Enable true no‑code workflows** — marketing teams, product managers, or clients can edit layouts and content without engineering support.\n\n- **Human‑readable JSON layouts** — version and review designs just like code.\n\n- **Dynamic, data‑driven interfaces** — bind component props to variables for personalized experiences.\n\n### Key Benefits\n\n1. **One‑step installation**\\\n Get up and running with a single `npx shadcn@latest add …` command.\n\n2. **Figma‑style editing**\\\n Intuitive drag‑and‑drop canvas, properties panel, and live preview.\n\n3. **Full React code export**\\\n Generate clean, type‑safe React code that matches your project structure.\n\n4. **Runtime variable binding**\\\n Create dynamic templates with string, number, and boolean variables—perfect for personalization, A/B testing, or multi‑tenant branding." + "children": "### How it unlocks novel product features:\n\n- **Give users no‑code superpowers** — add a full visual builder to your SaaS with one install\n- **Design with components you already ship** — nothing new to build or maintain\n- **Store layouts as human‑readable JSON** — render inside your product to ship changes immediately\n- **Create dynamic, data-driven interfaces** — bind component properties to variables for personalized content\n\n### Key Benefits\n\n1. **One‑step installation**\\\n Get up and running with a single `npx shadcn@latest add …` command.\n\n2. **Figma‑style editing**\\\n Intuitive drag‑and‑drop canvas, properties panel, and live preview.\n\n3. **Full React code export**\\\n Generate clean, type‑safe React code that matches your project structure.\n\n4. **Runtime variable binding**\\\n Create dynamic templates with string, number, and boolean variables—perfect for personalization, A/B testing, or multi‑tenant branding.\n\n### 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." } ] } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/layer-structure.ts b/app/docs/docs-data/docs-page-layers/layer-structure.ts new file mode 100644 index 0000000..4ce97a7 --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/layer-structure.ts @@ -0,0 +1,36 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const LAYER_STRUCTURE_LAYER = { + "id": "layer-structure", + "type": "div", + "name": "Layer Structure", + "props": { + "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", + "data-group": "layout-persistence" + }, + "children": [ + { + "type": "span", + "children": "Layer Structure", + "id": "layer-structure-title", + "name": "Text", + "props": { + "className": "text-4xl" + } + }, + { + "id": "layer-structure-intro", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "Understanding the layer structure is fundamental to working with UI Builder. Layers define the hierarchical component tree that powers the visual editor and rendering system." + }, + { + "id": "layer-structure-content", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Layer Schema\n\n### Basic Layer Structure\n```tsx\ninterface ComponentLayer {\n id: string; // Unique identifier\n type: string; // Component type (Button, div, etc.)\n name: string; // Display name in editor\n props: Record; // Component properties\n children: ComponentLayer[] | string; // Child layers or text content\n}\n```\n\n### Example Layer\n```tsx\nconst buttonLayer: ComponentLayer = {\n id: 'button-1',\n type: 'Button',\n name: 'Primary Button',\n props: {\n variant: 'primary',\n size: 'lg',\n className: 'w-full'\n },\n children: 'Click me'\n};\n```\n\n## Hierarchical Structure\n\n### Parent-Child Relationships\n```tsx\nconst cardLayer: ComponentLayer = {\n id: 'card-1',\n type: 'Card',\n name: 'Product Card',\n props: {\n className: 'p-6 border rounded-lg'\n },\n children: [\n {\n id: 'card-header',\n type: 'div',\n name: 'Header',\n props: { className: 'mb-4' },\n children: [\n {\n id: 'title',\n type: 'h3',\n name: 'Title',\n props: { className: 'text-lg font-semibold' },\n children: 'Product Name'\n }\n ]\n },\n {\n id: 'card-content',\n type: 'div',\n name: 'Content',\n props: { className: 'space-y-2' },\n children: [\n {\n id: 'description',\n type: 'p',\n name: 'Description',\n props: { className: 'text-gray-600' },\n children: 'Product description here'\n },\n {\n id: 'price',\n type: 'span',\n name: 'Price',\n props: { className: 'text-xl font-bold' },\n children: '$99.99'\n }\n ]\n }\n ]\n};\n```\n\n## Layer Types\n\n### Container Layers\n```tsx\n// Layout containers\nconst flexContainer: ComponentLayer = {\n id: 'flex-1',\n type: 'div',\n name: 'Flex Container',\n props: {\n className: 'flex items-center justify-between'\n },\n children: []\n};\n\nconst gridContainer: ComponentLayer = {\n id: 'grid-1',\n type: 'div',\n name: 'Grid Container',\n props: {\n className: 'grid grid-cols-3 gap-4'\n },\n children: []\n};\n```\n\n### Content Layers\n```tsx\n// Text content\nconst textLayer: ComponentLayer = {\n id: 'text-1',\n type: 'p',\n name: 'Paragraph',\n props: {\n className: 'text-base leading-relaxed'\n },\n children: 'Lorem ipsum dolor sit amet...'\n};\n\n// Rich content\nconst markdownLayer: ComponentLayer = {\n id: 'markdown-1',\n type: 'Markdown',\n name: 'Markdown Content',\n props: {},\n children: '# Heading\\n\\nThis is **bold** text.'\n};\n```\n\n### Interactive Layers\n```tsx\n// Form inputs\nconst inputLayer: ComponentLayer = {\n id: 'input-1',\n type: 'Input',\n name: 'Email Input',\n props: {\n type: 'email',\n placeholder: 'Enter your email',\n required: true\n },\n children: []\n};\n\n// Buttons\nconst buttonLayer: ComponentLayer = {\n id: 'button-1',\n type: 'Button',\n name: 'Submit Button',\n props: {\n type: 'submit',\n variant: 'primary'\n },\n children: 'Submit'\n};\n```\n\n## Layer Properties\n\n### Standard Props\n```tsx\ninterface LayerProps {\n // Styling\n className?: string;\n style?: React.CSSProperties;\n \n // Data binding\n 'data-*'?: string;\n \n // Event handlers\n onClick?: () => void;\n onChange?: (value: any) => void;\n \n // Accessibility\n 'aria-*'?: string;\n role?: string;\n tabIndex?: number;\n \n // Component-specific props\n [key: string]: any;\n}\n```\n\n### Dynamic Props\n```tsx\n// Variable-bound props\nconst dynamicLayer: ComponentLayer = {\n id: 'dynamic-1',\n type: 'Button',\n name: 'Dynamic Button',\n props: {\n disabled: '${isLoading}',\n className: '${buttonStyle}',\n children: '${buttonText}'\n },\n children: []\n};\n```\n\n## Layer Manipulation\n\n### Adding Layers\n```tsx\nfunction addLayer(parentId: string, newLayer: ComponentLayer) {\n const parent = findLayerById(parentId);\n if (parent && Array.isArray(parent.children)) {\n parent.children.push(newLayer);\n }\n}\n```\n\n### Removing Layers\n```tsx\nfunction removeLayer(layerId: string) {\n const parent = findParentLayer(layerId);\n if (parent && Array.isArray(parent.children)) {\n parent.children = parent.children.filter(child => \n typeof child === 'object' && child.id !== layerId\n );\n }\n}\n```\n\n### Moving Layers\n```tsx\nfunction moveLayer(\n layerId: string, \n newParentId: string, \n index?: number\n) {\n const layer = findLayerById(layerId);\n const newParent = findLayerById(newParentId);\n \n // Remove from current parent\n removeLayer(layerId);\n \n // Add to new parent\n if (newParent && Array.isArray(newParent.children)) {\n if (index !== undefined) {\n newParent.children.splice(index, 0, layer);\n } else {\n newParent.children.push(layer);\n }\n }\n}\n```\n\n### Duplicating Layers\n```tsx\nfunction duplicateLayer(layerId: string): ComponentLayer {\n const original = findLayerById(layerId);\n \n function deepClone(layer: ComponentLayer): ComponentLayer {\n return {\n ...layer,\n id: generateUniqueId(),\n children: Array.isArray(layer.children)\n ? layer.children.map(child => \n typeof child === 'string' ? child : deepClone(child)\n )\n : layer.children\n };\n }\n \n return deepClone(original);\n}\n```\n\n## Layer Validation\n\n### Schema Validation\n```tsx\nfunction validateLayer(layer: ComponentLayer): ValidationResult {\n const errors: string[] = [];\n \n // Required fields\n if (!layer.id) errors.push('Layer must have an id');\n if (!layer.type) errors.push('Layer must have a type');\n if (!layer.name) errors.push('Layer must have a name');\n \n // ID uniqueness\n if (isDuplicateId(layer.id)) {\n errors.push(`Duplicate layer id: ${layer.id}`);\n }\n \n // Component type validation\n if (!isValidComponentType(layer.type)) {\n errors.push(`Invalid component type: ${layer.type}`);\n }\n \n // Recursive validation for children\n if (Array.isArray(layer.children)) {\n layer.children.forEach(child => {\n if (typeof child === 'object') {\n const childValidation = validateLayer(child);\n errors.push(...childValidation.errors);\n }\n });\n }\n \n return {\n isValid: errors.length === 0,\n errors\n };\n}\n```\n\n## Layer Utilities\n\n### Tree Traversal\n```tsx\nfunction traverseLayer(\n layer: ComponentLayer,\n callback: (layer: ComponentLayer, depth: number) => void,\n depth = 0\n) {\n callback(layer, depth);\n \n if (Array.isArray(layer.children)) {\n layer.children.forEach(child => {\n if (typeof child === 'object') {\n traverseLayer(child, callback, depth + 1);\n }\n });\n }\n}\n```\n\n### Layer Search\n```tsx\nfunction findLayerById(layerId: string, root: ComponentLayer): ComponentLayer | null {\n if (root.id === layerId) return root;\n \n if (Array.isArray(root.children)) {\n for (const child of root.children) {\n if (typeof child === 'object') {\n const found = findLayerById(layerId, child);\n if (found) return found;\n }\n }\n }\n \n return null;\n}\n\nfunction findLayersByType(type: string, root: ComponentLayer): ComponentLayer[] {\n const results: ComponentLayer[] = [];\n \n traverseLayer(root, (layer) => {\n if (layer.type === type) {\n results.push(layer);\n }\n });\n \n return results;\n}\n```\n\n## Best Practices\n\n- **Unique IDs** - Ensure every layer has a unique identifier\n- **Meaningful Names** - Use descriptive names for editor clarity\n- **Proper Nesting** - Follow semantic HTML structure where possible\n- **Consistent Props** - Use consistent property naming conventions\n- **Validation** - Always validate layer structure before rendering\n- **Performance** - Keep layer trees reasonably shallow for performance" + } + ] + } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/page-theming.ts b/app/docs/docs-data/docs-page-layers/page-theming.ts new file mode 100644 index 0000000..9f3cb56 --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/page-theming.ts @@ -0,0 +1,36 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const PAGE_THEMING_LAYER = { + "id": "page-theming", + "type": "div", + "name": "Page Theming", + "props": { + "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", + "data-group": "rendering" + }, + "children": [ + { + "type": "span", + "children": "Page Theming", + "id": "page-theming-title", + "name": "Text", + "props": { + "className": "text-4xl" + } + }, + { + "id": "page-theming-intro", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "Apply consistent theming across UI Builder pages. Configure colors, typography, spacing, and design tokens for cohesive visual experiences." + }, + { + "id": "page-theming-content", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Theme Configuration\n\n```tsx\nconst theme = {\n colors: {\n primary: '#3B82F6',\n secondary: '#64748B',\n background: '#FFFFFF',\n text: '#1F2937'\n },\n typography: {\n fontFamily: 'Inter, sans-serif',\n sizes: {\n xs: '0.75rem',\n sm: '0.875rem',\n base: '1rem',\n lg: '1.125rem'\n }\n },\n spacing: {\n xs: '0.25rem',\n sm: '0.5rem',\n md: '1rem',\n lg: '1.5rem'\n }\n};\n\n\n```\n\n## CSS Variables\n\n```css\n:root {\n --color-primary: 59 130 246;\n --color-secondary: 100 116 139;\n --color-background: 255 255 255;\n --color-text: 31 41 55;\n \n --font-family: 'Inter', sans-serif;\n --font-size-base: 1rem;\n \n --spacing-unit: 0.25rem;\n}\n```\n\n## Dark Mode Support\n\n```tsx\nconst darkTheme = {\n colors: {\n primary: '#60A5FA',\n background: '#1F2937',\n text: '#F9FAFB'\n }\n};\n\nfunction ThemedUIBuilder() {\n const [isDark, setIsDark] = useState(false);\n \n return (\n \n );\n}\n```\n\n## Dynamic Theming\n\n```tsx\nfunction DynamicTheming() {\n const [theme, setTheme] = useState(defaultTheme);\n \n const updateThemeColor = (property, value) => {\n setTheme(prev => ({\n ...prev,\n colors: {\n ...prev.colors,\n [property]: value\n }\n }));\n };\n \n return (\n
\n \n \n
\n );\n}\n```" + } + ] + } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/pages-panel.ts b/app/docs/docs-data/docs-page-layers/pages-panel.ts new file mode 100644 index 0000000..0acf364 --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/pages-panel.ts @@ -0,0 +1,83 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const PAGES_PANEL_LAYER = { + "id": "pages-panel", + "type": "div", + "name": "Pages Panel", + "props": { + "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", + "data-group": "editor-features" + }, + "children": [ + { + "type": "span", + "children": "Pages Panel", + "id": "pages-panel-title", + "name": "Text", + "props": { + "className": "text-4xl" + } + }, + { + "id": "pages-panel-intro", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "The pages panel allows users to manage multiple pages within a single project. Create, organize, and navigate between different layouts and views using the layer store's page management system." + }, + { + "id": "pages-panel-demo", + "type": "div", + "name": "div", + "props": {}, + "children": [ + { + "id": "pages-panel-badge", + "type": "Badge", + "name": "Badge", + "props": { + "variant": "default", + "className": "rounded rounded-b-none" + }, + "children": [ + { + "id": "pages-panel-badge-text", + "type": "span", + "name": "span", + "props": {}, + "children": "Multi-Page Editor" + } + ] + }, + { + "id": "pages-panel-demo-frame", + "type": "div", + "name": "div", + "props": { + "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" + }, + "children": [ + { + "id": "pages-panel-iframe", + "type": "iframe", + "name": "iframe", + "props": { + "src": "http://localhost:3000/examples/editor/immutable-pages", + "title": "Pages Panel Demo", + "className": "aspect-square md:aspect-video w-full" + }, + "children": [] + } + ] + } + ] + }, + { + "id": "pages-panel-content", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Page Management\n\n### How Pages Work\n\nIn UI Builder, pages are essentially top-level component layers. Each page is a root component (like `div`, `main`, or any container) that serves as the foundation for a complete layout:\n\n```tsx\n// Example page structure\nconst initialLayers = [\n {\n id: \"homepage\",\n type: \"div\", // Any component can be a page\n name: \"Home Page\",\n props: {\n className: \"min-h-screen bg-background\"\n },\n children: [\n // Page content components\n ]\n },\n {\n id: \"about-page\", \n type: \"main\",\n name: \"About Us\",\n props: {\n className: \"container mx-auto py-8\"\n },\n children: [\n // About page content\n ]\n }\n];\n\n\n```\n\n### Page Creation and Management\n\nControl page creation through UIBuilder props:\n\n```tsx\n {\n // Save pages to your backend\n savePagesToDatabase(pages);\n }}\n/>\n```\n\n### Page Operations\n\nThe layer store provides methods for page management:\n\n```tsx\n// Access page management functions\nconst {\n pages, // Array of all pages\n selectedPageId, // Currently selected page ID\n addPage, // Create a new page\n removePage, // Delete a page\n duplicatePage, // Clone an existing page\n updatePage // Update page properties\n} = useLayerStore();\n\n// Example: Adding a new page programmatically\nconst createNewPage = () => {\n const newPage = {\n id: generateId(),\n type: \"div\",\n name: \"New Page\",\n props: {\n className: \"min-h-screen p-4\"\n },\n children: []\n };\n \n addPage(newPage);\n};\n```\n\n### Page Navigation\n\nUsers can navigate between pages through:\n\n- **Page List** - Click on any page to switch to it\n- **Page Tabs** - Quick switching between open pages\n- **Page Context Menu** - Right-click for page options\n- **Keyboard Shortcuts** - Fast navigation with hotkeys\n\n### Page Properties\n\nEach page supports the same properties as any component:\n\n```tsx\n// Page with custom properties\nconst customPage = {\n id: \"landing-page\",\n type: \"main\", // Use semantic HTML elements\n name: \"Landing Page\",\n props: {\n className: \"bg-gradient-to-b from-blue-50 to-white min-h-screen\",\n \"data-page-type\": \"landing\", // Custom attributes\n role: \"main\" // Accessibility\n },\n children: [\n // Page content structure\n ]\n};\n```\n\n## Multi-Page Project Structure\n\n### Shared Components Across Pages\n\nCreate reusable components that can be used across multiple pages:\n\n```tsx\nconst componentRegistry = {\n ...primitiveComponentDefinitions,\n // Shared header component\n SiteHeader: {\n component: SiteHeader,\n schema: z.object({\n title: z.string().default(\"My Site\"),\n showNavigation: z.boolean().default(true)\n }),\n from: \"@/components/site-header\"\n },\n // Shared footer component\n SiteFooter: {\n component: SiteFooter,\n schema: z.object({\n year: z.number().default(new Date().getFullYear())\n }),\n from: \"@/components/site-footer\"\n }\n};\n```\n\n### Global Variables\n\nUse variables to share data across all pages:\n\n```tsx\nconst globalVariables = [\n {\n id: \"site-title\",\n name: \"siteTitle\",\n type: \"string\",\n defaultValue: \"My Website\"\n },\n {\n id: \"brand-color\",\n name: \"brandColor\", \n type: \"string\",\n defaultValue: \"#3b82f6\"\n }\n];\n\n\n```\n\n### Page Templates\n\nCreate template pages that can be duplicated:\n\n```tsx\n// Template page with common structure\nconst pageTemplate = {\n id: \"page-template\",\n type: \"div\",\n name: \"Page Template\",\n props: {\n className: \"min-h-screen flex flex-col\"\n },\n children: [\n {\n id: \"header-section\",\n type: \"SiteHeader\",\n name: \"Header\",\n props: { title: { __variableRef: \"site-title\" } },\n children: []\n },\n {\n id: \"main-content\",\n type: \"main\",\n name: \"Main Content\",\n props: {\n className: \"flex-1 container mx-auto py-8\"\n },\n children: [\n // Template content\n ]\n },\n {\n id: \"footer-section\",\n type: \"SiteFooter\",\n name: \"Footer\", \n props: {},\n children: []\n }\n ]\n};\n```\n\n## Page Configuration Panel\n\nThe page configuration panel provides:\n\n- **Page Properties** - Edit page name and metadata\n- **Page Settings** - Configure page-specific options\n- **Duplicate Page** - Clone the current page\n- **Delete Page** - Remove the page (if multiple pages exist)\n\n## Responsive Pages\n\nPages automatically support responsive design:\n\n```tsx\n// Responsive page layout\nconst responsivePage = {\n id: \"responsive-page\",\n type: \"div\",\n name: \"Responsive Layout\",\n props: {\n className: \"min-h-screen p-4 md:p-8 lg:p-12\"\n },\n children: [\n {\n id: \"content-grid\",\n type: \"div\",\n name: \"Content Grid\",\n props: {\n className: \"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4\"\n },\n children: [\n // Responsive grid content\n ]\n }\n ]\n};\n```\n\n## Benefits of Multi-Page Support\n\n- **Organized Content** - Separate different sections into dedicated pages\n- **Modular Design** - Build reusable components that work across pages\n- **Efficient Workflow** - Edit multiple pages in the same session\n- **Consistent Branding** - Share variables and components across pages\n- **Easy Navigation** - Switch between pages without losing work" + } + ] + } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/panel-configuration.ts b/app/docs/docs-data/docs-page-layers/panel-configuration.ts new file mode 100644 index 0000000..d6ffeff --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/panel-configuration.ts @@ -0,0 +1,83 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const PANEL_CONFIGURATION_LAYER = { + "id": "panel-configuration", + "type": "div", + "name": "Panel Configuration", + "props": { + "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", + "data-group": "editor-features" + }, + "children": [ + { + "type": "span", + "children": "Panel Configuration", + "id": "panel-configuration-title", + "name": "Text", + "props": { + "className": "text-4xl" + } + }, + { + "id": "panel-configuration-intro", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "Configure the editor's panel system to match your workflow. Control the layout, content, and behavior of the main UI Builder panels through the `panelConfig` prop for a customized editing experience." + }, + { + "id": "panel-configuration-demo", + "type": "div", + "name": "div", + "props": {}, + "children": [ + { + "id": "panel-configuration-badge", + "type": "Badge", + "name": "Badge", + "props": { + "variant": "default", + "className": "rounded rounded-b-none" + }, + "children": [ + { + "id": "panel-configuration-badge-text", + "type": "span", + "name": "span", + "props": {}, + "children": "Customizable Layout" + } + ] + }, + { + "id": "panel-configuration-demo-frame", + "type": "div", + "name": "div", + "props": { + "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" + }, + "children": [ + { + "id": "panel-configuration-iframe", + "type": "iframe", + "name": "iframe", + "props": { + "src": "http://localhost:3000/examples/editor", + "title": "Panel Configuration Demo", + "className": "aspect-square md:aspect-video w-full" + }, + "children": [] + } + ] + } + ] + }, + { + "id": "panel-configuration-content", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Available Panels\n\nUI Builder consists of four main panels that can be customized:\n\n### Core Panels\n- **Navigation Bar** - Top toolbar with editor controls\n- **Page Config Panel** - Left panel with Layers, Appearance, and Data tabs\n- **Editor Panel** - Center canvas for visual editing\n- **Props Panel** - Right panel for component property editing\n\n## Basic Panel Configuration\n\nUse the `panelConfig` prop to customize any panel:\n\n```tsx\nimport UIBuilder from '@/components/ui/ui-builder';\nimport { NavBar } from '@/components/ui/ui-builder/internal/components/nav';\nimport LayersPanel from '@/components/ui/ui-builder/internal/layers-panel';\nimport EditorPanel from '@/components/ui/ui-builder/internal/editor-panel';\nimport PropsPanel from '@/components/ui/ui-builder/internal/props-panel';\n\n,\n \n // Custom editor panel\n editorPanel: ,\n \n // Custom props panel \n propsPanel: ,\n \n // Custom page config panel\n pageConfigPanel: \n }}\n/>\n```\n\n## Page Config Panel Tabs\n\nThe most common customization is modifying the left panel tabs:\n\n```tsx\nimport { VariablesPanel } from '@/components/ui/ui-builder/internal/variables-panel';\nimport { TailwindThemePanel } from '@/components/ui/ui-builder/internal/tailwind-theme-panel';\nimport { ConfigPanel } from '@/components/ui/ui-builder/internal/config-panel';\n\nconst customTabsContent = {\n // Required: Layers tab\n layers: { \n title: \"Structure\", \n content: \n },\n \n // Optional: Appearance tab\n appearance: { \n title: \"Styling\", \n content: (\n
\n \n \n \n
\n )\n },\n \n // Optional: Data tab\n data: { \n title: \"Variables\", \n content: \n },\n \n // Add completely custom tabs\n assets: {\n title: \"Assets\",\n content: \n }\n};\n\n\n```\n\n## Default Panel Configuration\n\nThis is the default panel setup (equivalent to not providing `panelConfig`):\n\n```tsx\nimport { \n defaultConfigTabsContent, \n getDefaultPanelConfigValues \n} from '@/components/ui/ui-builder';\n\n// Default tabs content\nconst defaultTabs = defaultConfigTabsContent();\n// Returns:\n// {\n// layers: { title: \"Layers\", content: },\n// appearance: { title: \"Appearance\", content: },\n// data: { title: \"Data\", content: }\n// }\n\n// Default panel values\nconst defaultPanels = getDefaultPanelConfigValues(defaultTabs);\n// Returns:\n// {\n// navBar: ,\n// pageConfigPanel: ,\n// editorPanel: ,\n// propsPanel: \n// }\n```\n\n## Custom Navigation Bar\n\nReplace the default navigation with your own:\n\n```tsx\nconst MyCustomNavBar = () => {\n const showLeftPanel = useEditorStore(state => state.showLeftPanel);\n const toggleLeftPanel = useEditorStore(state => state.toggleLeftPanel);\n const showRightPanel = useEditorStore(state => state.showRightPanel);\n const toggleRightPanel = useEditorStore(state => state.toggleRightPanel);\n \n return (\n \n );\n};\n\n\n }}\n/>\n```\n\n## Custom Editor Panel\n\nReplace the canvas area with custom functionality:\n\n```tsx\nconst MyCustomEditor = ({ className }) => {\n const selectedPageId = useLayerStore(state => state.selectedPageId);\n const findLayerById = useLayerStore(state => state.findLayerById);\n const currentPage = findLayerById(selectedPageId);\n \n return (\n
\n {/* Custom toolbar */}\n
\n \n \n \n \n
\n \n {/* Custom canvas */}\n
\n
\n {currentPage && (\n \n )}\n
\n
\n
\n );\n};\n\n\n }}\n/>\n```\n\n## Responsive Panel Behavior\n\nUI Builder automatically handles responsive layouts:\n\n### Desktop Layout\n- **Three panels** side by side using ResizablePanelGroup\n- **Resizable handles** between panels\n- **Collapsible panels** via editor store state\n\n### Mobile Layout\n- **Single panel view** with bottom navigation\n- **Panel switcher** at the bottom\n- **Full-screen panels** for better mobile experience\n\n### Panel Visibility Control\n\n```tsx\n// Control panel visibility programmatically\nconst MyPanelController = () => {\n const { \n showLeftPanel, \n showRightPanel,\n toggleLeftPanel, \n toggleRightPanel \n } = useEditorStore();\n \n return (\n
\n \n \n
\n );\n};\n```\n\n## Integration with Editor State\n\nPanels integrate with the editor state management:\n\n```tsx\n// Access editor state in custom panels\nconst MyCustomPanel = () => {\n const componentRegistry = useEditorStore(state => state.registry);\n const allowPagesCreation = useEditorStore(state => state.allowPagesCreation);\n const allowVariableEditing = useEditorStore(state => state.allowVariableEditing);\n \n const selectedLayerId = useLayerStore(state => state.selectedLayerId);\n const pages = useLayerStore(state => state.pages);\n const variables = useLayerStore(state => state.variables);\n \n return (\n
\n

Custom Panel

\n

Registry has {Object.keys(componentRegistry).length} components

\n

Current page: {pages.find(p => p.id === selectedLayerId)?.name}

\n

Variables: {variables.length}

\n
\n );\n};\n```\n\n## Best Practices\n\n### Panel Design\n- **Follow existing patterns** for consistency\n- **Use proper overflow handling** (`overflow-y-auto`) for scrollable content\n- **Include proper padding/spacing** (`px-4 py-2`)\n- **Respect theme variables** for colors and spacing\n\n### State Management\n- **Use editor and layer stores** for state access\n- **Don't duplicate state** - use the existing stores\n- **Subscribe to specific slices** to avoid unnecessary re-renders\n- **Use proper cleanup** in useEffect hooks\n\n### Performance\n- **Memoize expensive components** with React.memo\n- **Use virtualization** for large lists\n- **Debounce rapid updates** when needed\n- **Minimize re-renders** by careful state subscription\n\n### Accessibility\n- **Provide proper ARIA labels** for custom controls\n- **Ensure keyboard navigation** works correctly\n- **Use semantic HTML** where possible\n- **Test with screen readers** for complex interactions" + } + ] + } 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..0c849ac --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/persistence.ts @@ -0,0 +1,36 @@ +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": "Learn how UI Builder manages state and provides flexible persistence options for your layouts and variables. Save to databases, manage auto-save behavior, and handle state changes with simple, powerful APIs." + }, + { + "id": "persistence-content", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Understanding UI Builder State\n\nUI Builder manages two main types of state:\n- **Layers**: The component hierarchy and structure\n- **Variables**: Dynamic data that can be bound to component properties\n\n## Local Storage Persistence\n\nBy default, UI Builder automatically saves state to browser local storage:\n\n```tsx\n// Default behavior - auto-saves to localStorage\n\n\n// Disable local storage persistence\n\n```\n\n## Database Integration\n\nFor production applications, you'll want to save state to your database:\n\n```tsx\nimport { useState, useEffect } from 'react';\nimport UIBuilder from '@/components/ui/ui-builder';\nimport { ComponentLayer, Variable } from '@/components/ui/ui-builder/types';\n\nfunction DatabaseIntegratedBuilder({ userId }: { userId: string }) {\n const [initialLayers, setInitialLayers] = useState();\n const [initialVariables, setInitialVariables] = useState();\n const [isLoading, setIsLoading] = useState(true);\n\n // Load initial state from database\n useEffect(() => {\n async function loadUserLayout() {\n try {\n const response = await fetch(`/api/layouts/${userId}`);\n const data = await response.json();\n \n setInitialLayers(data.layers || []);\n setInitialVariables(data.variables || []);\n } catch (error) {\n console.error('Failed to load layout:', error);\n // Fallback to empty state\n setInitialLayers([]);\n setInitialVariables([]);\n } finally {\n setIsLoading(false);\n }\n }\n\n loadUserLayout();\n }, [userId]);\n\n // Save layers to database\n const handleLayersChange = async (updatedLayers: ComponentLayer[]) => {\n try {\n await fetch(`/api/layouts/${userId}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers: updatedLayers })\n });\n } catch (error) {\n console.error('Failed to save layers:', error);\n }\n };\n\n // Save variables to database\n const handleVariablesChange = async (updatedVariables: Variable[]) => {\n try {\n await fetch(`/api/variables/${userId}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ variables: updatedVariables })\n });\n } catch (error) {\n console.error('Failed to save variables:', error);\n }\n };\n\n if (isLoading) {\n return
Loading your layout...
;\n }\n\n return (\n \n );\n}\n```\n\n## Debounced Auto-Save\n\nTo avoid excessive API calls, implement debounced saving:\n\n```tsx\nimport { useCallback } from 'react';\nimport { debounce } from 'lodash';\n\nfunction AutoSaveBuilder() {\n // Debounced save function - waits 2 seconds after last change\n const debouncedSave = useCallback(\n debounce(async (layers: ComponentLayer[]) => {\n try {\n await fetch('/api/layouts/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers })\n });\n console.log('Auto-saved successfully');\n } catch (error) {\n console.error('Auto-save failed:', error);\n }\n }, 2000),\n []\n );\n\n const debouncedSaveVariables = useCallback(\n debounce(async (variables: Variable[]) => {\n try {\n await fetch('/api/variables/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ variables })\n });\n console.log('Variables auto-saved successfully');\n } catch (error) {\n console.error('Variables auto-save failed:', error);\n }\n }, 2000),\n []\n );\n\n return (\n \n );\n}\n```\n\n## Manual Save with UI Feedback\n\nProvide users with explicit save controls:\n\n```tsx\nimport { useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Badge } from '@/components/ui/badge';\n\nfunction ManualSaveBuilder() {\n const [currentLayers, setCurrentLayers] = useState([]);\n const [currentVariables, setCurrentVariables] = useState([]);\n const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);\n const [isSaving, setIsSaving] = useState(false);\n\n const handleLayersChange = (layers: ComponentLayer[]) => {\n setCurrentLayers(layers);\n setHasUnsavedChanges(true);\n };\n\n const handleVariablesChange = (variables: Variable[]) => {\n setCurrentVariables(variables);\n setHasUnsavedChanges(true);\n };\n\n const handleSave = async () => {\n setIsSaving(true);\n try {\n await Promise.all([\n fetch('/api/layouts/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers: currentLayers })\n }),\n fetch('/api/variables/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ variables: currentVariables })\n })\n ]);\n \n setHasUnsavedChanges(false);\n alert('Saved successfully!');\n } catch (error) {\n console.error('Save failed:', error);\n alert('Save failed. Please try again.');\n } finally {\n setIsSaving(false);\n }\n };\n\n return (\n
\n {/* Save Controls */}\n
\n \n \n {hasUnsavedChanges && (\n Unsaved Changes\n )}\n
\n \n {/* Builder */}\n
\n \n
\n
\n );\n}\n```\n\n## Data Format\n\nUI Builder saves data in a simple, readable JSON format:\n\n```json\n{\n \"layers\": [\n {\n \"id\": \"page-1\",\n \"type\": \"div\",\n \"name\": \"Page 1\",\n \"props\": {\n \"className\": \"p-4 bg-white\"\n },\n \"children\": [\n {\n \"id\": \"button-1\",\n \"type\": \"Button\",\n \"name\": \"Submit Button\",\n \"props\": {\n \"variant\": \"default\",\n \"children\": { \"__variableRef\": \"buttonText\" }\n },\n \"children\": []\n }\n ]\n }\n ],\n \"variables\": [\n {\n \"id\": \"buttonText\",\n \"name\": \"Button Text\",\n \"type\": \"string\",\n \"defaultValue\": \"Click Me!\"\n }\n ]\n}\n```\n\n## API Route Examples\n\nHere are example API routes for Next.js:\n\n### Save Layout API Route\n\n```tsx\n// app/api/layouts/[userId]/route.ts\nimport { NextRequest, NextResponse } from 'next/server';\n\nexport async function PUT(\n request: NextRequest,\n { params }: { params: { userId: string } }\n) {\n try {\n const { layers } = await request.json();\n const { userId } = params;\n \n // Save to your database\n await saveUserLayout(userId, layers);\n \n return NextResponse.json({ success: true });\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to save layout' },\n { status: 500 }\n );\n }\n}\n\nexport async function GET(\n request: NextRequest,\n { params }: { params: { userId: string } }\n) {\n try {\n const { userId } = params;\n const layout = await getUserLayout(userId);\n \n return NextResponse.json(layout);\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to load layout' },\n { status: 500 }\n );\n }\n}\n```\n\n### Save Variables API Route\n\n```tsx\n// app/api/variables/[userId]/route.ts\nimport { NextRequest, NextResponse } from 'next/server';\n\nexport async function PUT(\n request: NextRequest,\n { params }: { params: { userId: string } }\n) {\n try {\n const { variables } = await request.json();\n const { userId } = params;\n \n await saveUserVariables(userId, variables);\n \n return NextResponse.json({ success: true });\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to save variables' },\n { status: 500 }\n );\n }\n}\n```\n\n## Error Handling & Recovery\n\nImplement robust error handling for persistence:\n\n```tsx\nfunction RobustBuilder() {\n const [lastSavedState, setLastSavedState] = useState(null);\n const [saveError, setSaveError] = useState(null);\n\n const handleSaveWithRetry = async (layers: ComponentLayer[], retries = 3) => {\n for (let i = 0; i < retries; i++) {\n try {\n await fetch('/api/layouts/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers })\n });\n \n setLastSavedState(layers);\n setSaveError(null);\n return;\n } catch (error) {\n if (i === retries - 1) {\n setSaveError('Failed to save after multiple attempts');\n console.error('Save failed after retries:', error);\n } else {\n // Wait before retry\n await new Promise(resolve => setTimeout(resolve, 1000));\n }\n }\n }\n };\n\n const handleRecovery = () => {\n if (lastSavedState) {\n // Restore to last saved state\n window.location.reload();\n }\n };\n\n return (\n
\n {saveError && (\n
\n Save Error: {saveError}\n \n
\n )}\n \n \n
\n );\n}\n```\n\n## Best Practices\n\n1. **Always handle save errors gracefully** - Show user feedback and provide recovery options\n2. **Use debouncing for auto-save** - Avoid overwhelming your API with requests\n3. **Validate data before saving** - Ensure the data structure is correct\n4. **Provide manual save controls** - Give users explicit control over when data is saved\n5. **Consider offline support** - Store changes locally when the network is unavailable\n6. **Implement proper loading states** - Show users when data is being loaded or saved\n7. **Use proper error boundaries** - Prevent save errors from crashing the entire editor" + } + ] + } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/props-panel-customization.ts b/app/docs/docs-data/docs-page-layers/props-panel-customization.ts new file mode 100644 index 0000000..546d689 --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/props-panel-customization.ts @@ -0,0 +1,83 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const PROPS_PANEL_CUSTOMIZATION_LAYER = { + "id": "props-panel-customization", + "type": "div", + "name": "Props Panel Customization", + "props": { + "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", + "data-group": "editor-features" + }, + "children": [ + { + "type": "span", + "children": "Props Panel Customization", + "id": "props-panel-customization-title", + "name": "Text", + "props": { + "className": "text-4xl" + } + }, + { + "id": "props-panel-customization-intro", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "Customize the properties panel to create intuitive, context-aware editing experiences. Design custom field types through AutoForm field overrides and create specialized property editors for your components." + }, + { + "id": "props-panel-customization-demo", + "type": "div", + "name": "div", + "props": {}, + "children": [ + { + "id": "props-panel-customization-badge", + "type": "Badge", + "name": "Badge", + "props": { + "variant": "default", + "className": "rounded rounded-b-none" + }, + "children": [ + { + "id": "props-panel-customization-badge-text", + "type": "span", + "name": "span", + "props": {}, + "children": "Custom Property Forms" + } + ] + }, + { + "id": "props-panel-customization-demo-frame", + "type": "div", + "name": "div", + "props": { + "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" + }, + "children": [ + { + "id": "props-panel-customization-iframe", + "type": "iframe", + "name": "iframe", + "props": { + "src": "http://localhost:3000/examples/editor", + "title": "Props Panel Customization Demo", + "className": "aspect-square md:aspect-video w-full" + }, + "children": [] + } + ] + } + ] + }, + { + "id": "props-panel-customization-content", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Field Overrides System\n\nUI Builder uses AutoForm to generate property forms from Zod schemas. You can customize individual fields using the `fieldOverrides` property in your component registry:\n\n```tsx\nimport { z } from 'zod';\nimport { classNameFieldOverrides, childrenAsTextareaFieldOverrides } from '@/lib/ui-builder/registry/form-field-overrides';\n\nconst MyCard = {\n component: Card,\n schema: z.object({\n title: z.string().default('Card Title'),\n description: z.string().optional(),\n imageUrl: z.string().optional(),\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: '@/components/ui/card',\n fieldOverrides: {\n // Use built-in className editor with Tailwind autocomplete\n className: (layer) => classNameFieldOverrides(layer),\n \n // Use textarea for description\n description: {\n fieldType: 'textarea',\n inputProps: {\n placeholder: 'Enter card description...',\n rows: 3\n }\n },\n \n // Custom image picker\n imageUrl: {\n fieldType: 'input',\n inputProps: {\n type: 'url',\n placeholder: 'https://example.com/image.jpg'\n },\n description: 'URL of the image to display'\n },\n \n // Use textarea for children content\n children: (layer) => childrenAsTextareaFieldOverrides(layer)\n }\n};\n```\n\n## Built-in Field Overrides\n\nUI Builder provides several pre-built field overrides for common use cases:\n\n### Common Field Overrides\n\n```tsx\nimport { \n commonFieldOverrides,\n classNameFieldOverrides,\n childrenFieldOverrides,\n childrenAsTextareaFieldOverrides \n} from '@/lib/ui-builder/registry/form-field-overrides';\n\n// Apply both className and children overrides\nfieldOverrides: commonFieldOverrides()\n\n// Or use individual overrides\nfieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer),\n children: (layer) => childrenFieldOverrides(layer)\n}\n```\n\n### className Field Override\n\nProvides intelligent Tailwind CSS class suggestions:\n\n```tsx\nfieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer)\n}\n\n// Features:\n// - Autocomplete suggestions\n// - Tailwind class validation\n// - Responsive class support\n// - Common pattern suggestions\n```\n\n### children Field Override\n\nSmart handling for component children:\n\n```tsx\nfieldOverrides: {\n // Standard children editor\n children: (layer) => childrenFieldOverrides(layer),\n \n // Or force textarea for text content\n children: (layer) => childrenAsTextareaFieldOverrides(layer)\n}\n\n// Features:\n// - String content as textarea\n// - Component children as visual editor\n// - Variable binding support\n```\n\n## AutoForm Field Configuration\n\nCustomize how AutoForm renders your fields:\n\n### Basic Field Types\n\n```tsx\nfieldOverrides: {\n // Text input with placeholder\n title: {\n inputProps: {\n placeholder: 'Enter title...',\n maxLength: 100\n }\n },\n \n // Number input with constraints\n count: {\n inputProps: {\n min: 0,\n max: 999,\n step: 1\n }\n },\n \n // Textarea with custom rows\n description: {\n fieldType: 'textarea',\n inputProps: {\n rows: 4,\n placeholder: 'Describe your component...'\n }\n },\n \n // Color input\n backgroundColor: {\n fieldType: 'input',\n inputProps: {\n type: 'color'\n }\n },\n \n // URL input with validation\n link: {\n fieldType: 'input',\n inputProps: {\n type: 'url',\n placeholder: 'https://example.com'\n }\n }\n}\n```\n\n### Advanced Field Configuration\n\n```tsx\nfieldOverrides: {\n // Custom field with description and label\n apiEndpoint: {\n inputProps: {\n placeholder: '/api/data',\n pattern: '^/api/.*'\n },\n description: 'Relative API endpoint path',\n label: 'API Endpoint'\n },\n \n // Hidden field (for internal use)\n internalId: {\n isHidden: true\n },\n \n // Field with custom validation message\n email: {\n inputProps: {\n type: 'email',\n placeholder: 'user@example.com'\n },\n description: 'Valid email address required'\n }\n}\n```\n\n## Custom Field Components\n\nCreate completely custom field editors:\n\n```tsx\n// Custom spacing control component\nconst SpacingControl = ({ value, onChange }) => {\n const [spacing, setSpacing] = useState(value || { top: 0, right: 0, bottom: 0, left: 0 });\n \n const updateSpacing = (side, newValue) => {\n const updated = { ...spacing, [side]: newValue };\n setSpacing(updated);\n onChange(updated);\n };\n \n return (\n
\n
\n updateSpacing('top', parseInt(e.target.value))}\n className=\"text-center p-1 border rounded\"\n placeholder=\"T\"\n />\n
\n updateSpacing('left', parseInt(e.target.value))}\n className=\"text-center p-1 border rounded\"\n placeholder=\"L\"\n />\n
\n updateSpacing('right', parseInt(e.target.value))}\n className=\"text-center p-1 border rounded\"\n placeholder=\"R\"\n />\n
\n updateSpacing('bottom', parseInt(e.target.value))}\n className=\"text-center p-1 border rounded\"\n placeholder=\"B\"\n />\n
\n );\n};\n\n// Use custom component in field override\nfieldOverrides: {\n spacing: {\n renderParent: ({ children, ...props }) => (\n \n )\n }\n}\n```\n\n## Component-Specific Customizations\n\nTailor field overrides to specific component types:\n\n### Button Component\n\n```tsx\nconst ButtonComponent = {\n component: Button,\n schema: z.object({\n variant: z.enum(['default', 'destructive', 'outline', 'secondary', 'ghost', 'link']).default('default'),\n size: z.enum(['default', 'sm', 'lg', 'icon']).default('default'),\n disabled: z.boolean().optional(),\n children: z.any().optional(),\n onClick: z.string().optional(),\n className: z.string().optional()\n }),\n from: '@/components/ui/button',\n fieldOverrides: {\n // Use common overrides for basic props\n ...commonFieldOverrides(),\n \n // Custom click handler editor\n onClick: {\n inputProps: {\n placeholder: 'console.log(\"Button clicked\")',\n family: 'monospace'\n },\n description: 'JavaScript code to execute on click',\n label: 'Click Handler'\n },\n \n // Enhanced variant selector with descriptions\n variant: {\n description: 'Visual style of the button'\n }\n }\n};\n```\n\n### Image Component\n\n```tsx\nconst ImageComponent = {\n component: 'img',\n schema: z.object({\n src: z.string().url(),\n alt: z.string(),\n width: z.coerce.number().optional(),\n height: z.coerce.number().optional(),\n className: z.string().optional()\n }),\n fieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer),\n \n // Image URL with preview\n src: {\n inputProps: {\n type: 'url',\n placeholder: 'https://example.com/image.jpg'\n },\n description: 'URL of the image to display',\n // Note: Custom preview would require a custom render component\n },\n \n // Alt text with guidance\n alt: {\n inputProps: {\n placeholder: 'Describe the image for accessibility'\n },\n description: 'Alternative text for screen readers'\n },\n \n // Dimensions with constraints\n width: {\n inputProps: {\n min: 1,\n max: 2000,\n step: 1\n }\n },\n \n height: {\n inputProps: {\n min: 1,\n max: 2000,\n step: 1\n }\n }\n }\n};\n```\n\n## Variable Binding Integration\n\nField overrides work seamlessly with variable binding:\n\n```tsx\n// Component with variable-bindable properties\nconst UserCard = {\n component: UserCard,\n schema: z.object({\n name: z.string().default(''),\n email: z.string().email().optional(),\n avatar: z.string().url().optional(),\n role: z.enum(['admin', 'user', 'guest']).default('user')\n }),\n from: '@/components/user-card',\n fieldOverrides: {\n name: {\n inputProps: {\n placeholder: 'User full name'\n },\n description: 'Can be bound to user data variable'\n },\n \n email: {\n inputProps: {\n type: 'email',\n placeholder: 'user@example.com'\n },\n description: 'Bind to user email variable for dynamic content'\n },\n \n avatar: {\n inputProps: {\n type: 'url',\n placeholder: 'https://example.com/avatar.jpg'\n },\n description: 'Profile picture URL - can be bound to user avatar variable'\n }\n }\n};\n\n// Variables for binding\nconst userVariables = [\n { id: 'user-name', name: 'currentUserName', type: 'string', defaultValue: 'John Doe' },\n { id: 'user-email', name: 'currentUserEmail', type: 'string', defaultValue: 'john@example.com' },\n { id: 'user-avatar', name: 'currentUserAvatar', type: 'string', defaultValue: '/default-avatar.png' }\n];\n```\n\n## Best Practices\n\n### Field Override Design\n- **Use built-in overrides** for common properties like `className` and `children`\n- **Provide helpful placeholders** and descriptions\n- **Match field types** to the expected data (url, email, number, etc.)\n- **Include validation hints** in descriptions\n\n### User Experience\n- **Group related fields** logically\n- **Use appropriate input types** for better mobile experience\n- **Provide clear labels** and descriptions\n- **Test with real content** to ensure usability\n\n### Performance\n- **Memoize field override functions** to prevent unnecessary re-renders\n- **Use simple field overrides** when possible instead of custom components\n- **Debounce rapid input changes** for expensive operations\n\n### Accessibility\n- **Provide proper labels** for all form fields\n- **Include helpful descriptions** for complex fields\n- **Ensure keyboard navigation** works correctly\n- **Use semantic form elements** where appropriate" + } + ] + } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/props-panel.ts b/app/docs/docs-data/docs-page-layers/props-panel.ts new file mode 100644 index 0000000..6f063a7 --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/props-panel.ts @@ -0,0 +1,83 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const PROPS_PANEL_LAYER = { + "id": "props-panel", + "type": "div", + "name": "Props Panel", + "props": { + "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", + "data-group": "editor-features" + }, + "children": [ + { + "type": "span", + "children": "Props Panel", + "id": "props-panel-title", + "name": "Text", + "props": { + "className": "text-4xl" + } + }, + { + "id": "props-panel-intro", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "The props panel provides an intuitive interface for editing component properties. It automatically generates appropriate controls based on component Zod schemas using AutoForm, with support for custom field overrides." + }, + { + "id": "props-panel-demo", + "type": "div", + "name": "div", + "props": {}, + "children": [ + { + "id": "props-panel-badge", + "type": "Badge", + "name": "Badge", + "props": { + "variant": "default", + "className": "rounded rounded-b-none" + }, + "children": [ + { + "id": "props-panel-badge-text", + "type": "span", + "name": "span", + "props": {}, + "children": "Auto-Generated Forms" + } + ] + }, + { + "id": "props-panel-demo-frame", + "type": "div", + "name": "div", + "props": { + "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" + }, + "children": [ + { + "id": "props-panel-iframe", + "type": "iframe", + "name": "iframe", + "props": { + "src": "http://localhost:3000/examples/editor", + "title": "Props Panel Demo", + "className": "aspect-square md:aspect-video w-full" + }, + "children": [] + } + ] + } + ] + }, + { + "id": "props-panel-content", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Auto-Generated Controls\n\nThe props panel automatically creates appropriate input controls based on component Zod schemas:\n\n```tsx\nimport { z } from 'zod';\nimport { Button } from '@/components/ui/button';\n\n// Component registration with Zod schema\nconst componentRegistry = {\n Button: {\n component: Button,\n schema: z.object({\n variant: z.enum(['default', 'destructive', 'outline', 'secondary', 'ghost', 'link']).default('default'),\n size: z.enum(['default', 'sm', 'lg', 'icon']).default('default'),\n disabled: z.boolean().optional(),\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: '@/components/ui/button',\n defaultChildren: 'Click me'\n }\n};\n```\n\n## Supported Field Types\n\nAutoForm automatically generates controls for these Zod types:\n\n### Basic Types\n- **`z.string()`** - Text input\n- **`z.number()`** - Number input with step controls\n- **`z.boolean()`** - Checkbox toggle\n- **`z.date()`** - Date picker\n- **`z.enum()`** - Select dropdown\n\n### Advanced Types\n- **`z.array()`** - Array input with add/remove buttons\n- **`z.object()`** - Nested object editor\n- **`z.union()`** - Multiple type selector\n- **`z.optional()`** - Optional field with toggle\n\n### Examples\n\n```tsx\nconst advancedSchema = z.object({\n // Text with validation\n title: z.string().min(1, 'Title is required').max(100, 'Too long'),\n \n // Number with constraints\n count: z.coerce.number().min(0).max(100).default(1),\n \n // Enum for dropdowns\n size: z.enum(['sm', 'md', 'lg']).default('md'),\n \n // Optional boolean\n enabled: z.boolean().optional(),\n \n // Date input\n publishDate: z.coerce.date().optional(),\n \n // Array of objects\n items: z.array(z.object({\n name: z.string(),\n value: z.string()\n })).default([]),\n \n // Nested object\n config: z.object({\n theme: z.enum(['light', 'dark']),\n autoSave: z.boolean().default(true)\n }).optional()\n});\n```\n\n## Field Overrides\n\nCustomize the auto-generated form fields using `fieldOverrides`:\n\n```tsx\nimport { classNameFieldOverrides, childrenAsTextareaFieldOverrides } from '@/lib/ui-builder/registry/form-field-overrides';\n\nconst MyComponent = {\n component: MyComponent,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n color: z.string().default('#000000'),\n description: z.string().optional(),\n }),\n from: '@/components/my-component',\n fieldOverrides: {\n // Use built-in className editor with Tailwind suggestions\n className: (layer) => classNameFieldOverrides(layer),\n \n // Use textarea for children instead of default input\n children: (layer) => childrenAsTextareaFieldOverrides(layer),\n \n // Custom color picker\n color: {\n fieldType: 'input',\n inputProps: {\n type: 'color',\n className: 'w-full h-10'\n }\n },\n \n // Custom textarea with placeholder\n description: {\n fieldType: 'textarea',\n inputProps: {\n placeholder: 'Enter description...',\n rows: 3\n }\n }\n }\n};\n```\n\n### Built-in Field Overrides\n\nUI Builder provides several pre-built field overrides:\n\n```tsx\nimport { \n commonFieldOverrides,\n classNameFieldOverrides, \n childrenAsTextareaFieldOverrides,\n childrenFieldOverrides\n} from '@/lib/ui-builder/registry/form-field-overrides';\n\n// Apply common overrides (className + children)\nfieldOverrides: commonFieldOverrides()\n\n// Or individual overrides\nfieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer),\n children: (layer) => childrenFieldOverrides(layer)\n}\n```\n\n## Variable Binding\n\nThe props panel supports variable binding for dynamic content:\n\n```tsx\n// Component with variable-bound property\nconst buttonWithVariable = {\n id: 'dynamic-button',\n type: 'Button',\n props: {\n children: { __variableRef: 'button-text-var' }, // Bound to variable\n variant: 'primary' // Static value\n }\n};\n\n// Variable definition\nconst variables = [\n {\n id: 'button-text-var',\n name: 'buttonText',\n type: 'string',\n defaultValue: 'Click me!'\n }\n];\n```\n\nVariable-bound fields show:\n- **Variable icon** indicating the binding\n- **Variable name** instead of the raw value\n- **Quick unbind** option to convert back to static value\n\n## Panel Features\n\n### Component Actions\n- **Duplicate Component** - Clone the selected component\n- **Delete Component** - Remove the component from the page\n- **Component Type** - Shows the current component type\n\n### Form Validation\n- **Real-time validation** using Zod schema constraints\n- **Error messages** displayed inline with fields\n- **Required field indicators** for mandatory properties\n\n### Responsive Design\n- **Mobile-friendly** interface with collapsible sections\n- **Touch-optimized** controls for mobile editing\n- **Adaptive layout** based on screen size\n\n## Working with Complex Components\n\n### Nested Objects\n```tsx\nconst complexSchema = z.object({\n layout: z.object({\n direction: z.enum(['row', 'column']),\n gap: z.number().default(4),\n align: z.enum(['start', 'center', 'end'])\n }),\n styling: z.object({\n background: z.string().optional(),\n border: z.boolean().default(false),\n rounded: z.boolean().default(true)\n })\n});\n```\n\n### Array Fields\n```tsx\nconst listSchema = z.object({\n items: z.array(z.object({\n label: z.string(),\n value: z.string(),\n enabled: z.boolean().default(true)\n })).default([])\n});\n```\n\n## Integration with Layer Store\n\nThe props panel integrates directly with the layer store:\n\n```tsx\n// Access props panel state\nconst selectedLayerId = useLayerStore(state => state.selectedLayerId);\nconst findLayerById = useLayerStore(state => state.findLayerById);\nconst updateLayer = useLayerStore(state => state.updateLayer);\n\n// Props panel automatically updates when:\n// - A component is selected\n// - Component properties change\n// - Variables are updated\n```\n\n## Best Practices\n\n### Schema Design\n- **Use descriptive property names** that map to actual component props\n- **Provide sensible defaults** using `.default()`\n- **Add validation** with `.min()`, `.max()`, and custom refinements\n- **Use enums** for predefined options\n\n### Field Overrides\n- **Use built-in overrides** for common props like `className` and `children`\n- **Provide helpful placeholders** and labels\n- **Consider user experience** when choosing input types\n- **Test with real content** to ensure fields work as expected\n\n### Performance\n- **Memoize field overrides** to prevent unnecessary re-renders\n- **Use specific field types** rather than generic inputs\n- **Debounce rapid changes** for better performance" + } + ] + } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/quick-start.ts b/app/docs/docs-data/docs-page-layers/quick-start.ts new file mode 100644 index 0000000..68163ee --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/quick-start.ts @@ -0,0 +1,90 @@ +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 will walk you through installation and creating your first visual editor." + }, + { + "id": "quick-start-install", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Installation\n\nIf you are using shadcn/ui in your project, you can install the component directly from the registry:\n\n```bash\nnpx shadcn@latest add https://raw.githubusercontent.com/olliethedev/ui-builder/main/registry/block-registry.json\n```\n\nOr you can start a new project with the UI Builder:\n\n```bash\nnpx shadcn@latest init https://raw.githubusercontent.com/olliethedev/ui-builder/main/registry/block-registry.json\n```\n\n**Note:** You need to use [style variables](https://ui.shadcn.com/docs/theming) to have page theming working correctly.\n\n### Fixing Dependencies\n\nAdd dev dependencies, since there currently seems to be an issue with shadcn/ui not installing them from the registry:\n\n```bash\nnpm install -D @types/lodash.template @tailwindcss/typography @types/react-syntax-highlighter tailwindcss-animate @types/object-hash\n```" + }, + { + "id": "quick-start-basic-usage", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Basic Example\n\nTo use the UI Builder, you **must** provide a component registry:\n\n```tsx\nimport z from \"zod\";\nimport UIBuilder from \"@/components/ui/ui-builder\";\nimport { Button } from \"@/components/ui/button\";\nimport { ComponentRegistry, ComponentLayer } from \"@/components/ui/ui-builder/types\";\nimport { commonFieldOverrides, classNameFieldOverrides, childrenAsTextareaFieldOverrides } from \"@/lib/ui-builder/registry/form-field-overrides\";\n\n// Define your component registry\nconst myComponentRegistry: ComponentRegistry = {\n Button: {\n component: Button,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n variant: z\n .enum([\n \"default\",\n \"destructive\",\n \"outline\",\n \"secondary\",\n \"ghost\",\n \"link\",\n ])\n .default(\"default\"),\n size: z.enum([\"default\", \"sm\", \"lg\", \"icon\"]).default(\"default\"),\n }),\n from: \"@/components/ui/button\",\n defaultChildren: [\n {\n id: \"button-text\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Hello World\",\n } satisfies ComponentLayer,\n ],\n fieldOverrides: commonFieldOverrides()\n },\n span: {\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n fieldOverrides: {\n className:(layer)=> classNameFieldOverrides(layer),\n children: (layer)=> childrenAsTextareaFieldOverrides(layer)\n },\n defaultChildren: \"Text\"\n },\n};\n\nexport function App() {\n return (\n \n );\n}\n```\n\n**Important:** Make sure to include definitions for all component types referenced in your `defaultChildren`. In this example, the Button's `defaultChildren` references a `span` component, so we include `span` in our registry." + }, + { + "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": "http://localhost:3000/examples/basic", + "title": "Quick Start Example", + "className": "aspect-square md:aspect-video" + }, + "children": [] + } + ] + } + ] + } + ] + } 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..42af8d3 --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/read-only-mode.ts @@ -0,0 +1,36 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const READ_ONLY_MODE_LAYER = { + "id": "read-only-mode", + "type": "div", + "name": "Read Only Mode", + "props": { + "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", + "data-group": "data-variables" + }, + "children": [ + { + "type": "span", + "children": "Read Only Mode", + "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. This enables read-only modes perfect for production environments, previews, and restricted editing scenarios." + }, + { + "id": "read-only-mode-content", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Controlling Edit Permissions\n\nUI Builder provides granular control over editing capabilities through specific props:\n\n```tsx\n\n```\n\n### Available Permission Controls\n\n| Prop | Default | Description |\n|------|---------|-------------|\n| `allowVariableEditing` | `true` | Controls variable add/edit/delete operations |\n| `allowPagesCreation` | `true` | Controls ability to create new pages |\n| `allowPagesDeletion` | `true` | Controls ability to delete existing pages |\n\n## Read-Only Variable Mode\n\n### Disabling Variable Editing\n\nWhen `allowVariableEditing={false}`, the Variables panel becomes read-only:\n\n```tsx\nfunction ReadOnlyVariablesExample() {\n const systemVariables = [\n {\n id: 'company-name',\n name: 'companyName',\n type: 'string',\n defaultValue: 'Acme Corp'\n },\n {\n id: 'brand-color',\n name: 'brandColor',\n type: 'string',\n defaultValue: '#3b82f6'\n }\n ];\n\n return (\n \n );\n}\n```\n\n### What's Disabled in Read-Only Variable Mode\n\n- ❌ **Add Variable** button is hidden\n- ❌ **Edit Variable** buttons are hidden on variable cards\n- ❌ **Delete Variable** buttons are hidden on variable cards\n- ✅ **Variable binding** still works in props panel\n- ✅ **Variable values** can still be overridden in LayerRenderer\n- ✅ **Immutable bindings** remain enforced\n\n## Read-Only Pages Mode\n\n### Restricting Page Operations\n\n```tsx\nfunction RestrictedPagesExample() {\n const templatePages = [\n {\n id: 'home-template',\n type: 'div',\n name: 'Home Template',\n props: { className: 'p-4' },\n children: []\n },\n {\n id: 'about-template',\n type: 'div',\n name: 'About Template',\n props: { className: 'p-4' },\n children: []\n }\n ];\n\n return (\n \n );\n}\n```\n\n### Page Restriction Effects\n\nWith `allowPagesCreation={false}`:\n- ❌ **Add Page** functionality is disabled\n- ✅ **Page content editing** remains available\n- ✅ **Page switching** between existing pages works\n\nWith `allowPagesDeletion={false}`:\n- ❌ **Delete Page** buttons are hidden in props panel\n- ✅ **Page content editing** remains available\n- ✅ **Page duplication** may still work (creates duplicates, doesn't delete)\n\n## Complete Read-Only Mode\n\n### Fully Restricted Editor\n\nFor maximum restrictions, disable all editing capabilities:\n\n```tsx\nfunction FullyReadOnlyEditor() {\n return (\n \n );\n}\n```\n\n### What Still Works in Full Read-Only Mode\n\n- ✅ **Component selection** and navigation\n- ✅ **Visual editing** of component properties\n- ✅ **Layer manipulation** (add, remove, reorder components)\n- ✅ **Variable binding** in props panel\n- ✅ **Theme configuration** in appearance panel\n- ✅ **Code generation** and export\n- ✅ **Undo/Redo** operations\n\n## Use Cases and Patterns\n\n### Production Preview Mode\n\n```tsx\nfunction ProductionPreview({ templateId }) {\n const [template, setTemplate] = useState(null);\n const [variables, setVariables] = useState([]);\n\n useEffect(() => {\n // Load template and variables from API\n Promise.all([\n fetch(`/api/templates/${templateId}`).then(r => r.json()),\n fetch(`/api/templates/${templateId}/variables`).then(r => r.json())\n ]).then(([templateData, variableData]) => {\n setTemplate(templateData);\n setVariables(variableData);\n });\n }, [templateId]);\n\n if (!template) return
Loading...
;\n\n return (\n
\n
\n

Template Preview

\n

Read-only mode - variables locked

\n
\n \n \n
\n );\n}\n```\n\n### Role-Based Editing Restrictions\n\n```tsx\nfunction RoleBasedEditor({ user, template }) {\n const canEditVariables = user.role === 'admin' || user.role === 'developer';\n const canManagePages = user.role === 'admin';\n\n return (\n \n );\n}\n```\n\n### Content Editor Mode\n\n```tsx\nfunction ContentEditorMode() {\n // Content editors can modify component content but not structure\n return (\n \n );\n}\n```\n\n### Environment-Based Restrictions\n\n```tsx\nfunction EnvironmentAwareEditor() {\n const isProduction = process.env.NODE_ENV === 'production';\n const isDevelopment = process.env.NODE_ENV === 'development';\n \n return (\n \n );\n}\n```\n\n## Rendering Without Editor\n\n### Using LayerRenderer for Display-Only\n\nFor pure display without any editing interface, use `LayerRenderer`:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\n\nfunction DisplayOnlyPage({ pageData, variables, userValues }) {\n return (\n \n );\n}\n```\n\n### LayerRenderer vs. Restricted UIBuilder\n\n| Feature | LayerRenderer | Restricted UIBuilder |\n|---------|---------------|----------------------|\n| Bundle size | Smaller (no editor) | Larger (full editor) |\n| Performance | Faster (no editor overhead) | Slower (editor present) |\n| Editing UI | None | Present but restricted |\n| Variable binding | ✅ | ✅ |\n| Code generation | ❌ | ✅ |\n| Visual editing | ❌ | ✅ (limited) |\n\n## Programmatic Control\n\n### Dynamic Permission Updates\n\n```tsx\nfunction DynamicPermissionsEditor() {\n const [permissions, setPermissions] = useState({\n allowVariableEditing: false,\n allowPagesCreation: false,\n allowPagesDeletion: false\n });\n\n const enableEditMode = () => {\n setPermissions({\n allowVariableEditing: true,\n allowPagesCreation: true,\n allowPagesDeletion: true\n });\n };\n\n const enableReadOnlyMode = () => {\n setPermissions({\n allowVariableEditing: false,\n allowPagesCreation: false,\n allowPagesDeletion: false\n });\n };\n\n return (\n
\n
\n \n \n
\n \n \n
\n );\n}\n```\n\n### Feature Flag Integration\n\n```tsx\nfunction FeatureFlagEditor({ featureFlags }) {\n return (\n \n );\n}\n```\n\n## Security Considerations\n\n### Variable Security\n\n```tsx\n// Secure sensitive variables from editing\nconst secureVariables = [\n {\n id: 'api-key',\n name: 'apiKey',\n type: 'string',\n defaultValue: 'sk_live_...'\n },\n {\n id: 'user-permissions',\n name: 'userPermissions',\n type: 'string',\n defaultValue: 'read-only'\n }\n];\n\nfunction SecureEditor() {\n return (\n \n );\n}\n```\n\n### Input Validation\n\n```tsx\nfunction ValidatedEditor({ initialData }) {\n // Validate and sanitize data before passing to UI Builder\n const sanitizedPages = sanitizePageData(initialData.pages);\n const validatedVariables = validateVariables(initialData.variables);\n \n return (\n \n );\n}\n```\n\n## Best Practices\n\n### Choosing the Right Restrictions\n\n- **Use `allowVariableEditing={false}`** for production deployments\n- **Use `allowPagesCreation={false}`** for content-only editing\n- **Use `allowPagesDeletion={false}`** to prevent accidental page loss\n- **Use `LayerRenderer`** for pure display without editing needs\n\n### User Experience Considerations\n\n- **Provide clear feedback** about restricted functionality\n- **Use role-based restrictions** rather than blanket restrictions\n- **Consider progressive permissions** (unlock features as users gain trust)\n- **Document restriction reasons** for transparency\n\n### Performance Optimization\n\n- **Use `LayerRenderer`** when editing isn't needed\n- **Minimize editor bundle** in production builds\n- **Cache restricted configurations** to avoid re-computation\n- **Consider server-side rendering** for display-only scenarios" + } + ] + } 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..6dd4d15 --- /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 without the editor interface using the LayerRenderer component. Perfect for displaying your designs in production with full variable binding and dynamic content support." + }, + { + "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": "Live 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 Rendering\n\nUse the `LayerRenderer` component to render UI Builder pages without the editor:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\nimport { ComponentLayer, ComponentRegistry } from '@/components/ui/ui-builder/types';\n\n// Your component registry (same as used in UIBuilder)\nconst myComponentRegistry: ComponentRegistry = {\n // Your component definitions\n};\n\n// Page data (from UIBuilder or database)\nconst page: ComponentLayer = {\n id: \"my-page\",\n type: \"div\",\n name: \"My Page\",\n props: {\n className: \"p-4\"\n },\n children: [\n // Your page structure\n ]\n};\n\nfunction MyRenderedPage() {\n return (\n \n );\n}\n```\n\n## Rendering with Variables\n\nThe real power of LayerRenderer comes from variable binding - same page structure with different data:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\nimport { ComponentLayer, Variable } from '@/components/ui/ui-builder/types';\n\n// Define variables for dynamic content\nconst variables: Variable[] = [\n {\n id: \"userName\",\n name: \"User Name\",\n type: \"string\",\n defaultValue: \"John Doe\"\n },\n {\n id: \"userAge\",\n name: \"User Age\", \n type: \"number\",\n defaultValue: 25\n },\n {\n id: \"isActive\",\n name: \"Is Active\",\n type: \"boolean\",\n defaultValue: true\n }\n];\n\n// Page with variable bindings\nconst pageWithVariables: ComponentLayer = {\n id: \"user-profile\",\n type: \"div\",\n props: {\n className: \"p-6 bg-white rounded-lg shadow\"\n },\n children: [\n {\n id: \"welcome-text\",\n type: \"h1\",\n props: {\n className: \"text-2xl font-bold\",\n children: { __variableRef: \"userName\" } // Bound to userName variable\n },\n children: []\n },\n {\n id: \"age-text\",\n type: \"p\",\n props: {\n children: { __variableRef: \"userAge\" } // Bound to userAge variable\n },\n children: []\n }\n ]\n};\n\n// Override variable values at runtime\nconst variableValues = {\n userName: \"Jane Smith\", // Override default\n userAge: 30, // Override default\n isActive: false // Override default\n};\n\nfunction DynamicUserProfile() {\n return (\n \n );\n}\n```\n\n## Multi-Tenant Applications\n\nPerfect for white-label applications where each customer gets customized branding:\n\n```tsx\nfunction CustomerDashboard({ customerId }: { customerId: string }) {\n const [pageData, setPageData] = useState(null);\n const [customerVariables, setCustomerVariables] = useState({});\n \n useEffect(() => {\n async function loadCustomerPage() {\n // Load the page structure (same for all customers)\n const pageResponse = await fetch('/api/templates/dashboard');\n const page = await pageResponse.json();\n \n // Load customer-specific variable values\n const varsResponse = await fetch(`/api/customers/${customerId}/branding`);\n const variables = await varsResponse.json();\n \n setPageData(page);\n setCustomerVariables(variables);\n }\n \n loadCustomerPage();\n }, [customerId]);\n \n if (!pageData) return
Loading...
;\n \n return (\n \n );\n}\n```\n\n## Server-Side Rendering (SSR)\n\nLayerRenderer works with Next.js SSR for better performance and SEO:\n\n```tsx\n// pages/page/[id].tsx or app/page/[id]/page.tsx\nimport { GetServerSideProps } from 'next';\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\n\ninterface PageProps {\n page: ComponentLayer;\n variables: Variable[];\n variableValues: Record;\n}\n\n// Server-side data fetching\nexport const getServerSideProps: GetServerSideProps = async ({ params }) => {\n const pageId = params?.id as string;\n \n // Fetch page data from your database\n const [page, variables, userData] = await Promise.all([\n getPageById(pageId),\n getPageVariables(pageId),\n getCurrentUserData() // For personalization\n ]);\n \n const variableValues = {\n userName: userData.name,\n userEmail: userData.email,\n // Inject real data into variables\n };\n \n return {\n props: {\n page,\n variables,\n variableValues\n }\n };\n};\n\n// Component renders on server\nfunction ServerRenderedPage({ page, variables, variableValues }: PageProps) {\n return (\n \n );\n}\n\nexport default ServerRenderedPage;\n```\n\n## Real-Time Data Integration\n\nBind to live data sources for dynamic, real-time interfaces:\n\n```tsx\nfunction LiveDashboard() {\n const [liveData, setLiveData] = useState({\n activeUsers: 0,\n revenue: 0,\n conversionRate: 0\n });\n \n // Subscribe to real-time updates\n useEffect(() => {\n const socket = new WebSocket('ws://localhost:8080/analytics');\n \n socket.onmessage = (event) => {\n const data = JSON.parse(event.data);\n setLiveData(data);\n };\n \n return () => socket.close();\n }, []);\n \n return (\n \n );\n}\n```\n\n## A/B Testing & Feature Flags\n\nUse boolean variables for conditional rendering:\n\n```tsx\nfunction ABTestPage({ userId }: { userId: string }) {\n const [experimentFlags, setExperimentFlags] = useState({});\n \n useEffect(() => {\n // Determine which experiment variant user should see\n async function getExperimentFlags() {\n const response = await fetch(`/api/experiments/${userId}`);\n const flags = await response.json();\n setExperimentFlags(flags);\n }\n \n getExperimentFlags();\n }, [userId]);\n \n return (\n \n );\n}\n```\n\n## LayerRenderer Props Reference\n\n- **`page`** (required): The ComponentLayer to render\n- **`componentRegistry`** (required): Registry of available components\n- **`className`**: CSS class for the root container\n- **`variables`**: Array of Variable definitions for the page\n- **`variableValues`**: Object mapping variable IDs to runtime values\n- **`editorConfig`**: Internal editor configuration (rarely needed)\n\n## Performance Optimization\n\nOptimize rendering performance for large pages:\n\n```tsx\n// Memoize the renderer to prevent unnecessary re-renders\nconst MemoizedRenderer = React.memo(LayerRenderer, (prevProps, nextProps) => {\n return (\n prevProps.page === nextProps.page &&\n JSON.stringify(prevProps.variableValues) === JSON.stringify(nextProps.variableValues)\n );\n});\n\n// Use in your component\nfunction OptimizedPage() {\n return (\n \n );\n}\n```\n\n## Error Handling\n\nHandle rendering errors gracefully:\n\n```tsx\nimport { ErrorBoundary } from 'react-error-boundary';\n\nfunction ErrorFallback({ error }: { error: Error }) {\n return (\n
\n

Something went wrong

\n

{error.message}

\n \n
\n );\n}\n\nfunction SafeRenderedPage() {\n return (\n \n \n \n );\n}\n```\n\n## Best Practices\n\n1. **Always use the same componentRegistry** in both UIBuilder and LayerRenderer\n2. **Validate variable values** before passing to LayerRenderer to prevent runtime errors\n3. **Handle loading states** while fetching page data and variables\n4. **Use memoization** for expensive variable calculations\n5. **Implement error boundaries** to gracefully handle rendering failures\n6. **Consider caching** page data and variable values for better performance\n7. **Test with different variable combinations** to ensure your pages are robust" + } + ] + } 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..fe9df8c --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/variable-binding.ts @@ -0,0 +1,36 @@ +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": "Variable binding connects dynamic data to component properties, enabling interfaces that update automatically when variable values change. Learn how to bind variables through the UI and programmatically." + }, + { + "id": "variable-binding-content", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## How Variable Binding Works\n\nVariable binding in UI Builder replaces static property values with dynamic references to variables. When a component renders, these references are resolved to actual values.\n\n### Binding Structure\n\nWhen bound, a component property stores a variable reference object:\n\n```tsx\n// Before binding - static value\nconst button = {\n props: {\n children: 'Click me',\n disabled: false\n }\n};\n\n// After binding - variable references\nconst button = {\n props: {\n children: { __variableRef: 'button-text-var' },\n disabled: { __variableRef: 'is-loading-var' }\n }\n};\n```\n\n## Binding Variables Through the UI\n\n### Step-by-Step Binding Process\n\n1. **Select a component** in the editor canvas\n2. **Open the Properties panel** (right sidebar)\n3. **Find the property** you want to bind\n4. **Click the link icon** (🔗) next to the property field\n5. **Choose a variable** from the dropdown menu\n6. **The property is now bound** and shows the variable info\n\n### Visual Indicators\n\nBound properties are visually distinct in the props panel:\n\n- **Link icon** indicates the property supports binding\n- **Variable card** shows when a property is bound\n- **Variable name and type** are displayed\n- **Current value** shows the variable's default/resolved value\n- **Unlink button** allows unbinding (if not immutable)\n- **Lock icon** indicates immutable bindings\n\n### Unbinding Variables\n\nTo remove a variable binding:\n\n1. **Select the component** with bound properties\n2. **Find the bound property** in the props panel\n3. **Click the unlink icon** (🔗⛌) next to the variable card\n4. **Property reverts** to its default schema value\n\n**Note:** Immutable bindings (marked with 🔒) cannot be unbound through the UI.\n\n## Variable Resolution at Runtime\n\n### In LayerRenderer\n\nWhen rendering pages, variable references are resolved to actual values:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\n\n// Page with variable bindings\nconst pageWithBindings = {\n id: 'welcome-page',\n type: 'div',\n props: { className: 'p-4' },\n children: [\n {\n id: 'welcome-button',\n type: 'Button',\n props: {\n children: { __variableRef: 'welcome-message' },\n disabled: { __variableRef: 'is-loading' }\n }\n }\n ]\n};\n\n// Variables definition\nconst variables = [\n {\n id: 'welcome-message',\n name: 'welcomeMessage',\n type: 'string',\n defaultValue: 'Welcome!'\n },\n {\n id: 'is-loading',\n name: 'isLoading',\n type: 'boolean',\n defaultValue: false\n }\n];\n\n// Runtime values override defaults\nconst variableValues = {\n 'welcome-message': 'Hello, Jane!',\n 'is-loading': true\n};\n\nfunction MyPage() {\n return (\n \n );\n}\n\n// Renders as:\n// \n```\n\n### Resolution Process\n\n1. **Scan component props** for variable reference objects\n2. **Look up variable by ID** in the variables array\n3. **Use runtime value** from `variableValues` if provided\n4. **Fall back to default value** from variable definition\n5. **Replace reference** with resolved value\n6. **Pass resolved props** to React component\n\n## Automatic Variable Binding\n\n### Default Variable Bindings\n\nComponents can automatically bind to variables when added to the canvas:\n\n```tsx\nconst componentRegistry = {\n UserCard: {\n component: UserCard,\n schema: z.object({\n userId: z.string(),\n displayName: z.string(),\n avatarUrl: z.string().optional(),\n isOnline: z.boolean().default(false)\n }),\n from: '@/components/ui/user-card',\n defaultVariableBindings: [\n {\n propName: 'userId',\n variableId: 'current-user-id',\n immutable: true // Cannot be unbound\n },\n {\n propName: 'displayName',\n variableId: 'current-user-name',\n immutable: false // Can be changed\n },\n {\n propName: 'isOnline',\n variableId: 'user-online-status',\n immutable: true\n }\n ]\n }\n};\n```\n\n### Immutable Bindings\n\nImmutable bindings provide several benefits:\n\n- **System consistency** - Critical data cannot be accidentally unbound\n- **Security** - User permissions and IDs remain locked\n- **Branding** - Company logos and colors stay consistent\n- **Template integrity** - Essential bindings are preserved\n\n```tsx\n// Example: Brand-consistent button component\nconst BrandButton = {\n component: Button,\n schema: z.object({\n children: z.string(),\n style: z.object({\n backgroundColor: z.string(),\n color: z.string()\n }).optional()\n }),\n defaultVariableBindings: [\n {\n propName: 'style.backgroundColor',\n variableId: 'brand-primary-color',\n immutable: true // Locked to brand colors\n },\n {\n propName: 'style.color',\n variableId: 'brand-text-color',\n immutable: true\n }\n // children prop is left unbound for flexibility\n ]\n};\n```\n\n## Variable Binding in Code Generation\n\nWhen generating React code, variable bindings are converted to prop references:\n\n```tsx\n// Original component layer with bindings\nconst buttonLayer = {\n type: 'Button',\n props: {\n children: { __variableRef: 'button-text' },\n disabled: { __variableRef: 'is-disabled' },\n variant: 'primary' // Static value\n }\n};\n\n// Generated React code\ninterface PageProps {\n variables: {\n buttonText: string;\n isDisabled: boolean;\n };\n}\n\nconst Page = ({ variables }: PageProps) => {\n return (\n \n );\n};\n```\n\n## Managing Variable Bindings Programmatically\n\n### Using Layer Store Methods\n\n```tsx\nimport { useLayerStore } from '@/lib/ui-builder/store/layer-store';\n\nfunction CustomBindingControl() {\n const bindPropToVariable = useLayerStore((state) => state.bindPropToVariable);\n const unbindPropFromVariable = useLayerStore((state) => state.unbindPropFromVariable);\n const isBindingImmutable = useLayerStore((state) => state.isBindingImmutable);\n\n const handleBind = () => {\n // Bind a component's 'title' prop to a variable\n bindPropToVariable('button-123', 'title', 'page-title-var');\n };\n\n const handleUnbind = () => {\n // Check if binding is immutable first\n if (!isBindingImmutable('button-123', 'title')) {\n unbindPropFromVariable('button-123', 'title');\n }\n };\n\n return (\n
\n \n \n
\n );\n}\n```\n\n### Variable Reference Detection\n\n```tsx\nimport { isVariableReference } from '@/lib/ui-builder/utils/variable-resolver';\n\n// Check if a prop value is a variable reference\nconst propValue = layer.props.children;\n\nif (isVariableReference(propValue)) {\n console.log('Bound to variable:', propValue.__variableRef);\n} else {\n console.log('Static value:', propValue);\n}\n```\n\n## Advanced Binding Patterns\n\n### Conditional Property Binding\n\n```tsx\n// Use boolean variables to control component behavior\nconst variables = [\n {\n id: 'show-avatar',\n name: 'showAvatar',\n type: 'boolean',\n defaultValue: true\n },\n {\n id: 'user-role',\n name: 'userRole',\n type: 'string',\n defaultValue: 'user'\n }\n];\n\n// Bind to component properties\nconst userCard = {\n type: 'UserCard',\n props: {\n showAvatar: { __variableRef: 'show-avatar' },\n role: { __variableRef: 'user-role' }\n }\n};\n```\n\n### Multi-Component Binding\n\n```tsx\n// Bind the same variable to multiple components\nconst themeVariable = {\n id: 'current-theme',\n name: 'currentTheme',\n type: 'string',\n defaultValue: 'light'\n};\n\n// Multiple components can reference the same variable\nconst header = {\n type: 'Header',\n props: {\n theme: { __variableRef: 'current-theme' }\n }\n};\n\nconst sidebar = {\n type: 'Sidebar',\n props: {\n theme: { __variableRef: 'current-theme' }\n }\n};\n\nconst footer = {\n type: 'Footer',\n props: {\n theme: { __variableRef: 'current-theme' }\n }\n};\n```\n\n## Variable Binding Best Practices\n\n### Design Patterns\n\n- **Use meaningful variable names** that clearly indicate their purpose\n- **Group related variables** (e.g., user data, theme settings, feature flags)\n- **Set appropriate default values** for better editor preview experience\n- **Document variable purposes** in component registry definitions\n- **Use immutable bindings** for system-critical or brand-related data\n\n### Performance Considerations\n\n- **Variable resolution is optimized** through memoization in the rendering process\n- **Only bound properties** are processed during variable resolution\n- **Static values** are passed through without processing overhead\n- **Variable updates** trigger efficient re-renders only for affected components\n\n### Debugging Tips\n\n```tsx\n// Check variable bindings in browser dev tools\nconst layer = useLayerStore.getState().findLayerById('my-component');\nconsole.log('Layer props:', layer?.props);\n\n// Verify variable resolution\nimport { resolveVariableReferences } from '@/lib/ui-builder/utils/variable-resolver';\n\nconst resolved = resolveVariableReferences(\n layer.props,\n variables,\n variableValues\n);\nconsole.log('Resolved props:', resolved);\n```\n\n## Troubleshooting Common Issues\n\n### Variable Not Found\n\n- **Check variable ID** matches exactly in both definition and reference\n- **Verify variable exists** in the variables array\n- **Ensure variable scope** (editor vs. renderer) includes the needed variable\n\n### Binding Not Working\n\n- **Confirm variable reference format** uses `{ __variableRef: 'variable-id' }`\n- **Check variable type compatibility** with component prop expectations\n- **Verify component schema** allows the property to be bound\n\n### Immutable Binding Issues\n\n- **Check defaultVariableBindings** configuration in component registry\n- **Verify immutable flag** is set correctly for auto-bound properties\n- **Use layer store methods** to check binding immutability programmatically" + } + ] + } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/variables-panel.ts b/app/docs/docs-data/docs-page-layers/variables-panel.ts new file mode 100644 index 0000000..230fb06 --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/variables-panel.ts @@ -0,0 +1,83 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const VARIABLES_PANEL_LAYER = { + "id": "variables-panel", + "type": "div", + "name": "Variables Panel", + "props": { + "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", + "data-group": "editor-features" + }, + "children": [ + { + "type": "span", + "children": "Variables Panel", + "id": "variables-panel-title", + "name": "Text", + "props": { + "className": "text-4xl" + } + }, + { + "id": "variables-panel-intro", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "The variables panel provides a visual interface for creating and managing dynamic data in your layouts. Define variables that can be bound to component properties for data-driven, personalized interfaces." + }, + { + "id": "variables-panel-demo", + "type": "div", + "name": "div", + "props": {}, + "children": [ + { + "id": "variables-panel-badge", + "type": "Badge", + "name": "Badge", + "props": { + "variant": "default", + "className": "rounded rounded-b-none" + }, + "children": [ + { + "id": "variables-panel-badge-text", + "type": "span", + "name": "span", + "props": {}, + "children": "Live Example" + } + ] + }, + { + "id": "variables-panel-demo-frame", + "type": "div", + "name": "div", + "props": { + "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" + }, + "children": [ + { + "id": "variables-panel-iframe", + "type": "iframe", + "name": "iframe", + "props": { + "src": "http://localhost:3000/examples/renderer/variables", + "title": "Variables Panel Demo", + "className": "aspect-square md:aspect-video w-full" + }, + "children": [] + } + ] + } + ] + }, + { + "id": "variables-panel-content", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Variable Types\n\nUI Builder supports three core variable types:\n\n```tsx\n// String variable for text content\n{\n id: 'user-name',\n name: 'userName',\n type: 'string',\n defaultValue: 'John Doe'\n}\n\n// Number variable for counts, prices, etc.\n{\n id: 'product-count',\n name: 'productCount', \n type: 'number',\n defaultValue: 42\n}\n\n// Boolean variable for toggles, features flags\n{\n id: 'show-header',\n name: 'showHeader',\n type: 'boolean',\n defaultValue: true\n}\n```\n\n## Creating Variables\n\n### Through UI Builder Props\n\n```tsx\nconst initialVariables = [\n {\n id: 'welcome-msg',\n name: 'welcomeMessage',\n type: 'string',\n defaultValue: 'Welcome to our site!'\n },\n {\n id: 'user-age',\n name: 'userAge',\n type: 'number', \n defaultValue: 25\n },\n {\n id: 'is-premium',\n name: 'isPremiumUser',\n type: 'boolean',\n defaultValue: false\n }\n];\n\n {\n // Save variables to your backend\n saveVariables(variables);\n }}\n allowVariableEditing={true} // Allow users to edit variables\n/>\n```\n\n### Through the Variables Panel\n\nUsers can create variables directly in the editor:\n\n1. **Click \"Add Variable\"** in the variables panel\n2. **Choose variable type** (string, number, boolean)\n3. **Set name and default value**\n4. **Variable is immediately available** for binding\n\n## Variable Binding\n\n### Binding to Component Properties\n\nBind variables to any component property in the props panel:\n\n```tsx\n// Component with variable binding\nconst buttonComponent = {\n id: 'welcome-button',\n type: 'Button',\n props: {\n children: { __variableRef: 'welcome-msg' }, // Bound to variable\n disabled: { __variableRef: 'is-premium' }, // Boolean binding\n className: 'px-4 py-2' // Static value\n }\n};\n```\n\n### Variable Reference Format\n\nVariable bindings use a special reference format:\n\n```tsx\n// Variable reference object\n{ __variableRef: 'variable-id' }\n\n// Examples\nprops: {\n title: { __variableRef: 'page-title' }, // String variable\n count: { __variableRef: 'item-count' }, // Number variable\n visible: { __variableRef: 'show-banner' } // Boolean variable\n}\n```\n\n## Default Variable Bindings\n\nComponents can automatically bind to variables when added:\n\n```tsx\nconst UserProfile = {\n component: UserProfile,\n schema: z.object({\n name: z.string().default(''),\n email: z.string().default(''),\n avatar: z.string().optional()\n }),\n from: '@/components/user-profile',\n // Automatically bind these props to variables\n defaultVariableBindings: [\n { \n propName: 'name', \n variableId: 'current-user-name',\n immutable: false // Can be unbound by user\n },\n { \n propName: 'email', \n variableId: 'current-user-email',\n immutable: true // Cannot be unbound\n }\n ]\n};\n```\n\n## Variable Resolution\n\n### Runtime Values\n\nWhen rendering with `LayerRenderer`, override variable values:\n\n```tsx\n// Variables defined in editor\nconst editorVariables = [\n { id: 'user-name', name: 'userName', type: 'string', defaultValue: 'Guest' },\n { id: 'user-age', name: 'userAge', type: 'number', defaultValue: 0 }\n];\n\n// Runtime variable values\nconst runtimeValues = {\n 'user-name': 'Alice Johnson', // Override with real user data\n 'user-age': 28\n};\n\n\n```\n\n### Variable Resolution Process\n\n1. **Editor displays** default values during editing\n2. **Renderer uses** runtime values when provided\n3. **Falls back** to default values if runtime value missing\n4. **Type safety** ensures values match variable types\n\n## Use Cases\n\n### Personalized Content\n\n```tsx\n// User-specific variables\nconst userVariables = [\n { id: 'user-first-name', name: 'firstName', type: 'string', defaultValue: 'User' },\n { id: 'user-last-name', name: 'lastName', type: 'string', defaultValue: '' },\n { id: 'user-points', name: 'loyaltyPoints', type: 'number', defaultValue: 0 }\n];\n\n// Components bound to user data\nconst welcomeSection = {\n type: 'div',\n props: { className: 'welcome-section' },\n children: [\n {\n type: 'span',\n props: {\n children: { __variableRef: 'user-first-name' }\n }\n }\n ]\n};\n```\n\n### Feature Flags\n\n```tsx\n// Boolean variables for feature toggles\nconst featureFlags = [\n { id: 'show-beta-features', name: 'showBetaFeatures', type: 'boolean', defaultValue: false },\n { id: 'enable-notifications', name: 'enableNotifications', type: 'boolean', defaultValue: true }\n];\n\n// Conditionally show components\nconst betaFeature = {\n type: 'div',\n props: {\n className: 'beta-feature',\n style: { \n display: { __variableRef: 'show-beta-features' } ? 'block' : 'none'\n }\n }\n};\n```\n\n### Multi-Tenant Branding\n\n```tsx\n// Brand-specific variables\nconst brandVariables = [\n { id: 'company-name', name: 'companyName', type: 'string', defaultValue: 'Your Company' },\n { id: 'brand-color', name: 'primaryColor', type: 'string', defaultValue: '#3b82f6' },\n { id: 'logo-url', name: 'logoUrl', type: 'string', defaultValue: '/default-logo.png' }\n];\n\n// Components using brand variables\nconst header = {\n type: 'header',\n children: [\n {\n type: 'img',\n props: {\n src: { __variableRef: 'logo-url' },\n alt: { __variableRef: 'company-name' }\n }\n }\n ]\n};\n```\n\n## Variable Management\n\n### Panel Controls\n\n- **Add Variable** - Create new variables\n- **Edit Variable** - Modify name, type, or default value\n- **Delete Variable** - Remove unused variables\n- **Search Variables** - Find variables by name\n\n### Variable Validation\n\n- **Unique names** - Prevent duplicate variable names\n- **Type checking** - Ensure values match declared types\n- **Usage tracking** - Show which components use each variable\n- **Orphan detection** - Identify unused variables\n\n### Variable Panel Configuration\n\n```tsx\n { // Handle variable changes\n console.log('Variables updated:', vars);\n }}\n/>\n```\n\n## Best Practices\n\n### Naming Conventions\n- **Use camelCase** for variable names (`userName`, not `user_name`)\n- **Be descriptive** (`currentUserEmail` vs `email`)\n- **Group related variables** (`user*`, `brand*`, `feature*`)\n\n### Type Selection\n- **Use strings** for text, URLs, IDs, and enum-like values\n- **Use numbers** for counts, measurements, and calculations \n- **Use booleans** for flags, toggles, and conditional display\n\n### Organization\n- **Start with core variables** that many components will use\n- **Group by purpose** (user data, branding, features)\n- **Document variable purpose** in your codebase\n- **Plan for runtime data** structure when designing variables\n\n### Performance\n- **Avoid excessive variables** that aren't actually needed\n- **Use immutable bindings** for system-level data\n- **Cache runtime values** when possible to reduce re-renders" + } + ] + } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/variables.ts b/app/docs/docs-data/docs-page-layers/variables.ts new file mode 100644 index 0000000..e2f4198 --- /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 typed data containers that enable dynamic, data-driven interfaces in UI Builder. They allow you to bind component properties to values that can change at runtime, enabling personalization, theming, and reusable templates." + }, + { + "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": "http://localhost:3000/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 primitive variable types:\n\n```tsx\n// String variable for text content\nconst userNameVar: Variable = {\n id: 'user-name-var',\n name: 'userName',\n type: 'string',\n defaultValue: 'John Doe'\n};\n\n// Number variable for counts, prices, quantities\nconst itemCountVar: Variable = {\n id: 'item-count-var',\n name: 'itemCount', \n type: 'number',\n defaultValue: 42\n};\n\n// Boolean variable for feature flags, toggles\nconst showBannerVar: Variable = {\n id: 'show-banner-var',\n name: 'showBanner',\n type: 'boolean',\n defaultValue: true\n};\n```\n\n## Creating Variables\n\n### Through UIBuilder Props\n\nDefine initial variables when initializing the editor:\n\n```tsx\nimport UIBuilder from '@/components/ui/ui-builder';\nimport { Variable } from '@/components/ui/ui-builder/types';\n\nconst initialVariables: Variable[] = [\n {\n id: 'welcome-msg',\n name: 'welcomeMessage',\n type: 'string',\n defaultValue: 'Welcome to our site!'\n },\n {\n id: 'user-age',\n name: 'userAge',\n type: 'number', \n defaultValue: 25\n },\n {\n id: 'is-premium',\n name: 'isPremiumUser',\n type: 'boolean',\n defaultValue: false\n }\n];\n\nfunction App() {\n return (\n {\n // Save variables to your backend\n console.log('Variables updated:', variables);\n }}\n allowVariableEditing={true} // Allow users to edit variables\n componentRegistry={myComponentRegistry}\n />\n );\n}\n```\n\n### Through the Variables Panel\n\nUsers can create and manage variables directly in the editor:\n\n1. **Navigate to the \"Data\" tab** in the left panel\n2. **Click \"Add Variable\"** to create a new variable\n3. **Choose variable type** (string, number, boolean)\n4. **Set name and default value**\n5. **Variable is immediately available** for binding in the props panel\n\n## Variable Binding\n\n### Binding Through Props Panel\n\nVariables are bound to component properties through the UI:\n\n1. **Select a component** in the editor\n2. **Open the props panel** (right panel)\n3. **Click the link icon** next to any property field\n4. **Choose a variable** from the dropdown menu\n5. **The property is now bound** to the variable\n\n### Binding Structure\n\nWhen bound, component props store a variable reference:\n\n```tsx\n// Internal structure when a prop is bound to a variable\nconst buttonLayer: ComponentLayer = {\n id: 'my-button',\n type: 'Button',\n props: {\n children: { __variableRef: 'welcome-msg' }, // Bound to variable\n disabled: { __variableRef: 'is-loading' }, // Bound to variable\n variant: 'default' // Static value\n },\n children: []\n};\n```\n\n### Immutable Bindings\n\nUse `defaultVariableBindings` to automatically bind variables when components are added:\n\n```tsx\nconst componentRegistry = {\n UserProfile: {\n component: UserProfile,\n schema: z.object({\n userId: z.string(),\n displayName: z.string(),\n }),\n from: '@/components/ui/user-profile',\n // Automatically bind user data when component is added\n defaultVariableBindings: [\n { \n propName: 'userId', \n variableId: 'current-user-id', \n immutable: true // Cannot be unbound in UI\n },\n { \n propName: 'displayName', \n variableId: 'current-user-name', \n immutable: false // Can be changed by users\n }\n ]\n }\n};\n```\n\n## Runtime Variable Resolution\n\n### In LayerRenderer\n\nVariables are resolved when rendering pages:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\nimport { Variable } from '@/components/ui/ui-builder/types';\n\n// Define variables\nconst variables: Variable[] = [\n {\n id: 'user-name',\n name: 'userName',\n type: 'string',\n defaultValue: 'Anonymous'\n },\n {\n id: 'user-age',\n name: 'userAge',\n type: 'number',\n defaultValue: 0\n }\n];\n\n// Override variable values at runtime\nconst variableValues = {\n 'user-name': 'Jane Smith',\n 'user-age': 30\n};\n\nfunction MyPage() {\n return (\n \n );\n}\n```\n\n### Variable Resolution Process\n\n1. **Component props are scanned** for variable references\n2. **Variable references are resolved** using provided `variableValues` or defaults\n3. **Resolved values are passed** to React components\n4. **Components render** with dynamic data\n\n## Managing Variables\n\n### Read-Only Variables\n\nControl whether users can edit variables in the UI:\n\n```tsx\n\n```\n\n### Variable Change Handling\n\nRespond to variable changes in the editor:\n\n```tsx\nfunction App() {\n const handleVariablesChange = (variables: Variable[]) => {\n // Persist to backend\n fetch('/api/variables', {\n method: 'POST',\n body: JSON.stringify(variables)\n });\n };\n\n return (\n \n );\n}\n```\n\n## Code Generation\n\nVariables are included in generated React code:\n\n```tsx\n// Generated component with variables\ninterface PageProps {\n variables: {\n userName: string;\n userAge: number;\n showWelcome: boolean;\n };\n}\n\nconst Page = ({ variables }: PageProps) => {\n return (\n
\n \n Age: {variables.userAge}\n
\n );\n};\n```\n\n## Use Cases\n\n### Personalization\n\n```tsx\n// Variables for user-specific content\nconst userVariables = [\n { id: 'user-name', name: 'userName', type: 'string', defaultValue: 'User' },\n { id: 'user-avatar', name: 'userAvatar', type: 'string', defaultValue: '/default-avatar.png' },\n { id: 'is-premium', name: 'isPremiumUser', type: 'boolean', defaultValue: false }\n];\n```\n\n### Feature Flags\n\n```tsx\n// Variables for conditional features\nconst featureFlags = [\n { id: 'show-beta-feature', name: 'showBetaFeature', type: 'boolean', defaultValue: false },\n { id: 'enable-dark-mode', name: 'enableDarkMode', type: 'boolean', defaultValue: true }\n];\n```\n\n### Multi-tenant Branding\n\n```tsx\n// Variables for client-specific branding\nconst brandingVariables = [\n { id: 'company-name', name: 'companyName', type: 'string', defaultValue: 'Acme Corp' },\n { id: 'primary-color', name: 'primaryColor', type: 'string', defaultValue: '#3b82f6' },\n { id: 'logo-url', name: 'logoUrl', type: 'string', defaultValue: '/default-logo.png' }\n];\n```\n\n### Dynamic Content\n\n```tsx\n// Variables for content management\nconst contentVariables = [\n { id: 'page-title', name: 'pageTitle', type: 'string', defaultValue: 'Welcome' },\n { id: 'product-count', name: 'productCount', type: 'number', defaultValue: 0 },\n { id: 'show-special-offer', name: 'showSpecialOffer', type: 'boolean', defaultValue: false }\n];\n```\n\n## Best Practices\n\n- **Use descriptive names** for variables (e.g., `userName` not `u`)\n- **Choose appropriate types** for your data\n- **Set meaningful default values** for better preview experience\n- **Use immutable bindings** for system-critical data\n- **Group related variables** with consistent naming patterns\n- **Document variable purposes** in your component registry" + } + ] + } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/platform/app-sidebar.tsx b/app/platform/app-sidebar.tsx index f55b1c9..fc4450f 100644 --- a/app/platform/app-sidebar.tsx +++ b/app/platform/app-sidebar.tsx @@ -14,7 +14,11 @@ import { } from "@/components/ui/sidebar" import { MENU_DATA } from "@/app/docs/docs-data/data" -export function AppSidebar({ ...props }: React.ComponentProps) { +interface AppSidebarProps extends React.ComponentProps { + currentPath?: string; +} + +export function AppSidebar({ currentPath, ...props }: AppSidebarProps) { return ( @@ -23,16 +27,19 @@ export function AppSidebar({ ...props }: React.ComponentProps) { {section.title} - {section.items?.map((item) => ( - - - {item.title} - - - ))} + {section.items?.map((item) => { + const isActive = currentPath === item.url; + return ( + + + {item.title} + + + ); + })} 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 From dd4fb16ed62b7d9f46e61e55c46f7c7c472f36f3 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Wed, 23 Jul 2025 16:17:10 -0400 Subject: [PATCH 3/5] feat: add pages and docs pages --- README.md | 1 - app/(edit)/docs/[slug]/edit/layout.tsx | 3 + app/{ => (edit)}/docs/[slug]/edit/page.tsx | 2 +- app/docs/[slug]/page.tsx | 104 ++-- app/docs/docs-data/data.ts | 79 +-- .../docs-page-layers/appearance-panel.ts | 83 ---- .../docs-page-layers/canvas-editor.ts | 83 ---- .../docs-page-layers/component-registry.ts | 18 +- .../docs-page-layers/custom-components.ts | 6 +- .../docs-page-layers/data-binding.ts | 36 -- .../docs-page-layers/editor-panel-config.ts | 83 ---- .../docs-page-layers/field-overrides.ts | 60 ++- .../docs-page-layers/immutable-pages.ts | 83 ---- .../docs-page-layers/introduction.ts | 36 +- .../docs-page-layers/layer-structure.ts | 48 +- .../docs-page-layers/page-theming.ts | 36 -- .../docs-data/docs-page-layers/pages-panel.ts | 83 ---- .../docs-page-layers/panel-configuration.ts | 91 ++-- .../docs-data/docs-page-layers/persistence.ts | 69 ++- .../props-panel-customization.ts | 83 ---- .../docs-data/docs-page-layers/props-panel.ts | 83 ---- .../docs-data/docs-page-layers/quick-start.ts | 92 +++- .../docs-page-layers/read-only-mode.ts | 29 +- .../docs-page-layers/rendering-pages.ts | 6 +- .../docs-page-layers/variable-binding.ts | 51 +- .../docs-page-layers/variables-panel.ts | 83 ---- .../docs-data/docs-page-layers/variables.ts | 6 +- app/docs/layout.tsx | 38 +- app/docs/page.tsx | 59 +++ app/examples/editor/panel-config/page.tsx | 14 + app/examples/editor/read-only-mode/page.tsx | 14 + app/layout.tsx | 5 + app/page.tsx | 57 ++- app/platform/app-sidebar.tsx | 30 +- app/platform/builder-drag-drop-test.tsx | 1 + .../builder-with-immutable-bindings.tsx | 1 + app/platform/builder-with-pages.tsx | 1 + app/platform/doc-breadcrumbs.tsx | 46 ++ app/platform/panel-config-demo.tsx | 469 ++++++++++++++++++ app/platform/read-only-demo.tsx | 325 ++++++++++++ .../ui/ui-builder/components/codeblock.tsx | 6 +- .../primitive-component-definitions.ts | 69 +++ public/android-chrome-192x192.png | Bin 0 -> 10947 bytes public/android-chrome-512x512.png | Bin 0 -> 35399 bytes public/apple-touch-icon.png | Bin 0 -> 9834 bytes public/favicon-16x16.png | Bin 0 -> 782 bytes public/favicon-32x32.png | Bin 0 -> 1708 bytes public/favicon.ico | Bin 15086 -> 15406 bytes public/logo.svg | 1 + public/site.webmanifest | 1 + 50 files changed, 1618 insertions(+), 956 deletions(-) create mode 100644 app/(edit)/docs/[slug]/edit/layout.tsx rename app/{ => (edit)}/docs/[slug]/edit/page.tsx (85%) delete mode 100644 app/docs/docs-data/docs-page-layers/appearance-panel.ts delete mode 100644 app/docs/docs-data/docs-page-layers/canvas-editor.ts delete mode 100644 app/docs/docs-data/docs-page-layers/data-binding.ts delete mode 100644 app/docs/docs-data/docs-page-layers/editor-panel-config.ts delete mode 100644 app/docs/docs-data/docs-page-layers/immutable-pages.ts delete mode 100644 app/docs/docs-data/docs-page-layers/page-theming.ts delete mode 100644 app/docs/docs-data/docs-page-layers/pages-panel.ts delete mode 100644 app/docs/docs-data/docs-page-layers/props-panel-customization.ts delete mode 100644 app/docs/docs-data/docs-page-layers/props-panel.ts delete mode 100644 app/docs/docs-data/docs-page-layers/variables-panel.ts create mode 100644 app/docs/page.tsx create mode 100644 app/examples/editor/panel-config/page.tsx create mode 100644 app/examples/editor/read-only-mode/page.tsx create mode 100644 app/platform/doc-breadcrumbs.tsx create mode 100644 app/platform/panel-config-demo.tsx create mode 100644 app/platform/read-only-demo.tsx create mode 100644 public/android-chrome-192x192.png create mode 100644 public/android-chrome-512x512.png create mode 100644 public/apple-touch-icon.png create mode 100644 public/favicon-16x16.png create mode 100644 public/favicon-32x32.png create mode 100644 public/logo.svg create mode 100644 public/site.webmanifest diff --git a/README.md b/README.md index 5264379..16b8cbb 100644 --- a/README.md +++ b/README.md @@ -632,7 +632,6 @@ npm run test ## Roadmap -- [ ] Documentation site for UI Builder with more hands-on examples - [ ] Add variable binding to layer children and not just props - [ ] Update to React 19 - [ ] Update to latest Shadcn/ui + Tailwind CSS v4 diff --git a/app/(edit)/docs/[slug]/edit/layout.tsx b/app/(edit)/docs/[slug]/edit/layout.tsx new file mode 100644 index 0000000..8b91025 --- /dev/null +++ b/app/(edit)/docs/[slug]/edit/layout.tsx @@ -0,0 +1,3 @@ +export default function EditLayout({ children }: { children: React.ReactNode }) { + return
{children}
; +} \ No newline at end of file diff --git a/app/docs/[slug]/edit/page.tsx b/app/(edit)/docs/[slug]/edit/page.tsx similarity index 85% rename from app/docs/[slug]/edit/page.tsx rename to app/(edit)/docs/[slug]/edit/page.tsx index 8f038c1..5d77862 100644 --- a/app/docs/[slug]/edit/page.tsx +++ b/app/(edit)/docs/[slug]/edit/page.tsx @@ -1,7 +1,7 @@ import { notFound } from "next/navigation"; import { DocEditor } from "@/app/platform/doc-editor"; -import { getDocPageForSlug } from "../../docs-data/data"; +import { getDocPageForSlug } from "../../../../docs/docs-data/data"; export default async function DocEditPage({ params, diff --git a/app/docs/[slug]/page.tsx b/app/docs/[slug]/page.tsx index 746494e..c6025e1 100644 --- a/app/docs/[slug]/page.tsx +++ b/app/docs/[slug]/page.tsx @@ -1,59 +1,57 @@ -import { AppSidebar } from "@/app/platform/app-sidebar"; -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb"; -import { - SidebarInset, - SidebarProvider, - SidebarTrigger, -} from "@/components/ui/sidebar"; import { Suspense } from "react"; import { DocRenderer } from "@/app/platform/doc-renderer"; -import { getBreadcrumbsFromUrl, getDocPageForSlug } from "@/app/docs/docs-data/data"; -import { ThemeToggle } from "@/app/platform/theme-toggle"; -import { notFound } from "next/navigation"; +import { + getDocPageForSlug, +} from "@/app/docs/docs-data/data"; +import { notFound } from "next/navigation"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Metadata } from "next"; + +export async function generateMetadata( + { params }: { + params: Promise<{slug: string}>, + } +): Promise { + const slug = (await params).slug + + console.log({slug}); + + const page = getDocPageForSlug(slug); + + return { + title: page?.name ? `${page.name} - UI Builder` : "Documentation - UI Builder", + description: page?.props["data-group"] ? `Learn about ${page.props["data-group"]} features of the UI Builder component.` : "Documentation - UI Builder", + } +} export default async function DocPage({ - params, - }: { - params: Promise<{ slug: string }>; - }){ - const { slug } = await params; - const page = getDocPageForSlug(slug); - if (!page) { - notFound(); - } - const breadcrumbs = getBreadcrumbsFromUrl(slug); - const currentPath = `/docs/${slug}`; - console.log({slug}); - return - - -
- - - - - - {breadcrumbs.category.title} - - - - - {breadcrumbs.page.title} - - - - -
- Loading...}> + params, +}: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await params; + const page = getDocPageForSlug(slug); + if (!page) { + notFound(); + } + + return ( +
+ }> - - -} \ No newline at end of file +
+ ); +} + +function DocSkeleton() { + return ( +
+
+ + + +
+
+ ); +} diff --git a/app/docs/docs-data/data.ts b/app/docs/docs-data/data.ts index ad60b11..4aa6a3a 100644 --- a/app/docs/docs-data/data.ts +++ b/app/docs/docs-data/data.ts @@ -3,23 +3,13 @@ import { QUICK_START_LAYER } from "@/app/docs/docs-data/docs-page-layers/quick-s import { COMPONENT_REGISTRY_LAYER } from "@/app/docs/docs-data/docs-page-layers/component-registry"; import { FIELD_OVERRIDES_LAYER } from "@/app/docs/docs-data/docs-page-layers/field-overrides"; import { CUSTOM_COMPONENTS_LAYER } from "@/app/docs/docs-data/docs-page-layers/custom-components"; -import { CANVAS_EDITOR_LAYER } from "@/app/docs/docs-data/docs-page-layers/canvas-editor"; -import { PAGES_PANEL_LAYER } from "@/app/docs/docs-data/docs-page-layers/pages-panel"; -import { VARIABLES_PANEL_LAYER } from "@/app/docs/docs-data/docs-page-layers/variables-panel"; -import { PROPS_PANEL_LAYER } from "@/app/docs/docs-data/docs-page-layers/props-panel"; -import { APPEARANCE_PANEL_LAYER } from "@/app/docs/docs-data/docs-page-layers/appearance-panel"; -import { IMMUTABLE_PAGES_LAYER } from "@/app/docs/docs-data/docs-page-layers/immutable-pages"; import { PANEL_CONFIGURATION_LAYER } from "@/app/docs/docs-data/docs-page-layers/panel-configuration"; import { VARIABLES_LAYER } from "@/app/docs/docs-data/docs-page-layers/variables"; import { VARIABLE_BINDING_LAYER } from "@/app/docs/docs-data/docs-page-layers/variable-binding"; import { READ_ONLY_MODE_LAYER } from "@/app/docs/docs-data/docs-page-layers/read-only-mode"; -import { DATA_BINDING_LAYER } from "@/app/docs/docs-data/docs-page-layers/data-binding"; import { LAYER_STRUCTURE_LAYER } from "@/app/docs/docs-data/docs-page-layers/layer-structure"; import { PERSISTENCE_LAYER } from "@/app/docs/docs-data/docs-page-layers/persistence"; import { RENDERING_PAGES_LAYER } from "@/app/docs/docs-data/docs-page-layers/rendering-pages"; -import { PAGE_THEMING_LAYER } from "@/app/docs/docs-data/docs-page-layers/page-theming"; -import { EDITOR_PANEL_CONFIG_LAYER } from "@/app/docs/docs-data/docs-page-layers/editor-panel-config"; -import { PROPS_PANEL_CUSTOMIZATION_LAYER } from "@/app/docs/docs-data/docs-page-layers/props-panel-customization"; export const DOCS_PAGES = [ // Core @@ -30,23 +20,12 @@ export const DOCS_PAGES = [ COMPONENT_REGISTRY_LAYER, CUSTOM_COMPONENTS_LAYER, FIELD_OVERRIDES_LAYER, - - // Editor Features - CANVAS_EDITOR_LAYER, - PAGES_PANEL_LAYER, - IMMUTABLE_PAGES_LAYER, - APPEARANCE_PANEL_LAYER, - PROPS_PANEL_LAYER, - VARIABLES_PANEL_LAYER, PANEL_CONFIGURATION_LAYER, - EDITOR_PANEL_CONFIG_LAYER, - PROPS_PANEL_CUSTOMIZATION_LAYER, // Data & Variables VARIABLES_LAYER, VARIABLE_BINDING_LAYER, READ_ONLY_MODE_LAYER, - DATA_BINDING_LAYER, // Layout & Persistence LAYER_STRUCTURE_LAYER, @@ -54,7 +33,6 @@ export const DOCS_PAGES = [ // Rendering RENDERING_PAGES_LAYER, - PAGE_THEMING_LAYER, ] as const; @@ -89,58 +67,21 @@ export const MENU_DATA: DocPageNavItem[] = [ title: "Component System", items: [ { - title: "Getting Started with Components", + title: "Components Intro", url: "/docs/component-registry", }, { - title: "Creating Custom Components", + title: "Custom Components", url: "/docs/custom-components", }, { - title: "Advanced Component Configuration", + title: "Advanced Configuration", url: "/docs/field-overrides", - } - ], - }, - { - title: "Editor Features", - items: [ - { - title: "Canvas Editor", - url: "/docs/canvas-editor", - }, - { - title: "Pages Panel", - url: "/docs/pages-panel", - }, - { - title: "Immutable Pages", - url: "/docs/immutable-pages", - }, - { - title: "Appearance Panel", - url: "/docs/appearance-panel", - }, - { - title: "Props Panel", - url: "/docs/props-panel", - }, - { - title: "Variables Panel", - url: "/docs/variables-panel", }, { title: "Panel Configuration", url: "/docs/panel-configuration", - }, - { - title: "Editor Panel Config", - url: "/docs/editor-panel-config", - }, - { - title: "Props Panel Customization", - url: "/docs/props-panel-customization", - }, + } ], }, { @@ -155,13 +96,9 @@ export const MENU_DATA: DocPageNavItem[] = [ url: "/docs/variable-binding", }, { - title: "Read Only Mode", + title: "Editing Restrictions", url: "/docs/read-only-mode", }, - { - title: "Data Binding", - url: "/docs/data-binding", - }, ], }, { @@ -184,10 +121,6 @@ export const MENU_DATA: DocPageNavItem[] = [ title: "Rendering Pages", url: "/docs/rendering-pages", }, - { - title: "Page Theming", - url: "/docs/page-theming", - }, ], } ] as const; @@ -228,7 +161,7 @@ export function getBreadcrumbsFromUrl(url: string) { url: "#" }, page: { - title: "Page", + title: "Home", url: url } }; diff --git a/app/docs/docs-data/docs-page-layers/appearance-panel.ts b/app/docs/docs-data/docs-page-layers/appearance-panel.ts deleted file mode 100644 index 2648c6f..0000000 --- a/app/docs/docs-data/docs-page-layers/appearance-panel.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { ComponentLayer } from "@/components/ui/ui-builder/types"; - -export const APPEARANCE_PANEL_LAYER = { - "id": "appearance-panel", - "type": "div", - "name": "Appearance Panel", - "props": { - "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", - "data-group": "editor-features" - }, - "children": [ - { - "type": "span", - "children": "Appearance Panel", - "id": "appearance-panel-title", - "name": "Text", - "props": { - "className": "text-4xl" - } - }, - { - "id": "appearance-panel-intro", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "The appearance panel provides visual tools for page configuration and Tailwind CSS theme management. Configure page settings and global styling through an intuitive interface that integrates with your design system." - }, - { - "id": "appearance-panel-demo", - "type": "div", - "name": "div", - "props": {}, - "children": [ - { - "id": "appearance-panel-badge", - "type": "Badge", - "name": "Badge", - "props": { - "variant": "default", - "className": "rounded rounded-b-none" - }, - "children": [ - { - "id": "appearance-panel-badge-text", - "type": "span", - "name": "span", - "props": {}, - "children": "Theme & Config" - } - ] - }, - { - "id": "appearance-panel-demo-frame", - "type": "div", - "name": "div", - "props": { - "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" - }, - "children": [ - { - "id": "appearance-panel-iframe", - "type": "iframe", - "name": "iframe", - "props": { - "src": "http://localhost:3000/examples/editor", - "title": "Appearance Panel Demo", - "className": "aspect-square md:aspect-video w-full" - }, - "children": [] - } - ] - } - ] - }, - { - "id": "appearance-panel-content", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "## Panel Components\n\nThe appearance panel consists of two main components:\n\n### ConfigPanel\nManages page-level configuration:\n- **Page Name** - Edit the current page's display name\n- **Page Properties** - Configure page-specific settings\n- **Page Actions** - Duplicate or delete pages\n\n### TailwindThemePanel\nProvides Tailwind CSS theme management:\n- **CSS Variables** - Edit theme CSS custom properties\n- **Color Palette** - Manage theme colors\n- **Theme Toggle** - Switch between light/dark modes\n- **Live Preview** - See theme changes instantly\n\n## Page Configuration\n\nThe ConfigPanel allows you to edit page properties:\n\n```tsx\n// Current page configuration\nconst currentPage = {\n id: 'homepage',\n name: 'Home Page', // Editable in ConfigPanel\n type: 'div',\n props: {\n className: 'min-h-screen bg-background'\n },\n children: []\n};\n\n// ConfigPanel provides:\n// - Page name editing\n// - Duplicate page button\n// - Delete page button (if multiple pages exist)\n```\n\n## Tailwind Theme Management\n\nThe TailwindThemePanel integrates with shadcn/ui's CSS variable system:\n\n### CSS Variables Structure\n\n```css\n/* Light mode variables */\n:root {\n --background: 0 0% 100%;\n --foreground: 222.2 84% 4.9%;\n --primary: 221.2 83.2% 53.3%;\n --primary-foreground: 210 40% 98%;\n --secondary: 210 40% 96%;\n --secondary-foreground: 222.2 84% 4.9%;\n /* ... more variables */\n}\n\n/* Dark mode variables */\n.dark {\n --background: 222.2 84% 4.9%;\n --foreground: 210 40% 98%;\n --primary: 217.2 91.2% 59.8%;\n --primary-foreground: 222.2 84% 4.9%;\n /* ... more variables */\n}\n```\n\n### Theme Editing\n\nThe TailwindThemePanel allows real-time editing of theme variables:\n\n- **Color Pickers** - Visual color selection for theme variables\n- **HSL Values** - Direct HSL value editing\n- **Live Preview** - Changes apply instantly to the editor\n- **Reset Options** - Restore default theme values\n\n## Customizing the Appearance Panel\n\nYou can customize the appearance panel through `panelConfig`:\n\n```tsx\nimport { ConfigPanel } from '@/components/ui/ui-builder/internal/config-panel';\nimport { TailwindThemePanel } from '@/components/ui/ui-builder/internal/tailwind-theme-panel';\n\n// Custom appearance panel content\nconst customAppearancePanel = (\n
\n \n \n {/* Add your custom components */}\n \n
\n);\n\n },\n appearance: { \n title: \"Theme\", \n content: customAppearancePanel \n },\n data: { title: \"Data\", content: }\n }\n }}\n/>\n```\n\n## Theme Integration with Components\n\nComponents automatically inherit theme variables through Tailwind CSS classes:\n\n```tsx\n// Component using theme colors\nconst ThemedButton = {\n component: Button,\n schema: z.object({\n variant: z.enum(['default', 'destructive', 'outline']).default('default'),\n children: z.any().optional()\n }),\n from: '@/components/ui/button'\n};\n\n// Button component CSS (simplified)\n.btn-default {\n background-color: hsl(var(--primary));\n color: hsl(var(--primary-foreground));\n}\n\n.btn-destructive {\n background-color: hsl(var(--destructive));\n color: hsl(var(--destructive-foreground));\n}\n```\n\n## Working with CSS Variables\n\n### Adding Custom Variables\n\nExtend the theme system with custom CSS variables:\n\n```css\n/* In your global CSS */\n:root {\n /* Existing shadcn/ui variables */\n --background: 0 0% 100%;\n --foreground: 222.2 84% 4.9%;\n \n /* Your custom variables */\n --brand-primary: 220 90% 56%;\n --brand-secondary: 340 82% 52%;\n --accent-gradient: linear-gradient(135deg, hsl(var(--brand-primary)), hsl(var(--brand-secondary)));\n}\n\n.dark {\n /* Dark mode overrides */\n --brand-primary: 220 90% 66%;\n --brand-secondary: 340 82% 62%;\n}\n```\n\n### Using Variables in Components\n\n```tsx\n// Component that uses custom theme variables\nconst BrandCard = {\n component: ({ children, className, ...props }) => (\n
\n {children}\n
\n ),\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional()\n }),\n from: '@/components/brand-card'\n};\n```\n\n## Responsive Theming\n\nTailwind CSS and the theme panel support responsive design:\n\n```tsx\n// Responsive component using theme variables\nconst ResponsiveHero = {\n type: 'div',\n props: {\n className: [\n 'bg-background text-foreground',\n 'p-4 md:p-8 lg:p-12', // Responsive padding\n 'min-h-screen flex items-center justify-center'\n ].join(' ')\n },\n children: [\n {\n type: 'div',\n props: {\n className: [\n 'text-center max-w-2xl mx-auto',\n 'space-y-4 md:space-y-6 lg:space-y-8'\n ].join(' ')\n },\n children: [\n // Hero content\n ]\n }\n ]\n};\n```\n\n## Theme Persistence\n\nTheme changes are automatically persisted:\n\n- **localStorage** - Theme preferences saved locally\n- **CSS Variables** - Applied immediately to the document\n- **Component Updates** - All components reflect theme changes instantly\n\n## Best Practices\n\n### Theme Design\n- **Use HSL values** for better color manipulation\n- **Maintain contrast ratios** for accessibility\n- **Test both light and dark modes** thoroughly\n- **Keep semantic naming** (primary, secondary, destructive)\n\n### Component Integration\n- **Use theme variables** instead of hardcoded colors\n- **Follow shadcn/ui patterns** for consistency\n- **Test with different themes** to ensure compatibility\n- **Provide fallbacks** for custom variables\n\n### Performance\n- **CSS variables are fast** - they update instantly\n- **Avoid inline styles** when possible\n- **Use Tailwind classes** for optimal performance\n- **Minimize theme variable count** to reduce complexity\n\n## Integration with Design Systems\n\nThe appearance panel works well with existing design systems:\n\n```tsx\n// Design system integration\nconst designSystemTheme = {\n colors: {\n // Map design system colors to CSS variables\n primary: 'hsl(var(--brand-blue))',\n secondary: 'hsl(var(--brand-purple))',\n accent: 'hsl(var(--brand-green))',\n neutral: 'hsl(var(--neutral-500))'\n },\n spacing: {\n // Use consistent spacing scale\n xs: '0.25rem',\n sm: '0.5rem',\n md: '1rem',\n lg: '1.5rem',\n xl: '2rem'\n }\n};\n```" - } - ] - } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/canvas-editor.ts b/app/docs/docs-data/docs-page-layers/canvas-editor.ts deleted file mode 100644 index 20d9a01..0000000 --- a/app/docs/docs-data/docs-page-layers/canvas-editor.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { ComponentLayer } from "@/components/ui/ui-builder/types"; - -export const CANVAS_EDITOR_LAYER = { - "id": "canvas-editor", - "type": "div", - "name": "Canvas Editor", - "props": { - "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", - "data-group": "editor-features" - }, - "children": [ - { - "type": "span", - "children": "Canvas Editor", - "id": "canvas-editor-title", - "name": "Text", - "props": { - "className": "text-4xl" - } - }, - { - "id": "canvas-editor-intro", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "The canvas editor is the main workspace where users visually compose their layouts. It provides intuitive drag-and-drop functionality with live preview capabilities powered by React DnD." - }, - { - "id": "canvas-editor-demo", - "type": "div", - "name": "div", - "props": {}, - "children": [ - { - "id": "canvas-editor-badge", - "type": "Badge", - "name": "Badge", - "props": { - "variant": "default", - "className": "rounded rounded-b-none" - }, - "children": [ - { - "id": "canvas-editor-badge-text", - "type": "span", - "name": "span", - "props": {}, - "children": "Interactive Demo" - } - ] - }, - { - "id": "canvas-editor-demo-frame", - "type": "div", - "name": "div", - "props": { - "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" - }, - "children": [ - { - "id": "canvas-editor-iframe", - "type": "iframe", - "name": "iframe", - "props": { - "src": "http://localhost:3000/examples/editor", - "title": "Canvas Editor Demo", - "className": "aspect-square md:aspect-video w-full" - }, - "children": [] - } - ] - } - ] - }, - { - "id": "canvas-editor-content", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "## Key Features\n\n### Visual Editing\n- **Drag & Drop** - Intuitive component placement from the component library\n- **Direct Selection** - Click components on the canvas to select and edit\n- **Visual Feedback** - Hover states and selection indicators\n- **Live Preview** - See changes instantly as you edit properties\n\n### Layout Management\n- **Nested Components** - Build complex layouts with nested structures\n- **Auto-Frame Wrapper** - Intelligent container resizing for responsive layouts\n- **Drop Zones** - Visual indicators showing where components can be placed\n- **Layer Hierarchy** - Maintain proper component nesting and relationships\n\n### Interaction\n- **Multi-Component Support** - Work with multiple components simultaneously\n- **Undo/Redo** - Full history management via Zustand temporal\n- **Keyboard Shortcuts** - Power user productivity features\n- **Contextual Menus** - Right-click for component-specific actions\n\n## Component Library Integration\n\nThe canvas integrates with your `componentRegistry` to provide available components:\n\n```tsx\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, // Basic HTML elements\n ...complexComponentDefinitions, // shadcn/ui components\n // Your custom components\n MyCustomCard: {\n component: MyCustomCard,\n schema: z.object({\n title: z.string().default(\"Card Title\"),\n description: z.string().optional(),\n }),\n from: \"@/components/my-custom-card\",\n defaultChildren: \"Card content\"\n }\n};\n\n\n```\n\n## Responsive Design\n\nThe canvas supports responsive editing through the appearance panel:\n\n- **Mobile-First** approach with breakpoint controls\n- **Auto-Frame** wrapper ensures components resize properly\n- **Tailwind CSS** classes for responsive styling\n- **Live Preview** of different screen sizes\n\n## State Management\n\nThe canvas uses Zustand stores for state management:\n\n```tsx\n// Access current canvas state\nconst selectedLayerId = useLayerStore(state => state.selectedLayerId);\nconst pages = useLayerStore(state => state.pages);\nconst variables = useLayerStore(state => state.variables);\n\n// Editor state\nconst showLeftPanel = useEditorStore(state => state.showLeftPanel);\nconst componentRegistry = useEditorStore(state => state.registry);\n```\n\n## Variable Binding on Canvas\n\nComponents can display bound variable values directly on the canvas:\n\n```tsx\n// Variable binding example\nconst welcomeMessage = {\n id: \"welcome-var\",\n name: \"welcomeMessage\", \n type: \"string\",\n defaultValue: \"Hello, World!\"\n};\n\n// Component with variable binding\nconst buttonComponent = {\n id: \"welcome-btn\",\n type: \"Button\",\n props: {\n children: { __variableRef: \"welcome-var\" } // Bound to variable\n }\n};\n```\n\nThe canvas will render the actual variable value, updating in real-time as variables change.\n\n## Accessibility\n\n- **Keyboard Navigation** - Full keyboard support for component selection\n- **Screen Reader Support** - Proper ARIA labels and roles\n- **Focus Management** - Logical tab order and focus indicators\n- **High Contrast** - Works with system theme preferences" - } - ] - } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/component-registry.ts b/app/docs/docs-data/docs-page-layers/component-registry.ts index 98ec93a..8ab5966 100644 --- a/app/docs/docs-data/docs-page-layers/component-registry.ts +++ b/app/docs/docs-data/docs-page-layers/component-registry.ts @@ -3,7 +3,7 @@ import { ComponentLayer } from "@/components/ui/ui-builder/types"; export const COMPONENT_REGISTRY_LAYER = { "id": "component-registry", "type": "div", - "name": "Getting Started with Components", + "name": "Components Intro", "props": { "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", "data-group": "component-system" @@ -11,7 +11,7 @@ export const COMPONENT_REGISTRY_LAYER = { "children": [ { "type": "span", - "children": "Getting Started with Components", + "children": "Components Introduction", "id": "component-registry-title", "name": "Text", "props": { @@ -23,14 +23,14 @@ export const COMPONENT_REGISTRY_LAYER = { "type": "Markdown", "name": "Markdown", "props": {}, - "children": "The component registry is the heart of UI Builder. It defines which React components are available in the visual editor and how they should be configured. You provide a `ComponentRegistry` object to the `UIBuilder` component via the `componentRegistry` prop." + "children": "The component registry is the heart of UI Builder. It defines which React components are available in the visual editor and how they should be configured. Understanding the registry is essential for using UI Builder effectively with your own components." }, { "id": "component-registry-content", "type": "Markdown", "name": "Markdown", "props": {}, - "children": "## What is the Component Registry?\n\nThe component registry is a JavaScript object that maps component type names to their definitions. Each definition tells UI Builder:\n\n- How to render the component\n- What properties it accepts \n- How to generate forms for editing those properties\n- What import path to use when generating code\n\n```tsx\nimport { ComponentRegistry } from '@/components/ui/ui-builder/types';\n\nconst myComponentRegistry: ComponentRegistry = {\n // Component type name → Component definition\n 'Button': { /* Button definition */ },\n 'span': { /* span definition */ },\n // ... more components\n};\n```\n\n## Using Pre-built Component Definitions\n\nUI Builder includes comprehensive component definitions for common HTML elements and shadcn/ui components:\n\n```tsx\nimport { primitiveComponentDefinitions } from '@/lib/ui-builder/registry/primitive-component-definitions';\nimport { complexComponentDefinitions } from '@/lib/ui-builder/registry/complex-component-definitions';\nimport UIBuilder from '@/components/ui/ui-builder';\n\n// Start with pre-built components\nconst componentRegistry: ComponentRegistry = {\n ...primitiveComponentDefinitions, // div, span, img, a, iframe\n ...complexComponentDefinitions, // Button, Badge, Card, Icon, etc.\n};\n\nfunction MyApp() {\n return (\n \n );\n}\n```\n\n### Available Pre-built Components\n\n**Primitive Components** (no React component needed):\n- **Layout**: `div`, `span` \n- **Media**: `img`, `iframe`\n- **Navigation**: `a` (links)\n\n**Complex Components** (with React components):\n- **Layout**: `Flexbox`, `Grid` \n- **Content**: `Markdown`, `CodePanel`\n- **UI Elements**: `Button`, `Badge`\n- **Advanced**: `Card`, `Icon`, `Accordion`\n\n## Adding a Simple Custom Component\n\nHere's how to add your own component to the registry:\n\n```tsx\nimport { z } from 'zod';\nimport { Alert } from '@/components/ui/alert';\nimport { commonFieldOverrides } from '@/lib/ui-builder/registry/form-field-overrides';\n\nconst myComponentRegistry: ComponentRegistry = {\n // Include pre-built components\n ...primitiveComponentDefinitions,\n ...complexComponentDefinitions,\n \n // Add your custom component\n Alert: {\n component: Alert,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n variant: z.enum(['default', 'destructive']).default('default'),\n }),\n from: '@/components/ui/alert',\n fieldOverrides: commonFieldOverrides()\n }\n};\n```\n\n## Basic Component Definition Structure\n\nEvery component definition has these key properties:\n\n### Required Properties\n- **`schema`**: Zod schema defining the component's props and their types\n- **`component`**: The React component (for complex components)\n- **`from`**: Import path for code generation (for complex components)\n\n### Optional Properties\n- **`fieldOverrides`**: Customize how props are edited in the properties panel\n- **`defaultChildren`**: Default child components when added to canvas\n- **`defaultVariableBindings`**: Auto-bind properties to variables\n- **`isFromDefaultExport`**: Use default export when generating code\n\n## Component Dependencies\n\n**Important**: Make sure all component types referenced in your `defaultChildren` are included in your registry:\n\n```tsx\nconst componentRegistry: ComponentRegistry = {\n ...primitiveComponentDefinitions, // ← Includes 'span' needed below\n Button: {\n // This Button references 'span' in defaultChildren\n defaultChildren: [{ type: 'span', children: 'Click me' }]\n }\n};\n```\n\n## Next Steps\n\nNow that you understand the component registry basics:\n\n- **Creating Custom Components**: Learn how to add complex custom components with advanced features\n- **Advanced Component Configuration**: Explore field overrides, default children, and variable bindings" + "children": "## What is the Component Registry?\n\nThe component registry is a TypeScript object that maps component type names to their definitions. It tells UI Builder:\n\n- **How to render** the component in the editor\n- **What properties** it accepts and their types \n- **How to generate forms** for editing those properties\n- **Import paths** for code generation\n\n```tsx\nimport { ComponentRegistry } from '@/components/ui/ui-builder/types';\n\nconst myComponentRegistry: ComponentRegistry = {\n // Complex component with React component\n 'Button': {\n component: Button, // React component\n schema: z.object({...}), // Zod schema for props\n from: '@/components/ui/button' // Import path\n },\n // Primitive component (no React component needed)\n 'span': {\n schema: z.object({...}) // Just the schema\n }\n};\n```\n\n## Registry Structure\n\nEach registry entry can have these properties:\n\n### Required Properties\n- **`schema`**: Zod schema defining the component's props and their types\n- **`component`**: The React component (required for complex components)\n- **`from`**: Import path for code generation (required for complex components)\n\n### Optional Properties\n- **`isFromDefaultExport`**: Boolean, use default export in generated code\n- **`fieldOverrides`**: Object mapping prop names to custom form fields\n- **`defaultChildren`**: Array of ComponentLayer objects or string\n- **`defaultVariableBindings`**: Array of automatic variable bindings\n\n## Two Types of Components\n\n### Primitive Components\nHTML elements that don't need a React component:\n\n```tsx\nspan: {\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n })\n // No 'component' or 'from' needed\n}\n```\n\n### Complex Components\nCustom React components that need to be imported:\n\n```tsx\nButton: {\n component: Button,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n variant: z.enum(['default', 'destructive']).default('default'),\n }),\n from: '@/components/ui/button'\n}\n```\n\n## Pre-built Component Definitions\n\nUI Builder includes example component definitions for testing and getting started:\n\n```tsx\nimport { primitiveComponentDefinitions } from '@/lib/ui-builder/registry/primitive-component-definitions';\nimport { complexComponentDefinitions } from '@/lib/ui-builder/registry/complex-component-definitions';\n\nconst componentRegistry: ComponentRegistry = {\n ...primitiveComponentDefinitions, // div, span, h1, h2, h3, p, ul, ol, li, img, iframe, a\n ...complexComponentDefinitions, // Button, Badge, Card, Icon, Flexbox, Grid, Markdown, etc.\n};\n```\n\n**Available Pre-built Components:**\n\n**Primitive Components:**\n- **Layout**: `div`, `span` \n- **Typography**: `h1`, `h2`, `h3`, `p`\n- **Lists**: `ul`, `ol`, `li`\n- **Media**: `img`, `iframe`\n- **Navigation**: `a` (links)\n\n**Complex Components:**\n- **Layout**: `Flexbox`, `Grid` \n- **Content**: `Markdown`, `CodePanel`\n- **UI Elements**: `Button`, `Badge`\n- **Advanced**: `Card`, `Icon`, `Accordion`\n\n## Simple Registry Example\n\nHere's a minimal registry with one custom component:\n\n```tsx\nimport { z } from 'zod';\nimport { Alert } from '@/components/ui/alert';\nimport { primitiveComponentDefinitions } from '@/lib/ui-builder/registry/primitive-component-definitions';\nimport { commonFieldOverrides } from '@/lib/ui-builder/registry/form-field-overrides';\n\nconst myComponentRegistry: ComponentRegistry = {\n // Include primitive components for basic HTML elements\n ...primitiveComponentDefinitions,\n \n // Add your custom component\n Alert: {\n component: Alert,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n variant: z.enum(['default', 'destructive']).default('default'),\n }),\n from: '@/components/ui/alert',\n fieldOverrides: commonFieldOverrides()\n }\n};\n```\n\n## Component Dependencies\n\n**Important**: Make sure all component types referenced in your `defaultChildren` are included in your registry:\n\n```tsx\nconst componentRegistry: ComponentRegistry = {\n ...primitiveComponentDefinitions, // ← Includes 'span' needed below\n Button: {\n component: Button,\n schema: z.object({...}),\n from: '@/components/ui/button',\n // This Button references 'span' in defaultChildren\n defaultChildren: [{ \n id: 'btn-text',\n type: 'span', // ← Must be in registry\n name: 'Button Text',\n props: {},\n children: 'Click me'\n }]\n }\n};\n```\n\n## Schema Design Principles\n\nThe Zod schema is crucial as it drives the auto-generated form in the properties panel:\n\n```tsx\nschema: z.object({\n // Use .default() values for better UX\n title: z.string().default('Default Title'),\n \n // Use coerce for type conversion from strings\n count: z.coerce.number().default(1),\n \n // Boolean props become toggle switches\n disabled: z.boolean().optional(),\n \n // Enums become select dropdowns\n variant: z.enum(['default', 'destructive']).default('default'),\n \n // Special props need field overrides\n className: z.string().optional(),\n children: z.any().optional(),\n})\n```\n\n## Building Your Own Registry\n\n**For production applications**, you should create your own component registry with your specific components:\n\n```tsx\n// Your production registry\nconst productionRegistry: ComponentRegistry = {\n // Add only the components you need\n MyButton: { /* your button definition */ },\n MyCard: { /* your card definition */ },\n MyModal: { /* your modal definition */ },\n // Include primitives for basic HTML\n ...primitiveComponentDefinitions,\n};\n```\n\n**The pre-built registries are examples** to help you understand the system and test quickly, but you should replace them with your own component definitions that match your design system." }, { "id": "component-registry-example", @@ -71,14 +71,20 @@ export const COMPONENT_REGISTRY_LAYER = { "props": { "src": "/examples/editor", "title": "UI Builder Component Registry Demo", - "className": "w-full aspect-video", - "frameBorder": 0 + "className": "w-full aspect-video" }, "children": [] } ] } ] + }, + { + "id": "next-steps-registry", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Next Steps\n\nNow that you understand the component registry:\n\n- **Custom Components** - Learn how to add complex custom components with advanced features\n- **Advanced Component Config** - Explore field overrides, default children, and variable bindings\n- **Variables** - Create dynamic content with variable binding\n\nRemember: The registry is just a configuration object. The real power comes from how you design your components and their schemas to create the best editing experience for your users." } ] } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/custom-components.ts b/app/docs/docs-data/docs-page-layers/custom-components.ts index bd94c28..22e84a9 100644 --- a/app/docs/docs-data/docs-page-layers/custom-components.ts +++ b/app/docs/docs-data/docs-page-layers/custom-components.ts @@ -3,7 +3,7 @@ import { ComponentLayer } from "@/components/ui/ui-builder/types"; export const CUSTOM_COMPONENTS_LAYER = { "id": "custom-components", "type": "div", - "name": "Creating Custom Components", + "name": "Custom Components", "props": { "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", "data-group": "component-system" @@ -23,14 +23,14 @@ export const CUSTOM_COMPONENTS_LAYER = { "type": "Markdown", "name": "Markdown", "props": {}, - "children": "Learn how to integrate your existing React components into UI Builder. This guide covers everything from basic component definitions to advanced features like variable bindings and custom form fields." + "children": "Learn how to integrate your existing React components into UI Builder. This focused guide shows you how to take any React component and make it available in the visual editor." }, { "id": "custom-components-content", "type": "Markdown", "name": "Markdown", "props": {}, - "children": "## Step 1: Create Your React Component\n\nUI Builder works with your existing React components without any modifications. Here's a complete example:\n\n```tsx\n// components/ui/fancy-component.tsx\ninterface FancyComponentProps {\n className?: string;\n children?: React.ReactNode;\n title: string;\n count: number;\n disabled?: boolean;\n timestamp?: Date;\n mode: 'fancy' | 'boring';\n}\n\nexport function FancyComponent({ \n className,\n children,\n title,\n count,\n disabled,\n timestamp,\n mode\n}: FancyComponentProps) {\n return (\n
\n

\n {title} ({count})\n

\n {timestamp && }\n
\n {children}\n
\n
\n );\n}\n```\n\n## Step 2: Create the Component Definition\n\nAdd your component to the registry with a complete definition:\n\n```tsx\nimport { z } from 'zod';\nimport { FancyComponent } from '@/components/ui/fancy-component';\nimport { classNameFieldOverrides, childrenFieldOverrides } from '@/lib/ui-builder/registry/form-field-overrides';\nimport { ComponentRegistry } from '@/components/ui/ui-builder/types';\n\nconst myComponentRegistry: ComponentRegistry = {\n // Include existing components\n ...primitiveComponentDefinitions,\n ...complexComponentDefinitions,\n \n // Add your custom component\n FancyComponent: {\n // The React component itself\n component: FancyComponent,\n \n // Zod schema defining props for auto-form generation\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n title: z.string().default('Default Title'),\n count: z.coerce.number().default(1),\n disabled: z.boolean().optional(),\n timestamp: z.coerce.date().optional(),\n mode: z.enum(['fancy', 'boring']).default('fancy'),\n }),\n \n // Import path for code generation\n from: '@/components/ui/fancy-component',\n \n // Custom form field overrides (optional)\n fieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer),\n children: (layer) => childrenFieldOverrides(layer),\n // Other props use auto-generated form fields\n }\n }\n};\n```\n\n## Step 3: Design the Zod Schema\n\nThe schema is crucial as it drives the auto-generated form in the properties panel:\n\n### Supported Zod Types\n\n```tsx\nschema: z.object({\n // Text input\n title: z.string().default('Default Title'),\n \n // Number input (use coerce for string-to-number conversion)\n count: z.coerce.number().default(1),\n \n // Toggle switch\n disabled: z.boolean().optional(),\n \n // Date picker (use coerce for string-to-date conversion)\n timestamp: z.coerce.date().optional(),\n \n // Select dropdown\n mode: z.enum(['fancy', 'boring']).default('fancy'),\n \n // Special props (use z.any() and provide fieldOverrides)\n className: z.string().optional(),\n children: z.any().optional(),\n \n // Nested objects\n config: z.object({\n theme: z.string().default('light'),\n size: z.enum(['sm', 'md', 'lg']).default('md')\n }).optional(),\n \n // Arrays\n items: z.array(z.object({\n label: z.string(),\n value: z.string()\n })).default([])\n})\n```\n\n### Schema Best Practices\n\n- **Use `.default()` values** to provide sensible starting points\n- **Use `z.coerce.number()`** for numeric inputs to handle string conversion\n- **Use `z.coerce.date()`** for date inputs to handle string conversion \n- **Use `z.any()` for `children`** and provide field overrides\n- **Keep nested objects simple** for better editing experience\n- **Use descriptive enum values** that make sense to non-technical users\n\n## Advanced Features\n\n### Default Variable Bindings\n\nAutomatically bind component properties to variables when components are added:\n\n```tsx\nFancyComponent: {\n component: FancyComponent,\n schema: /* ... */,\n from: '@/components/ui/fancy-component',\n \n // Auto-bind properties to variables\n defaultVariableBindings: [\n {\n propName: 'title',\n variableId: 'page-title-var',\n immutable: false // Users can unbind this\n },\n {\n propName: 'count', \n variableId: 'item-count-var',\n immutable: true // Locked binding for system data\n }\n ]\n}\n```\n\n**Use cases for variable bindings:**\n- **User data**: Auto-bind user names, emails, profile info\n- **Branding**: Lock brand colors, logos, company names \n- **System data**: Bind counts, statuses, timestamps\n- **Multi-tenant**: Different data per customer/tenant\n\n### Default Children\n\nProvide default child components when users add your component:\n\n```tsx\nFancyComponent: {\n component: FancyComponent,\n schema: /* ... */,\n from: '@/components/ui/fancy-component',\n \n // Default children when component is added\n defaultChildren: [\n {\n id: 'fancy-content',\n type: 'div',\n name: 'Content Container',\n props: { className: 'p-4' },\n children: [\n {\n id: 'fancy-text',\n type: 'span', \n name: 'Default Text',\n props: {},\n children: 'Default content goes here'\n }\n ]\n }\n ]\n}\n```\n\n## Component Definition Properties Reference\n\n### Required\n- **`component`**: React component function/class\n- **`schema`**: Zod schema defining props\n- **`from`**: Import path for code generation\n\n### Optional\n- **`isFromDefaultExport`**: Boolean, use default export in generated code\n- **`fieldOverrides`**: Object mapping prop names to custom form fields\n- **`defaultChildren`**: Array of ComponentLayer objects or string\n- **`defaultVariableBindings`**: Array of automatic variable bindings\n\n### Variable Binding Properties\n- **`propName`**: Component property to bind\n- **`variableId`**: ID of variable to bind to\n- **`immutable`**: Boolean, prevent users from unbinding\n\n## Best Practices\n\n### Component Design\n- **Accept standard props**: Always include `className` and `children`\n- **Use TypeScript interfaces**: Clear prop definitions help with schema creation\n- **Keep props simple**: Complex nested objects are hard to edit visually\n- **Follow design system**: Consistent with your existing component patterns\n\n### Schema Design\n- **Provide meaningful defaults**: Reduce setup friction for users\n- **Use clear enum values**: Help non-technical users understand options\n- **Group related props**: Use nested objects for logical grouping\n- **Validate appropriately**: Don't over-constrain creative use\n\n### Testing Your Components\n- **Test in the editor**: Add to canvas and verify the editing experience\n- **Check form generation**: Ensure all props have appropriate form fields\n- **Verify code generation**: Export and check the generated React code\n- **Test with variables**: Ensure variable bindings work as expected" + "children": "## The Process: From React Component to UI Builder\n\nIntegrating a custom component into UI Builder is a straightforward 3-step process:\n\n1. **Your React Component** - Works as-is, no modifications needed\n2. **Create Component Definition** - Define schema and configuration\n3. **Add to Registry** - Include in your componentRegistry prop\n\n## Step 1: Your Existing React Component\n\nUI Builder works with your existing React components without any modifications. Here's a realistic example:\n\n```tsx\n// components/ui/user-card.tsx\ninterface UserCardProps {\n className?: string;\n children?: React.ReactNode;\n name: string;\n email: string;\n role: 'admin' | 'user' | 'viewer';\n avatarUrl?: string;\n isOnline?: boolean;\n}\n\nexport function UserCard({ \n className,\n children,\n name,\n email,\n role,\n avatarUrl,\n isOnline = false\n}: UserCardProps) {\n return (\n
\n
\n {avatarUrl && (\n {name}\n\n )}\n
\n
\n

{name}

\n {isOnline && (\n
\n )}\n
\n

{email}

\n \n {role}\n \n
\n
\n {children && (\n
\n {children}\n
\n )}\n
\n );\n}\n```\n\n**Key Requirements for UI Builder Components:**\n- **Accept `className`**: For styling integration\n- **Accept `children`**: For content composition (optional)\n- **Use TypeScript interfaces**: Clear prop definitions help with schema creation\n- **Follow your design system**: Keep consistent with existing patterns\n\n## Step 2: Create the Component Definition\n\nNext, create a definition that tells UI Builder how to work with your component:\n\n```tsx\nimport { z } from 'zod';\nimport { UserCard } from '@/components/ui/user-card';\nimport { classNameFieldOverrides, childrenFieldOverrides } from '@/lib/ui-builder/registry/form-field-overrides';\n\nconst userCardDefinition = {\n // The React component itself\n component: UserCard,\n \n // Zod schema defining props for the auto-generated form\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n name: z.string().default('John Doe'),\n email: z.string().default('john@example.com'),\n role: z.enum(['admin', 'user', 'viewer']).default('user'),\n avatarUrl: z.string().optional(),\n isOnline: z.boolean().default(false),\n }),\n \n // Import path for code generation\n from: '@/components/ui/user-card',\n \n // Custom form field overrides (optional)\n fieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer),\n children: (layer) => childrenFieldOverrides(layer),\n email: (layer) => ({\n inputProps: {\n type: 'email',\n placeholder: 'user@example.com'\n }\n }),\n avatarUrl: (layer) => ({\n inputProps: {\n type: 'url',\n placeholder: 'https://example.com/avatar.jpg'\n }\n })\n }\n};\n```\n\n**Schema Design Tips:**\n- Use `.default()` values for better user experience\n- Use `z.coerce.number()` for numeric inputs (handles string conversion)\n- Use `z.coerce.date()` for date inputs (handles string conversion)\n- Always include `className` and `children` props for flexibility\n- Use clear enum values that make sense to non-technical users\n\n## Step 3: Add to Your Component Registry\n\nInclude your definition in the componentRegistry prop:\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 myComponentRegistry = {\n // Include pre-built components\n ...primitiveComponentDefinitions, // div, span, img, etc.\n ...complexComponentDefinitions, // Button, Badge, Card, etc.\n \n // Add your custom components\n UserCard: userCardDefinition,\n // Add more custom components...\n};\n\nexport function App() {\n return (\n \n );\n}\n```\n\nThat's it! Your `UserCard` component is now available in the UI Builder editor.\n\n## Advanced Features\n\n### Automatic Variable Bindings\n\nMake components automatically bind to system data when added to the canvas:\n\n```tsx\nUserCard: {\n component: UserCard,\n schema: z.object({...}),\n from: '@/components/ui/user-card',\n \n // Auto-bind properties to variables when component is added\n defaultVariableBindings: [\n {\n propName: 'name',\n variableId: 'current-user-name',\n immutable: false // Users can unbind this\n },\n {\n propName: 'email', \n variableId: 'current-user-email',\n immutable: true // Locked for security\n },\n {\n propName: 'role',\n variableId: 'current-user-role',\n immutable: true // Prevent role tampering\n }\n ]\n}\n```\n\n**Use Cases for Variable Bindings:**\n- **User profiles**: Auto-bind to current user data\n- **Multi-tenant apps**: Bind to tenant-specific branding\n- **System data**: Connect to live counters, statuses, timestamps\n- **A/B testing**: Bind to feature flag variables\n\n**Immutable Bindings**: Set `immutable: true` to prevent users from unbinding critical data like user IDs, brand colors, or security permissions.\n\n### Default Children Structure\n\nProvide default child components when users add your component:\n\n```tsx\nUserCard: {\n component: UserCard,\n schema: z.object({...}),\n from: '@/components/ui/user-card',\n \n // Default children when component is added\n defaultChildren: [\n {\n id: 'user-actions',\n type: 'div',\n name: 'Action Buttons',\n props: { \n className: 'flex gap-2 mt-2' \n },\n children: [\n {\n id: 'edit-btn',\n type: 'Button',\n name: 'Edit Button',\n props: {\n variant: 'outline',\n size: 'sm'\n },\n children: 'Edit Profile'\n },\n {\n id: 'message-btn', \n type: 'Button',\n name: 'Message Button',\n props: {\n variant: 'default',\n size: 'sm'\n },\n children: 'Send Message'\n }\n ]\n }\n ]\n}\n```\n\n**Important**: All component types referenced in `defaultChildren` must exist in your componentRegistry (like `Button` and `div` in the example above).\n\n## Complete Example: Blog Post Card\n\nHere's a complete example showing a blog post component with all advanced features:\n\n```tsx\n// components/ui/blog-post-card.tsx\ninterface BlogPostCardProps {\n className?: string;\n children?: React.ReactNode;\n title: string;\n excerpt: string;\n author: string;\n publishedAt: Date;\n readTime: number;\n category: string;\n featured?: boolean;\n}\n\nexport function BlogPostCard({ \n className, \n children, \n title,\n excerpt,\n author,\n publishedAt,\n readTime,\n category,\n featured = false\n}: BlogPostCardProps) {\n return (\n
\n {featured && (\n
\n ⭐ Featured Post\n
\n )}\n
\n {category} • {readTime} min read\n
\n

{title}

\n

{excerpt}

\n
\n
\n By {author} • {publishedAt.toLocaleDateString()}\n
\n {children}\n
\n
\n );\n}\n\n// Component definition with all features\nconst blogPostCardDefinition = {\n component: BlogPostCard,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n title: z.string().default('Sample Blog Post Title'),\n excerpt: z.string().default('A brief description of the blog post content...'),\n author: z.string().default('John Author'),\n publishedAt: z.coerce.date().default(new Date()),\n readTime: z.coerce.number().default(5),\n category: z.string().default('Technology'),\n featured: z.boolean().default(false),\n }),\n from: '@/components/ui/blog-post-card',\n fieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer),\n children: (layer) => childrenFieldOverrides(layer),\n excerpt: (layer) => ({\n fieldType: 'textarea',\n inputProps: {\n placeholder: 'Brief post description...'\n }\n }),\n publishedAt: (layer) => ({\n fieldType: 'date'\n })\n },\n defaultVariableBindings: [\n {\n propName: 'author',\n variableId: 'current-author-name',\n immutable: false\n }\n ],\n defaultChildren: [\n {\n id: 'post-actions',\n type: 'div',\n name: 'Post Actions',\n props: { className: 'flex gap-2' },\n children: [\n {\n id: 'read-more',\n type: 'Button',\n name: 'Read More',\n props: {\n variant: 'outline',\n size: 'sm'\n },\n children: 'Read More'\n }\n ]\n }\n ]\n};\n```\n\n## See It In Action\n\nView the [Immutable Bindings Example](/examples/editor/immutable-bindings) to see custom components with automatic variable bindings in action. This example demonstrates:\n\n- Custom components with system data bindings\n- Immutable bindings for security-sensitive data\n- Real-world component integration patterns\n\n## Testing Your Custom Components\n\nAfter adding your component to the registry:\n\n1. **Add to Canvas**: Find your component in the component panel and add it\n2. **Test Properties**: Use the properties panel to configure all props\n3. **Check Children**: Verify children support works if implemented\n4. **Test Variables**: If using variable bindings, test the binding UI\n5. **Export Code**: Use the export feature to verify generated React code\n6. **Render Test**: Test with LayerRenderer to ensure runtime rendering works\n\n## Best Practices\n\n### Component Design\n- **Keep props simple**: Complex nested objects are hard to edit visually\n- **Provide sensible defaults**: Reduce setup friction for content creators\n- **Use semantic prop names**: Make properties self-explanatory\n- **Handle edge cases**: Always provide fallbacks for optional data\n- **Follow accessibility guidelines**: Ensure components work for all users\n\n### Schema Design\n- **Use meaningful defaults**: Help users understand expected values\n- **Validate appropriately**: Don't over-constrain creative usage\n- **Group related props**: Use nested objects for logical groupings (sparingly)\n- **Provide helpful enums**: Use descriptive enum values instead of codes\n- **Consider the editing experience**: Think about how non-technical users will configure props\n\n### Variable Bindings\n- **Use immutable bindings** for system data, security info, and brand consistency\n- **Leave content editable** by not binding text props that should be customizable\n- **Provide meaningful variable names** that clearly indicate their purpose\n- **Test binding scenarios** to ensure the editing experience is smooth\n\n### Performance\n- **Minimize re-renders**: Use React.memo if your component is expensive to render\n- **Optimize images**: Handle image loading states and errors gracefully\n- **Consider bundle size**: Avoid heavy dependencies in components used in the editor\n\nWith these patterns, your custom components will provide a seamless editing experience while maintaining the flexibility and power of your existing React components." } ] } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/data-binding.ts b/app/docs/docs-data/docs-page-layers/data-binding.ts deleted file mode 100644 index abff983..0000000 --- a/app/docs/docs-data/docs-page-layers/data-binding.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ComponentLayer } from "@/components/ui/ui-builder/types"; - -export const DATA_BINDING_LAYER = { - "id": "data-binding", - "type": "div", - "name": "Data 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": "Data Binding", - "id": "data-binding-title", - "name": "Text", - "props": { - "className": "text-4xl" - } - }, - { - "id": "data-binding-intro", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "Connect UI Builder components to external data sources through variable binding and runtime value injection. Transform static designs into dynamic, data-driven interfaces that respond to real-time data." - }, - { - "id": "data-binding-content", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "## How Data Binding Works\n\nData binding in UI Builder operates through a two-phase process:\n\n1. **Design Phase**: Variables are defined and bound to component properties\n2. **Runtime Phase**: Variable values are injected when rendering pages\n\n```tsx\n// Design phase - define structure with variable bindings\nconst pageStructure = {\n id: 'user-profile',\n type: 'div',\n children: [\n {\n id: 'user-name',\n type: 'h1',\n props: {\n children: { __variableRef: 'userName' } // Bound to variable\n }\n },\n {\n id: 'user-email',\n type: 'p',\n props: {\n children: { __variableRef: 'userEmail' } // Bound to variable\n }\n }\n ]\n};\n\n// Runtime phase - inject real data\nconst variableValues = {\n userName: 'Jane Smith',\n userEmail: 'jane@example.com'\n};\n\n\n```\n\n## Static Data Binding\n\n### Direct Value Injection\n\nThe simplest form of data binding uses static data:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\n\nfunction StaticDataExample() {\n // Static data from your application\n const userData = {\n name: 'John Doe',\n email: 'john@example.com',\n role: 'Admin',\n isActive: true\n };\n\n // Map data to variable values\n const variableValues = {\n 'user-name-var': userData.name,\n 'user-email-var': userData.email,\n 'user-role-var': userData.role,\n 'user-active-var': userData.isActive\n };\n\n return (\n \n );\n}\n```\n\n### Configuration-Based Data\n\n```tsx\nfunction ConfigurationExample() {\n // Application configuration\n const appConfig = {\n companyName: 'Acme Corporation',\n primaryColor: '#3b82f6',\n supportEmail: 'support@acme.com',\n featuresEnabled: {\n darkMode: true,\n notifications: false,\n analytics: true\n }\n };\n\n const configVariables = {\n 'company-name': appConfig.companyName,\n 'primary-color': appConfig.primaryColor,\n 'support-email': appConfig.supportEmail,\n 'dark-mode-enabled': appConfig.featuresEnabled.darkMode,\n 'notifications-enabled': appConfig.featuresEnabled.notifications\n };\n\n return (\n \n );\n}\n```\n\n## Dynamic Data Binding\n\n### API Data Integration\n\nConnect UI Builder to API responses:\n\n```tsx\nfunction APIDataExample({ userId }) {\n const [user, setUser] = useState(null);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n fetch(`/api/users/${userId}`)\n .then(response => response.json())\n .then(userData => {\n setUser(userData);\n setLoading(false);\n })\n .catch(error => {\n console.error('Failed to load user data:', error);\n setLoading(false);\n });\n }, [userId]);\n\n if (loading) {\n return
Loading...
;\n }\n\n if (!user) {\n return
User not found
;\n }\n\n // Map API response to variable values\n const apiVariables = {\n 'user-id': user.id,\n 'user-name': user.name,\n 'user-email': user.email,\n 'user-avatar': user.avatarUrl || '/default-avatar.png',\n 'is-premium': user.subscription?.plan === 'premium',\n 'last-login': user.lastLoginAt\n };\n\n return (\n \n );\n}\n```\n\n### Real-Time Data Updates\n\n```tsx\nfunction RealTimeDataExample() {\n const [liveData, setLiveData] = useState({\n currentPrice: 0,\n onlineUsers: 0,\n serverStatus: 'unknown'\n });\n\n useEffect(() => {\n // WebSocket connection for real-time updates\n const ws = new WebSocket('wss://api.example.com/live');\n \n ws.onmessage = (event) => {\n const update = JSON.parse(event.data);\n setLiveData(prev => ({ ...prev, ...update }));\n };\n\n return () => ws.close();\n }, []);\n\n const liveVariables = {\n 'current-price': liveData.currentPrice,\n 'online-users': liveData.onlineUsers,\n 'server-status': liveData.serverStatus,\n 'last-updated': new Date().toLocaleTimeString()\n };\n\n return (\n \n );\n}\n```\n\n## Database Integration\n\n### Server-Side Data Binding\n\n```tsx\n// pages/dashboard/[id].tsx\nexport async function getServerSideProps({ params }) {\n try {\n // Fetch data on the server\n const [user, analytics, notifications] = await Promise.all([\n fetch(`${process.env.API_URL}/users/${params.id}`).then(r => r.json()),\n fetch(`${process.env.API_URL}/analytics/${params.id}`).then(r => r.json()),\n fetch(`${process.env.API_URL}/notifications/${params.id}`).then(r => r.json())\n ]);\n\n // Map database results to variable values\n const variableValues = {\n 'user-name': user.name,\n 'user-email': user.email,\n 'total-visits': analytics.totalVisits,\n 'conversion-rate': analytics.conversionRate,\n 'unread-notifications': notifications.unreadCount,\n 'last-activity': user.lastActivityAt\n };\n\n return {\n props: {\n pageData: dashboardPageStructure,\n variables: dashboardVariables,\n variableValues\n }\n };\n } catch (error) {\n return {\n notFound: true\n };\n }\n}\n\nfunction UserDashboard({ pageData, variables, variableValues }) {\n return (\n \n );\n}\n```\n\n### Client-Side Database Queries\n\n```tsx\nimport { useQuery } from '@tanstack/react-query';\n\nfunction ClientSideDataBinding({ projectId }) {\n const { data: project, isLoading } = useQuery({\n queryKey: ['project', projectId],\n queryFn: () => fetch(`/api/projects/${projectId}`).then(r => r.json())\n });\n\n const { data: metrics } = useQuery({\n queryKey: ['metrics', projectId],\n queryFn: () => fetch(`/api/metrics/${projectId}`).then(r => r.json()),\n enabled: !!projectId\n });\n\n if (isLoading) {\n return ;\n }\n\n const projectVariables = {\n 'project-name': project?.name || 'Unknown Project',\n 'project-description': project?.description || '',\n 'project-status': project?.status || 'inactive',\n 'total-tasks': metrics?.totalTasks || 0,\n 'completed-tasks': metrics?.completedTasks || 0,\n 'team-size': project?.team?.length || 0\n };\n\n return (\n \n );\n}\n```\n\n## Context and State Integration\n\n### React Context Data\n\n```tsx\nimport { useContext } from 'react';\nimport { AuthContext, ThemeContext } from '../contexts';\n\nfunction ContextDataBinding() {\n const { user, permissions } = useContext(AuthContext);\n const { theme, preferences } = useContext(ThemeContext);\n\n const contextVariables = {\n 'current-user': user?.name || 'Guest',\n 'user-role': user?.role || 'viewer',\n 'can-edit': permissions?.canEdit || false,\n 'can-admin': permissions?.canAdmin || false,\n 'theme-mode': theme.mode,\n 'primary-color': theme.colors.primary,\n 'font-size': preferences.fontSize\n };\n\n return (\n \n );\n}\n```\n\n### Redux Store Integration\n\n```tsx\nimport { useSelector } from 'react-redux';\n\nfunction ReduxDataBinding() {\n const user = useSelector(state => state.auth.user);\n const cart = useSelector(state => state.cart);\n const notifications = useSelector(state => state.notifications);\n\n const storeVariables = {\n 'user-name': user?.name,\n 'user-avatar': user?.avatar,\n 'cart-items': cart.items.length,\n 'cart-total': cart.total,\n 'unread-notifications': notifications.unread.length,\n 'is-logged-in': !!user\n };\n\n return (\n \n );\n}\n```\n\n## Form Data Binding\n\n### Controlled Form Components\n\n```tsx\nfunction FormDataBinding() {\n const [formData, setFormData] = useState({\n name: '',\n email: '',\n newsletter: false,\n plan: 'basic'\n });\n\n const [errors, setErrors] = useState({});\n\n const handleInputChange = (field, value) => {\n setFormData(prev => ({ ...prev, [field]: value }));\n // Clear errors when user starts typing\n if (errors[field]) {\n setErrors(prev => ({ ...prev, [field]: null }));\n }\n };\n\n const formVariables = {\n 'form-name': formData.name,\n 'form-email': formData.email,\n 'form-newsletter': formData.newsletter,\n 'form-plan': formData.plan,\n 'name-error': errors.name || '',\n 'email-error': errors.email || '',\n 'form-valid': Object.keys(errors).length === 0\n };\n\n return (\n
\n \n \n {/* Form controls outside of UI Builder */}\n
\n handleInputChange('name', e.target.value)}\n placeholder=\"Name\"\n />\n handleInputChange('email', e.target.value)}\n placeholder=\"Email\"\n />\n
\n
\n );\n}\n```\n\n## Data Transformation\n\n### Data Mapping and Formatting\n\n```tsx\nfunction DataTransformationExample({ rawData }) {\n // Transform raw API data into UI-friendly format\n const transformedData = useMemo(() => {\n if (!rawData) return {};\n\n return {\n // Format currency\n 'display-price': new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD'\n }).format(rawData.price),\n \n // Format dates\n 'formatted-date': new Date(rawData.createdAt).toLocaleDateString(),\n 'relative-time': formatDistanceToNow(new Date(rawData.updatedAt)),\n \n // Transform status\n 'status-color': {\n 'active': 'green',\n 'pending': 'yellow',\n 'inactive': 'red'\n }[rawData.status] || 'gray',\n \n // Calculate derived values\n 'completion-percentage': Math.round(\n (rawData.completedTasks / rawData.totalTasks) * 100\n ),\n \n // Format arrays\n 'tags-list': rawData.tags?.join(', ') || 'No tags',\n 'team-names': rawData.team?.map(member => member.name).join(', ') || 'No team'\n };\n }, [rawData]);\n\n return (\n \n );\n}\n```\n\n### Conditional Data Binding\n\n```tsx\nfunction ConditionalDataBinding({ user, features }) {\n const conditionalVariables = useMemo(() => {\n const base = {\n 'user-name': user?.name || 'Guest',\n 'is-authenticated': !!user\n };\n\n // Add premium features if user has access\n if (user?.subscription?.plan === 'premium') {\n base['show-premium-badge'] = true;\n base['premium-features'] = 'Unlock unlimited projects';\n } else {\n base['show-premium-badge'] = false;\n base['premium-features'] = 'Upgrade to Premium';\n }\n\n // Feature flags\n if (features?.enableBetaFeatures) {\n base['show-beta-banner'] = true;\n base['beta-message'] = 'Try our new beta features!';\n }\n\n // Admin-only data\n if (user?.role === 'admin') {\n base['admin-panel-visible'] = true;\n base['user-count'] = features?.userCount || 0;\n }\n\n return base;\n }, [user, features]);\n\n return (\n \n );\n}\n```\n\n## Error Handling and Loading States\n\n### Graceful Error Handling\n\n```tsx\nfunction ErrorHandlingExample({ dataId }) {\n const [data, setData] = useState(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState(null);\n\n useEffect(() => {\n setLoading(true);\n setError(null);\n \n fetch(`/api/data/${dataId}`)\n .then(response => {\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n return response.json();\n })\n .then(setData)\n .catch(setError)\n .finally(() => setLoading(false));\n }, [dataId]);\n\n const statusVariables = {\n 'is-loading': loading,\n 'has-error': !!error,\n 'error-message': error?.message || '',\n 'has-data': !!data && !loading && !error,\n \n // Data variables (safe with fallbacks)\n 'title': data?.title || 'No title available',\n 'description': data?.description || 'No description available',\n 'status': data?.status || 'unknown',\n 'updated-date': data?.updatedAt \n ? new Date(data.updatedAt).toLocaleDateString() \n : 'Never'\n };\n\n return (\n \n );\n}\n```\n\n## Performance Optimization\n\n### Memoized Data Processing\n\n```tsx\nfunction OptimizedDataBinding({ largeDataset }) {\n // Memoize expensive data transformations\n const processedData = useMemo(() => {\n if (!largeDataset?.length) return {};\n\n // Expensive calculations only run when dataset changes\n const total = largeDataset.reduce((sum, item) => sum + item.value, 0);\n const average = total / largeDataset.length;\n const maximum = Math.max(...largeDataset.map(item => item.value));\n const minimum = Math.min(...largeDataset.map(item => item.value));\n \n return {\n 'total-value': total,\n 'average-value': average.toFixed(2),\n 'max-value': maximum,\n 'min-value': minimum,\n 'item-count': largeDataset.length\n };\n }, [largeDataset]);\n\n // Memoize the component to prevent unnecessary re-renders\n return useMemo(() => (\n \n ), [processedData]);\n}\n```\n\n### Lazy Data Loading\n\n```tsx\nfunction LazyDataBinding({ shouldLoad }) {\n const [data, setData] = useState(null);\n const [loaded, setLoaded] = useState(false);\n\n useEffect(() => {\n if (shouldLoad && !loaded) {\n fetch('/api/expensive-data')\n .then(response => response.json())\n .then(result => {\n setData(result);\n setLoaded(true);\n });\n }\n }, [shouldLoad, loaded]);\n\n const lazyVariables = {\n 'data-loaded': loaded,\n 'show-placeholder': !loaded,\n 'content': data?.content || 'Loading...',\n 'metadata': data?.metadata || {}\n };\n\n return (\n \n );\n}\n```\n\n## Best Practices\n\n### Data Binding Patterns\n\n- **Use meaningful variable names** that reflect their data source\n- **Provide fallback values** for all variables to prevent rendering errors\n- **Memoize expensive data transformations** to optimize performance\n- **Handle loading and error states** gracefully in your data variables\n- **Separate data fetching logic** from UI Builder rendering logic\n- **Use TypeScript** for better type safety with variable values\n\n### Security Considerations\n\n- **Sanitize user-provided data** before binding to variables\n- **Validate data types** match variable definitions\n- **Use environment variables** for sensitive configuration\n- **Implement proper authentication** for data endpoints\n- **Escape HTML content** in string variables to prevent XSS\n\n### Performance Tips\n\n- **Batch data requests** when possible to reduce API calls\n- **Use React.memo** to prevent unnecessary re-renders\n- **Implement pagination** for large datasets\n- **Cache frequently accessed data** using React Query or SWR\n- **Debounce rapid data updates** to avoid excessive re-renders" - } - ] - } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/editor-panel-config.ts b/app/docs/docs-data/docs-page-layers/editor-panel-config.ts deleted file mode 100644 index f8cf505..0000000 --- a/app/docs/docs-data/docs-page-layers/editor-panel-config.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { ComponentLayer } from "@/components/ui/ui-builder/types"; - -export const EDITOR_PANEL_CONFIG_LAYER = { - "id": "editor-panel-config", - "type": "div", - "name": "Editor Panel Config", - "props": { - "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", - "data-group": "editor-features" - }, - "children": [ - { - "type": "span", - "children": "Editor Panel Config", - "id": "editor-panel-config-title", - "name": "Text", - "props": { - "className": "text-4xl" - } - }, - { - "id": "editor-panel-config-intro", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "Configure specific editor panels and components to create tailored editing experiences. Customize the navigation bar, editor panel functionality, and create branded, role-based interfaces." - }, - { - "id": "editor-panel-config-demo", - "type": "div", - "name": "div", - "props": {}, - "children": [ - { - "id": "editor-panel-config-badge", - "type": "Badge", - "name": "Badge", - "props": { - "variant": "default", - "className": "rounded rounded-b-none" - }, - "children": [ - { - "id": "editor-panel-config-badge-text", - "type": "span", - "name": "span", - "props": {}, - "children": "Custom Editor Interface" - } - ] - }, - { - "id": "editor-panel-config-demo-frame", - "type": "div", - "name": "div", - "props": { - "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" - }, - "children": [ - { - "id": "editor-panel-config-iframe", - "type": "iframe", - "name": "iframe", - "props": { - "src": "http://localhost:3000/examples/editor", - "title": "Editor Panel Config Demo", - "className": "aspect-square md:aspect-video w-full" - }, - "children": [] - } - ] - } - ] - }, - { - "id": "editor-panel-config-content", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "## Custom Navigation Bar\n\nReplace or customize the default navigation bar to match your brand and workflow:\n\n```tsx\nimport { useEditorStore } from '@/lib/ui-builder/store/editor-store';\nimport { useLayerStore } from '@/lib/ui-builder/store/layer-store';\nimport { Button } from '@/components/ui/button';\nimport { ThemeToggle } from '@/components/theme-toggle';\n\nconst CustomNavBar = () => {\n const showLeftPanel = useEditorStore(state => state.showLeftPanel);\n const toggleLeftPanel = useEditorStore(state => state.toggleLeftPanel);\n const showRightPanel = useEditorStore(state => state.showRightPanel);\n const toggleRightPanel = useEditorStore(state => state.toggleRightPanel);\n \n const pages = useLayerStore(state => state.pages);\n const selectedPageId = useLayerStore(state => state.selectedPageId);\n const currentPage = pages.find(p => p.id === selectedPageId);\n \n return (\n \n );\n};\n\n\n }}\n/>\n```\n\n## Branded Navigation\n\nCreate a fully branded navigation experience:\n\n```tsx\nconst BrandedNavBar = () => {\n const hasUnsavedChanges = useLayerStore(state => state.hasUnsavedChanges);\n const saveLayout = async () => {\n // Your save logic\n const pages = useLayerStore.getState().pages;\n await saveToYourBackend(pages);\n };\n \n return (\n \n );\n};\n```\n\n## Role-Based Navigation\n\nCustomize the navigation based on user permissions:\n\n```tsx\nconst RoleBasedNavBar = ({ user }) => {\n const canPublish = user.role === 'admin' || user.role === 'editor';\n const canDeletePages = user.role === 'admin';\n \n return (\n \n );\n};\n\n\n }}\n/>\n```\n\n## Custom Editor Panel\n\nEnhance the editor panel with additional tools and features:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\nimport { useLayerStore } from '@/lib/ui-builder/store/layer-store';\nimport { useEditorStore } from '@/lib/ui-builder/store/editor-store';\n\nconst EnhancedEditorPanel = ({ className }) => {\n const selectedPageId = useLayerStore(state => state.selectedPageId);\n const findLayerById = useLayerStore(state => state.findLayerById);\n const componentRegistry = useEditorStore(state => state.registry);\n const variables = useLayerStore(state => state.variables);\n \n const currentPage = findLayerById(selectedPageId);\n const [zoom, setZoom] = useState(1);\n const [deviceMode, setDeviceMode] = useState('desktop');\n \n const deviceSizes = {\n mobile: { width: 375, height: 667 },\n tablet: { width: 768, height: 1024 },\n desktop: { width: '100%', height: '100%' }\n };\n \n return (\n
\n {/* Enhanced Toolbar */}\n
\n
\n {/* Device Selector */}\n \n \n \n \n \n \n \n \n \n \n \n \n {/* Zoom Control */}\n
\n \n \n {Math.round(zoom * 100)}%\n \n \n
\n
\n \n
\n {/* Grid Toggle */}\n \n \n {/* Undo/Redo */}\n
\n \n \n
\n
\n
\n \n {/* Canvas Area */}\n
\n
\n {currentPage && (\n \n )}\n
\n
\n \n {/* Status Bar */}\n
\n \n Page: {currentPage?.name || 'Untitled'}\n \n \n {variables.length} variables • {deviceMode} view\n \n
\n
\n );\n};\n\n\n }}\n/>\n```\n\n## Context-Aware Navigation\n\nMake the navigation respond to editor state:\n\n```tsx\nconst ContextAwareNavBar = () => {\n const selectedLayerId = useLayerStore(state => state.selectedLayerId);\n const findLayerById = useLayerStore(state => state.findLayerById);\n const duplicateLayer = useLayerStore(state => state.duplicateLayer);\n const removeLayer = useLayerStore(state => state.removeLayer);\n \n const selectedLayer = findLayerById(selectedLayerId);\n const isComponentSelected = selectedLayer && selectedLayerId !== selectedPageId;\n \n return (\n \n );\n};\n```\n\n## Integration with External Systems\n\nConnect your navigation to external services:\n\n```tsx\nconst IntegratedNavBar = () => {\n const [savingStatus, setSavingStatus] = useState('saved'); // 'saving', 'saved', 'error'\n const [collaborators, setCollaborators] = useState([]);\n \n const handleSave = async () => {\n setSavingStatus('saving');\n try {\n const pages = useLayerStore.getState().pages;\n await saveToYourCMS(pages);\n setSavingStatus('saved');\n } catch (error) {\n setSavingStatus('error');\n }\n };\n \n const handlePublish = async () => {\n const pages = useLayerStore.getState().pages;\n await publishToLiveSite(pages);\n showNotification('Published successfully!');\n };\n \n return (\n \n );\n};\n```\n\n## Mobile-Responsive Navigation\n\nCreate navigation that works well on mobile devices:\n\n```tsx\nconst ResponsiveNavBar = () => {\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const isMobile = useMediaQuery('(max-width: 768px)');\n \n if (isMobile) {\n return (\n \n );\n }\n \n // Desktop navigation\n return (\n \n );\n};\n```\n\n## Best Practices\n\n### Navigation Design\n- **Keep it simple** - Don't overcrowd the navigation\n- **Group related actions** - Use dropdowns for secondary actions\n- **Show context** - Display current page/component information\n- **Provide feedback** - Show save status and loading states\n\n### Performance\n- **Memoize components** - Use React.memo for nav components\n- **Debounce actions** - Avoid rapid save/publish calls\n- **Optimize re-renders** - Subscribe to specific store slices\n\n### Accessibility\n- **Keyboard navigation** - Ensure all actions are keyboard accessible\n- **ARIA labels** - Provide proper labels for screen readers\n- **Focus management** - Maintain logical focus order\n- **Color contrast** - Ensure sufficient contrast for all elements" - } - ] - } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/field-overrides.ts b/app/docs/docs-data/docs-page-layers/field-overrides.ts index 9f40fa4..4662d24 100644 --- a/app/docs/docs-data/docs-page-layers/field-overrides.ts +++ b/app/docs/docs-data/docs-page-layers/field-overrides.ts @@ -3,7 +3,7 @@ import { ComponentLayer } from "@/components/ui/ui-builder/types"; export const FIELD_OVERRIDES_LAYER = { "id": "field-overrides", "type": "div", - "name": "Advanced Component Configuration", + "name": "Advanced Configuration", "props": { "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", "data-group": "component-system" @@ -23,14 +23,68 @@ export const FIELD_OVERRIDES_LAYER = { "type": "Markdown", "name": "Markdown", "props": {}, - "children": "Master advanced component configuration techniques including field overrides, default children, and variable bindings to create sophisticated, user-friendly editing experiences." + "children": "Take your component integration to the next level with field overrides, default children, and automatic variable bindings. These advanced techniques create polished, user-friendly editing experiences." }, { "id": "field-overrides-content", "type": "Markdown", "name": "Markdown", "props": {}, - "children": "## Field Overrides\n\nField overrides replace auto-generated form fields with specialized input controls, providing better user experiences for complex data types.\n\n### How Field Overrides Work\n\nField overrides are defined within component definitions using the `fieldOverrides` property:\n\n```tsx\nimport { z } from 'zod';\nimport { classNameFieldOverrides, childrenFieldOverrides } from '@/lib/ui-builder/registry/form-field-overrides';\n\nconst componentRegistry = {\n MyComponent: {\n component: MyComponent,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n icon: z.string().default('home'),\n color: z.string().default('#000000'),\n }),\n from: '@/components/ui/my-component',\n fieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer), // Advanced Tailwind editor\n children: (layer) => childrenFieldOverrides(layer), // Component selector\n icon: (layer) => iconNameFieldOverrides(layer), // Icon picker\n // color prop uses auto-generated form field\n }\n }\n};\n```\n\n### Built-in Field Overrides\n\n#### `classNameFieldOverrides(layer)`\nAdvanced Tailwind CSS class editor:\n- Auto-complete for Tailwind classes\n- Responsive breakpoint controls \n- Visual class grouping\n- Theme-aware suggestions\n\n```tsx\nfieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer)\n}\n```\n\n#### `childrenFieldOverrides(layer)`\nSearchable component selector for child components:\n- Dropdown with available component types\n- Search and filter capabilities\n- Respects component hierarchy\n\n```tsx\nfieldOverrides: {\n children: (layer) => childrenFieldOverrides(layer)\n}\n```\n\n#### `childrenAsTextareaFieldOverrides(layer)`\nSimple textarea for text content:\n- Multi-line text editing\n- Perfect for span, p, and text elements\n\n```tsx\nfieldOverrides: {\n children: (layer) => childrenAsTextareaFieldOverrides(layer)\n}\n```\n\n#### `childrenAsTipTapFieldOverrides(layer)`\nRich text editor using TipTap:\n- WYSIWYG markdown editing\n- Formatting toolbar\n- Ideal for Markdown components\n\n```tsx\nfieldOverrides: {\n children: (layer) => childrenAsTipTapFieldOverrides(layer)\n}\n```\n\n#### `iconNameFieldOverrides(layer)`\nIcon picker with visual preview:\n- Grid of available icons\n- Search functionality\n- Live icon preview\n\n```tsx\nfieldOverrides: {\n iconName: (layer) => iconNameFieldOverrides(layer)\n}\n```\n\n#### `commonFieldOverrides()`\nConvenience function for standard className and children overrides:\n\n```tsx\nfieldOverrides: commonFieldOverrides()\n// Equivalent to:\n// fieldOverrides: {\n// className: (layer) => classNameFieldOverrides(layer),\n// children: (layer) => childrenFieldOverrides(layer)\n// }\n```\n\n### Creating Custom Field Overrides\n\nCreate specialized input controls for unique data types:\n\n```tsx\nimport { AutoFormInputComponentProps } from '@/components/ui/ui-builder/types';\nimport { FormItem, FormLabel, FormControl } from '@/components/ui/form';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';\n\nconst customColorFieldOverride = (layer) => ({\n fieldType: ({ label, field }: AutoFormInputComponentProps) => (\n \n {label}\n \n
\n field.onChange(e.target.value)}\n className=\"w-12 h-8 rounded border\"\n />\n field.onChange(e.target.value)}\n placeholder=\"#000000\"\n className=\"flex-1 px-2 border rounded\"\n />\n
\n
\n
\n )\n});\n\n// Use in component definition:\nfieldOverrides: {\n brandColor: customColorFieldOverride,\n className: (layer) => classNameFieldOverrides(layer)\n}\n```\n\n## Default Children\n\nConfigure default child components that appear when users add components to the canvas.\n\n### Simple Text Default Children\n\n```tsx\nspan: {\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n fieldOverrides: commonFieldOverrides(),\n defaultChildren: \"Default text content\"\n}\n```\n\n### Component Layer Default Children\n\nFor complex nested structures:\n\n```tsx\nButton: {\n component: Button,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n variant: z.enum(['default', 'destructive']).default('default'),\n }),\n from: '@/components/ui/button',\n defaultChildren: [\n {\n id: \"button-text\",\n type: \"span\",\n name: \"Button Text\",\n props: {},\n children: \"Click me\",\n }\n ],\n fieldOverrides: commonFieldOverrides()\n}\n```\n\n### Complex Nested Structures\n\n```tsx\nCard: {\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: \"div\",\n name: \"Header\",\n props: { className: \"p-6 pb-0\" },\n children: [\n {\n id: \"card-title\",\n type: \"span\",\n name: \"Title\",\n props: { className: \"text-2xl font-semibold\" },\n children: \"Card Title\"\n }\n ]\n },\n {\n id: \"card-content\",\n type: \"div\",\n name: \"Content\",\n props: { className: \"p-6\" },\n children: [\n {\n id: \"card-text\",\n type: \"span\",\n name: \"Text\",\n props: {},\n children: \"Card content goes here.\"\n }\n ]\n }\n ],\n fieldOverrides: commonFieldOverrides()\n}\n```\n\n## Default Variable Bindings\n\nAutomatically bind component properties to variables when components are added to the canvas.\n\n### Basic Variable Bindings\n\n```tsx\nUserProfile: {\n component: UserProfile,\n schema: z.object({\n userId: z.string().default(''),\n displayName: z.string().default('Anonymous'),\n email: z.string().optional(),\n }),\n from: '@/components/ui/user-profile',\n defaultVariableBindings: [\n {\n propName: 'userId',\n variableId: 'current-user-id',\n immutable: true // System data - cannot be unbound\n },\n {\n propName: 'displayName',\n variableId: 'current-user-name', \n immutable: false // Can be customized\n }\n ]\n}\n```\n\n### Immutable Bindings for Brand Consistency\n\n```tsx\nBrandedButton: {\n component: BrandedButton,\n schema: z.object({\n text: z.string().default('Click me'),\n brandColor: z.string().default('#000000'),\n companyName: z.string().default('Company'),\n }),\n from: '@/components/ui/branded-button',\n defaultVariableBindings: [\n {\n propName: 'brandColor',\n variableId: 'primary-brand-color',\n immutable: true // Prevents breaking brand guidelines\n },\n {\n propName: 'companyName',\n variableId: 'company-name',\n immutable: true // Consistent branding\n }\n // 'text' is not bound, allowing content customization\n ]\n}\n```\n\n## Advanced Patterns\n\n### Conditional Field Overrides\n\n```tsx\nconst conditionalFieldOverride = (layer) => ({\n isHidden: (currentValues) => currentValues.mode === 'simple',\n fieldType: ({ label, field }) => (\n \n {label}\n \n \n \n \n )\n});\n```\n\n### Dynamic Default Children\n\nWhile UI Builder doesn't support function-based default children, you can create multiple component variants:\n\n```tsx\nSimpleCard: {\n // ... basic card with minimal default children\n},\nRichCard: {\n // ... card with comprehensive default structure\n}\n```\n\n## Best Practices\n\n### Field Overrides\n- **Always override `className`** with `classNameFieldOverrides()` for consistent Tailwind editing\n- **Always override `children`** with appropriate children override based on content type\n- **Create specialized overrides** for domain-specific data types (colors, icons, etc.)\n- **Test thoroughly** to ensure overrides work as expected in the editor\n\n### Default Children\n- **Provide meaningful defaults** that demonstrate proper component usage\n- **Keep structures shallow** to avoid overwhelming new users\n- **Use unique IDs** to prevent conflicts when components are duplicated\n- **Include all dependencies** - ensure referenced component types are in your registry\n\n### Variable Bindings\n- **Use immutable bindings** for system data that shouldn't be modified\n- **Use immutable bindings** for brand consistency (colors, logos, names)\n- **Leave content unbound** so editors can customize text and messaging\n- **Group related variables** logically in your variable definitions\n\n### Performance\n- **Use `commonFieldOverrides()`** when you need standard className/children handling\n- **Memoize expensive field overrides** if they perform complex calculations\n- **Keep default children structures reasonable** to avoid slow initial renders" + "children": "## Field Overrides\n\nField overrides replace auto-generated form fields with specialized input controls. Instead of basic text inputs, users get rich editors, color pickers, icon selectors, and more.\n\n### Available Built-in Field Overrides\n\n#### `classNameFieldOverrides(layer)`\nAdvanced Tailwind CSS class editor with:\n- Auto-complete for Tailwind classes\n- Responsive breakpoint controls (sm:, md:, lg:, xl:)\n- Visual class grouping and validation\n- Theme-aware suggestions\n\n```tsx\nfieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer)\n}\n```\n\n#### `childrenFieldOverrides(layer)`\nSearchable component selector for child components:\n- Dropdown with available component types\n- Search and filter capabilities\n- Respects component hierarchy\n\n```tsx\nfieldOverrides: {\n children: (layer) => childrenFieldOverrides(layer)\n}\n```\n\n#### `childrenAsTextareaFieldOverrides(layer)`\nMulti-line text editor for simple text content:\n- Perfect for span, p, and heading elements\n- Multi-line editing support\n\n```tsx\nfieldOverrides: {\n children: (layer) => childrenAsTextareaFieldOverrides(layer)\n}\n```\n\n#### `childrenAsTipTapFieldOverrides(layer)`\nRich text editor using TipTap:\n- WYSIWYG markdown editing\n- Formatting toolbar with bold, italic, links\n- Ideal for Markdown components\n\n```tsx\nfieldOverrides: {\n children: (layer) => childrenAsTipTapFieldOverrides(layer)\n}\n```\n\n#### `iconNameFieldOverrides(layer)`\nVisual icon picker:\n- Grid of available icons\n- Search functionality\n- Live preview\n\n```tsx\nfieldOverrides: {\n iconName: (layer) => iconNameFieldOverrides(layer)\n}\n```\n\n#### `textInputFieldOverrides(layer, allowVariableBinding, propName)`\nEnhanced text input with optional variable binding support:\n- Variable binding UI when `allowVariableBinding` is true\n- Automatic binding/unbinding controls\n- Immutable binding badges\n\n```tsx\nfieldOverrides: {\n title: (layer) => textInputFieldOverrides(layer, true, 'title')\n}\n```\n\n#### `commonFieldOverrides()`\nConvenience function that applies standard overrides for `className` and `children`:\n\n```tsx\nfieldOverrides: commonFieldOverrides()\n// Equivalent to:\n// {\n// className: (layer) => classNameFieldOverrides(layer),\n// children: (layer) => childrenFieldOverrides(layer)\n// }\n```\n\n### Creating Custom Field Overrides\n\nCreate specialized input controls for unique data types:\n\n```tsx\nimport { AutoFormInputComponentProps } from '@/components/ui/ui-builder/types';\nimport { FormItem, FormLabel, FormControl } from '@/components/ui/form';\n\nconst colorPickerFieldOverride = (layer) => ({\n fieldType: ({ label, field }: AutoFormInputComponentProps) => (\n \n {label}\n \n
\n field.onChange(e.target.value)}\n className=\"w-12 h-8 rounded border cursor-pointer\"\n />\n field.onChange(e.target.value)}\n placeholder=\"#000000\"\n className=\"flex-1 px-3 py-2 border rounded-md\"\n />\n
\n
\n
\n )\n});\n\n// Use in component definition:\nfieldOverrides: {\n brandColor: colorPickerFieldOverride,\n className: (layer) => classNameFieldOverrides(layer)\n}\n```\n\n### Conditional Field Overrides\n\nHide or show fields based on other prop values:\n\n```tsx\nconst conditionalFieldOverride = (layer) => ({\n isHidden: (currentValues) => currentValues.mode === 'simple',\n fieldType: ({ label, field }) => (\n \n {label}\n \n \n \n \n )\n});\n```\n\n## Default Children\n\nProvide sensible default child components when users add your component to the canvas.\n\n### Simple Text Defaults\n\nFor basic text components:\n\n```tsx\nspan: {\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: \"Default text content\"\n}\n```\n\n### Component Layer Defaults\n\nFor complex nested structures:\n\n```tsx\nButton: {\n component: Button,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n variant: z.enum(['default', 'destructive']).default('default'),\n }),\n from: '@/components/ui/button',\n defaultChildren: [\n {\n id: \"button-text\",\n type: \"span\",\n name: \"Button Text\",\n props: {},\n children: \"Click me\",\n }\n ],\n fieldOverrides: commonFieldOverrides()\n}\n```\n\n**Important**: All component types referenced in `defaultChildren` must exist in your registry.\n\n### Rich Default Structures\n\nCreate sophisticated default layouts:\n\n```tsx\nCard: {\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: \"div\",\n name: \"Header\",\n props: { className: \"p-6 pb-0\" },\n children: [\n {\n id: \"card-title\",\n type: \"span\",\n name: \"Title\",\n props: { className: \"text-2xl font-semibold\" },\n children: \"Card Title\"\n }\n ]\n },\n {\n id: \"card-content\",\n type: \"div\",\n name: \"Content\",\n props: { className: \"p-6\" },\n children: \"Card content goes here.\"\n }\n ],\n fieldOverrides: commonFieldOverrides()\n}\n```\n\n## Default Variable Bindings\n\nAutomatically bind component properties to variables when components are added to the canvas. Perfect for system data, branding, and user information.\n\n### Basic Variable Bindings\n\n```tsx\nUserProfile: {\n component: UserProfile,\n schema: z.object({\n userId: z.string().default(''),\n displayName: z.string().default('Anonymous'),\n email: z.string().optional(),\n }),\n from: '@/components/ui/user-profile',\n defaultVariableBindings: [\n {\n propName: 'userId',\n variableId: 'current_user_id',\n immutable: true // System data - cannot be unbound\n },\n {\n propName: 'displayName',\n variableId: 'current_user_name', \n immutable: false // Can be customized\n }\n ],\n fieldOverrides: {\n userId: (layer) => textInputFieldOverrides(layer, true, 'userId'),\n displayName: (layer) => textInputFieldOverrides(layer, true, 'displayName'),\n email: (layer) => textInputFieldOverrides(layer, true, 'email'),\n }\n}\n```\n\n### Immutable Bindings for Critical Data\n\nUse `immutable: true` to prevent users from unbinding critical data:\n\n```tsx\nBrandedButton: {\n component: BrandedButton,\n schema: z.object({\n text: z.string().default('Click me'),\n brandColor: z.string().default('#000000'),\n companyName: z.string().default('Company'),\n }),\n from: '@/components/ui/branded-button',\n defaultVariableBindings: [\n {\n propName: 'brandColor',\n variableId: 'primary_brand_color',\n immutable: true // Prevents breaking brand guidelines\n },\n {\n propName: 'companyName',\n variableId: 'company_name',\n immutable: true // Consistent branding\n }\n // 'text' is not bound, allowing content customization\n ],\n fieldOverrides: {\n text: (layer) => textInputFieldOverrides(layer, true, 'text'),\n brandColor: (layer) => textInputFieldOverrides(layer, true, 'brandColor'),\n companyName: (layer) => textInputFieldOverrides(layer, true, 'companyName'),\n }\n}\n```\n\n**When to Use Immutable Bindings:**\n- **System data**: User IDs, tenant IDs, system versions\n- **Brand consistency**: Colors, logos, company names\n- **Security**: Roles, permissions, access levels\n- **Template integrity**: Critical variables in white-label scenarios\n\n### Variable Binding UI\n\nWhen using `textInputFieldOverrides` with variable binding enabled, users see:\n- **🔗 Bind Variable** button for unbound properties\n- **Variable name, type, and default value** for bound properties\n- **🔒 Immutable badge** for protected bindings\n- **Unbind button** for mutable bindings only\n\n## Live Example\n\nSee these advanced configuration techniques in action:" + }, + { + "id": "immutable-bindings-example", + "type": "div", + "name": "div", + "props": {}, + "children": [ + { + "id": "example-badge", + "type": "Badge", + "name": "Badge", + "props": { + "variant": "default", + "className": "rounded rounded-b-none" + }, + "children": [ + { + "id": "example-badge-text", + "type": "span", + "name": "span", + "props": {}, + "children": "Live Demo: Immutable Bindings" + } + ] + }, + { + "id": "example-demo", + "type": "div", + "name": "div", + "props": { + "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" + }, + "children": [ + { + "id": "example-iframe", + "type": "iframe", + "name": "iframe", + "props": { + "src": "/examples/editor/immutable-bindings", + "title": "UI Builder Immutable Bindings Demo", + "className": "w-full aspect-video" + }, + "children": [] + } + ] + } + ] + }, + { + "id": "advanced-patterns-content", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Best Practices\n\n### Field Overrides\n- **Always override `className`** with `classNameFieldOverrides()` for consistent Tailwind editing\n- **Choose appropriate children overrides** based on content type:\n - `childrenAsTextareaFieldOverrides()` for simple text (span, p, headings)\n - `childrenFieldOverrides()` for nested components (div, containers)\n - `childrenAsTipTapFieldOverrides()` for rich text (Markdown components)\n- **Create domain-specific overrides** for complex data types (colors, dates, files)\n- **Use conditional overrides** to hide advanced options in simple mode\n\n### Default Children\n- **Provide meaningful defaults** that demonstrate proper component usage\n- **Keep structures shallow initially** to avoid overwhelming new users\n- **Use descriptive names** for child layers to help with navigation\n- **Include all dependencies** - ensure referenced component types are in your registry\n- **Use unique IDs** to prevent conflicts when components are duplicated\n\n### Variable Bindings\n- **Use immutable bindings** for:\n - System data (user IDs, system versions)\n - Brand elements (colors, logos, company names)\n - Security-sensitive data (roles, permissions)\n - Template integrity variables\n- **Leave content variables mutable** so editors can customize text and messaging\n- **Combine with field overrides** using `textInputFieldOverrides()` for the best UX\n- **Test binding scenarios** to ensure the editing experience is smooth\n\n### Performance Tips\n- **Use `commonFieldOverrides()`** when you need standard className/children handling\n- **Memoize expensive overrides** if they perform complex calculations\n- **Keep default children reasonable** to avoid slow initial renders\n- **Cache field override functions** to prevent unnecessary re-renders\n\n## Integration with Other Features\n\nThese advanced configuration techniques work seamlessly with:\n- **Variables Panel** - Manage variables that power your bindings\n- **Props Panel** - Enhanced forms with your custom field overrides\n- **Code Generation** - Exported code respects your component definitions\n- **LayerRenderer** - Variable values are resolved at render time\n\n## What's Next?\n\nWith advanced configuration mastered, explore:\n- **Variables** - Create dynamic, data-driven interfaces\n- **Panel Configuration** - Customize the editor panels themselves\n- **Persistence** - Save and load your enhanced component configurations\n\nThese advanced techniques transform UI Builder from a simple visual editor into a powerful, domain-specific design tool tailored to your exact needs." } ] } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/immutable-pages.ts b/app/docs/docs-data/docs-page-layers/immutable-pages.ts deleted file mode 100644 index 2842c4f..0000000 --- a/app/docs/docs-data/docs-page-layers/immutable-pages.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { ComponentLayer } from "@/components/ui/ui-builder/types"; - -export const IMMUTABLE_PAGES_LAYER = { - "id": "immutable-pages", - "type": "div", - "name": "Immutable Pages", - "props": { - "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", - "data-group": "editor-features" - }, - "children": [ - { - "type": "span", - "children": "Immutable Pages", - "id": "immutable-pages-title", - "name": "Text", - "props": { - "className": "text-4xl" - } - }, - { - "id": "immutable-pages-intro", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "Control page creation and deletion permissions to create template-based editing experiences. Perfect for maintaining approved layouts while allowing content customization, or creating read-only preview modes." - }, - { - "id": "immutable-pages-demo", - "type": "div", - "name": "div", - "props": {}, - "children": [ - { - "id": "immutable-pages-badge", - "type": "Badge", - "name": "Badge", - "props": { - "variant": "default", - "className": "rounded rounded-b-none" - }, - "children": [ - { - "id": "immutable-pages-badge-text", - "type": "span", - "name": "span", - "props": {}, - "children": "Controlled Editing" - } - ] - }, - { - "id": "immutable-pages-demo-frame", - "type": "div", - "name": "div", - "props": { - "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" - }, - "children": [ - { - "id": "immutable-pages-iframe", - "type": "iframe", - "name": "iframe", - "props": { - "src": "http://localhost:3000/examples/editor/immutable-pages", - "title": "Immutable Pages Demo", - "className": "aspect-square md:aspect-video w-full" - }, - "children": [] - } - ] - } - ] - }, - { - "id": "immutable-pages-content", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "## Page Control Props\n\nUI Builder provides props to control page creation and deletion permissions:\n\n```tsx\n\n```\n\n## Use Cases\n\n### Template-Based Editing\n\nProvide pre-built page templates that users can customize but not restructure:\n\n```tsx\n// Pre-defined page templates\nconst templatePages = [\n {\n id: 'homepage-template',\n type: 'div',\n name: 'Homepage',\n props: {\n className: 'min-h-screen bg-background'\n },\n children: [\n {\n id: 'hero-section',\n type: 'div',\n name: 'Hero Section',\n props: {\n className: 'py-20 text-center'\n },\n children: [\n {\n id: 'hero-title',\n type: 'span',\n name: 'Hero Title',\n props: {\n className: 'text-4xl font-bold',\n children: { __variableRef: 'hero-title-var' }\n },\n children: []\n }\n ]\n }\n ]\n },\n {\n id: 'about-template',\n type: 'div', \n name: 'About Page',\n props: {\n className: 'min-h-screen bg-background p-8'\n },\n children: [\n // Pre-structured about page content\n ]\n }\n];\n\n// Variables for customization\nconst templateVariables = [\n { id: 'hero-title-var', name: 'heroTitle', type: 'string', defaultValue: 'Welcome to Our Site' },\n { id: 'company-name-var', name: 'companyName', type: 'string', defaultValue: 'Your Company' }\n];\n\n\n```\n\n### White-Label Solutions\n\nCreate branded templates for different clients:\n\n```tsx\nconst ClientTemplateBuilder = ({ clientConfig }) => {\n const clientPages = [\n {\n id: 'client-homepage',\n type: 'div',\n name: 'Homepage',\n props: {\n className: 'min-h-screen',\n style: {\n '--brand-color': clientConfig.brandColor\n }\n },\n children: [\n // Client-specific template structure\n ]\n }\n ];\n \n const clientVariables = [\n { id: 'client-name', name: 'clientName', type: 'string', defaultValue: clientConfig.name },\n { id: 'client-logo', name: 'clientLogo', type: 'string', defaultValue: clientConfig.logoUrl },\n { id: 'contact-email', name: 'contactEmail', type: 'string', defaultValue: clientConfig.email }\n ];\n \n return (\n saveClientPages(clientConfig.id, pages)}\n onVariablesChange={(vars) => saveClientVariables(clientConfig.id, vars)}\n />\n );\n};\n```\n\n### Content-Only Editing\n\nAllow content updates without structural changes:\n\n```tsx\n// Blog template with fixed structure\nconst blogTemplate = [\n {\n id: 'blog-page',\n type: 'article',\n name: 'Blog Post',\n props: {\n className: 'max-w-4xl mx-auto py-8 px-4'\n },\n children: [\n {\n id: 'blog-header',\n type: 'header',\n name: 'Post Header',\n props: { className: 'mb-8' },\n children: [\n {\n id: 'blog-title',\n type: 'span',\n name: 'Post Title',\n props: {\n className: 'text-3xl font-bold mb-4 block',\n children: { __variableRef: 'post-title' }\n },\n children: []\n },\n {\n id: 'blog-meta',\n type: 'div',\n name: 'Post Meta',\n props: { className: 'text-sm text-muted-foreground' },\n children: [\n {\n id: 'publish-date',\n type: 'span',\n name: 'Publish Date',\n props: {\n children: { __variableRef: 'publish-date' }\n },\n children: []\n }\n ]\n }\n ]\n },\n {\n id: 'blog-content',\n type: 'div',\n name: 'Post Content',\n props: {\n className: 'prose prose-lg max-w-none',\n children: { __variableRef: 'post-content' }\n },\n children: []\n }\n ]\n }\n];\n\nconst contentVariables = [\n { id: 'post-title', name: 'postTitle', type: 'string', defaultValue: 'New Blog Post' },\n { id: 'publish-date', name: 'publishDate', type: 'string', defaultValue: 'January 1, 2024' },\n { id: 'post-content', name: 'postContent', type: 'string', defaultValue: 'Write your blog post content here...' }\n];\n\n\n```\n\n## Read-Only Preview Mode\n\nCreate completely read-only experiences for previewing:\n\n```tsx\nconst PreviewOnlyBuilder = ({ pageData, variables }) => {\n // Custom read-only props panel\n const ReadOnlyPropsPanel = ({ className }) => (\n
\n

Preview Mode

\n

\n This is a read-only preview. Changes cannot be made.\n

\n \n {/* Show selected component info */}\n \n \n {/* Show variable values */}\n
\n

Variables

\n
\n {variables.map(variable => (\n
\n {variable.name}:\n {variable.defaultValue}\n
\n ))}\n
\n
\n
\n );\n \n // Custom navigation for preview mode\n const PreviewNavBar = () => (\n \n );\n \n return (\n ,\n propsPanel: \n }}\n />\n );\n};\n```\n\n## Role-Based Permissions\n\nImplement different permission levels based on user roles:\n\n```tsx\nconst getRoleBasedConfig = (userRole) => {\n switch (userRole) {\n case 'admin':\n return {\n allowPagesCreation: true,\n allowPagesDeletion: true,\n allowVariableEditing: true\n };\n \n case 'editor':\n return {\n allowPagesCreation: false, // Can't change structure\n allowPagesDeletion: false,\n allowVariableEditing: true // Can edit content\n };\n \n case 'content-manager':\n return {\n allowPagesCreation: false,\n allowPagesDeletion: false,\n allowVariableEditing: true // Content-only editing\n };\n \n case 'viewer':\n return {\n allowPagesCreation: false,\n allowPagesDeletion: false,\n allowVariableEditing: false // Read-only\n };\n \n default:\n return {\n allowPagesCreation: false,\n allowPagesDeletion: false,\n allowVariableEditing: false\n };\n }\n};\n\nconst RoleBasedBuilder = ({ user, pages, variables }) => {\n const permissions = getRoleBasedConfig(user.role);\n \n return (\n \n );\n};\n```\n\n## Progressive Enhancement\n\nStart with limited permissions and unlock features based on user progression:\n\n```tsx\nconst ProgressiveBuilder = ({ userLevel, achievements }) => {\n const canCreatePages = userLevel >= 3 || achievements.includes('page-master');\n const canDeletePages = userLevel >= 5 || achievements.includes('admin');\n const canEditVariables = userLevel >= 1;\n \n return (\n \n
\n Level {userLevel} Builder\n
\n {achievements.map(achievement => (\n \n {achievement}\n \n ))}\n
\n
\n \n
\n {!canCreatePages && 'Unlock page creation at Level 3'}\n
\n
\n )\n }}\n />\n );\n};\n```\n\n## Best Practices\n\n### Template Design\n- **Create comprehensive templates** that cover all necessary content areas\n- **Use meaningful variable names** that content editors will understand\n- **Provide sensible defaults** for all variables\n- **Test templates** with real content scenarios\n\n### Permission Strategy\n- **Start restrictive** and gradually unlock features\n- **Clearly communicate** what users can and cannot do\n- **Provide upgrade paths** for users who need more permissions\n- **Document permission levels** for team understanding\n\n### User Experience\n- **Show permission status** clearly in the UI\n- **Provide helpful messages** when actions are restricted\n- **Focus on enabled capabilities** rather than disabled ones\n- **Offer alternative paths** for restricted actions\n\n### Content Management\n- **Separate structure from content** using variables effectively\n- **Version control templates** separately from content\n- **Provide content guidelines** for variable editing\n- **Monitor content quality** with validation and review processes" - } - ] - } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/introduction.ts b/app/docs/docs-data/docs-page-layers/introduction.ts index 6578a1e..2a868d8 100644 --- a/app/docs/docs-data/docs-page-layers/introduction.ts +++ b/app/docs/docs-data/docs-page-layers/introduction.ts @@ -45,7 +45,7 @@ export const INTRODUCTION_LAYER = { "type": "span", "name": "span", "props": {}, - "children": "Example" + "children": "Live Demo" } ] }, @@ -62,8 +62,8 @@ export const INTRODUCTION_LAYER = { "type": "iframe", "name": "iframe", "props": { - "src": "http://localhost:3000/examples/basic", - "title": "", + "src": "/examples/basic", + "title": "UI Builder Basic Example", "className": "aspect-square md:aspect-video" }, "children": [] @@ -77,7 +77,35 @@ export const INTRODUCTION_LAYER = { "type": "Markdown", "name": "Markdown", "props": {}, - "children": "### How it unlocks novel product features:\n\n- **Give users no‑code superpowers** — add a full visual builder to your SaaS with one install\n- **Design with components you already ship** — nothing new to build or maintain\n- **Store layouts as human‑readable JSON** — render inside your product to ship changes immediately\n- **Create dynamic, data-driven interfaces** — bind component properties to variables for personalized content\n\n### Key Benefits\n\n1. **One‑step installation**\\\n Get up and running with a single `npx shadcn@latest add …` command.\n\n2. **Figma‑style editing**\\\n Intuitive drag‑and‑drop canvas, properties panel, and live preview.\n\n3. **Full React code export**\\\n Generate clean, type‑safe React code that matches your project structure.\n\n4. **Runtime variable binding**\\\n Create dynamic templates with string, number, and boolean variables—perfect for personalization, A/B testing, or multi‑tenant branding.\n\n### 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." + "children": "## How UI Builder Works\n\nUI Builder empowers you to visually construct and modify user interfaces by leveraging your own React components. Here's how it works:\n\n**🧩 Component-Driven Foundation**\\\nOperates on your existing React components. You provide a `componentRegistry` detailing which components are available in the editor.\n\n**🎨 Layer-Based Canvas**\\\nThe UI is constructed as a tree of \"layers.\" Each layer represents a component instance that users can visually add, remove, reorder, and nest on an interactive canvas.\n\n**⚙️ Dynamic Props Editing**\\\nEach component uses a Zod schema to automatically generate a properties panel, allowing users to configure component instances in real-time.\n\n**🔗 Variable-Driven Dynamic Content**\\\nVariables transform static designs into dynamic, data-driven interfaces. Bind component properties to typed variables for personalization, theming, and reusable templates.\n\n**📦 Flexible State Management**\\\nBy default, the editor's state persists in local storage. For production apps, provide `initialLayers` and use the `onChange` callback to persist state to your backend.\n\n**⚡ React Code Generation**\\\nExport visually designed pages as clean, readable React code that correctly references your components.\n\n**🚀 Runtime Variable Resolution**\\\nWhen rendering pages with `LayerRenderer`, provide `variableValues` to override defaults with real data from APIs, databases, or user input." + }, + { + "id": "understanding-variables", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Understanding Variables\n\n**Variables are the key to creating dynamic, data-driven interfaces.** Instead of hardcoding static values, variables allow you to bind component properties to dynamic data that changes at runtime.\n\n**Variable Types:**\n- **String**: For text content, names, descriptions, etc.\n- **Number**: For counts, ages, prices, quantities, etc.\n- **Boolean**: For feature flags, visibility toggles, active states, etc.\n\n**Powerful Use Cases:**\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\n- **Content management** where non‑technical users can update dynamic content" + }, + { + "id": "key-benefits", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Key Benefits\n\n**🎯 One‑step Installation**\\\nGet up and running with a single `npx shadcn@latest add …` command.\n\n**🎨 Figma‑style Editing**\\\nIntuitive drag‑and‑drop canvas, properties panel, and live preview.\n\n**⚡ Full React Code Export**\\\nGenerate clean, type‑safe React code that matches your project structure.\n\n**🔗 Runtime Variable Binding**\\\nCreate dynamic templates with string, number, and boolean variables—perfect for personalization, A/B testing, or multi‑tenant branding.\n\n**🧩 Bring Your Own Components**\\\nUse your existing React component library—no need to rebuild from scratch.\n\n**💾 Flexible Persistence**\\\nControl how and when editor state is saved, with built‑in local storage support or custom database integration." + }, + { + "id": "live-examples", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Live Examples\n\nExplore different UI Builder features with these interactive examples:\n\n**🎨 [Basic Editor](/examples/basic)** - Simple drag‑and‑drop interface with basic components\n\n**🔧 [Full Featured Editor](/examples/editor)** - Complete editor with all panels and advanced features\n\n**📄 [Static Renderer](/examples/renderer)** - See how pages render without the editor interface\n\n**🔗 [Variables in Action](/examples/renderer/variables)** - Dynamic content with runtime variable binding" + }, + { + "id": "next-steps", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Next Steps\n\nReady to get started?\n\n1. **Quick Start** - Install and set up your first UI Builder\n2. **Components Intro** - Learn about the component registry system\n3. **Variables** - Create dynamic, data-driven interfaces\n4. **Custom Components** - Add your own React components to the editor" } ] } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/layer-structure.ts b/app/docs/docs-data/docs-page-layers/layer-structure.ts index 4ce97a7..09738c0 100644 --- a/app/docs/docs-data/docs-page-layers/layer-structure.ts +++ b/app/docs/docs-data/docs-page-layers/layer-structure.ts @@ -23,14 +23,56 @@ export const LAYER_STRUCTURE_LAYER = { "type": "Markdown", "name": "Markdown", "props": {}, - "children": "Understanding the layer structure is fundamental to working with UI Builder. Layers define the hierarchical component tree that powers the visual editor and rendering system." + "children": "Understanding layer structure is fundamental to working with UI Builder. Layers define the hierarchical component tree that powers both the visual editor and the rendering system." }, { - "id": "layer-structure-content", + "id": "layer-structure-interface", "type": "Markdown", "name": "Markdown", "props": {}, - "children": "## Layer Schema\n\n### Basic Layer Structure\n```tsx\ninterface ComponentLayer {\n id: string; // Unique identifier\n type: string; // Component type (Button, div, etc.)\n name: string; // Display name in editor\n props: Record; // Component properties\n children: ComponentLayer[] | string; // Child layers or text content\n}\n```\n\n### Example Layer\n```tsx\nconst buttonLayer: ComponentLayer = {\n id: 'button-1',\n type: 'Button',\n name: 'Primary Button',\n props: {\n variant: 'primary',\n size: 'lg',\n className: 'w-full'\n },\n children: 'Click me'\n};\n```\n\n## Hierarchical Structure\n\n### Parent-Child Relationships\n```tsx\nconst cardLayer: ComponentLayer = {\n id: 'card-1',\n type: 'Card',\n name: 'Product Card',\n props: {\n className: 'p-6 border rounded-lg'\n },\n children: [\n {\n id: 'card-header',\n type: 'div',\n name: 'Header',\n props: { className: 'mb-4' },\n children: [\n {\n id: 'title',\n type: 'h3',\n name: 'Title',\n props: { className: 'text-lg font-semibold' },\n children: 'Product Name'\n }\n ]\n },\n {\n id: 'card-content',\n type: 'div',\n name: 'Content',\n props: { className: 'space-y-2' },\n children: [\n {\n id: 'description',\n type: 'p',\n name: 'Description',\n props: { className: 'text-gray-600' },\n children: 'Product description here'\n },\n {\n id: 'price',\n type: 'span',\n name: 'Price',\n props: { className: 'text-xl font-bold' },\n children: '$99.99'\n }\n ]\n }\n ]\n};\n```\n\n## Layer Types\n\n### Container Layers\n```tsx\n// Layout containers\nconst flexContainer: ComponentLayer = {\n id: 'flex-1',\n type: 'div',\n name: 'Flex Container',\n props: {\n className: 'flex items-center justify-between'\n },\n children: []\n};\n\nconst gridContainer: ComponentLayer = {\n id: 'grid-1',\n type: 'div',\n name: 'Grid Container',\n props: {\n className: 'grid grid-cols-3 gap-4'\n },\n children: []\n};\n```\n\n### Content Layers\n```tsx\n// Text content\nconst textLayer: ComponentLayer = {\n id: 'text-1',\n type: 'p',\n name: 'Paragraph',\n props: {\n className: 'text-base leading-relaxed'\n },\n children: 'Lorem ipsum dolor sit amet...'\n};\n\n// Rich content\nconst markdownLayer: ComponentLayer = {\n id: 'markdown-1',\n type: 'Markdown',\n name: 'Markdown Content',\n props: {},\n children: '# Heading\\n\\nThis is **bold** text.'\n};\n```\n\n### Interactive Layers\n```tsx\n// Form inputs\nconst inputLayer: ComponentLayer = {\n id: 'input-1',\n type: 'Input',\n name: 'Email Input',\n props: {\n type: 'email',\n placeholder: 'Enter your email',\n required: true\n },\n children: []\n};\n\n// Buttons\nconst buttonLayer: ComponentLayer = {\n id: 'button-1',\n type: 'Button',\n name: 'Submit Button',\n props: {\n type: 'submit',\n variant: 'primary'\n },\n children: 'Submit'\n};\n```\n\n## Layer Properties\n\n### Standard Props\n```tsx\ninterface LayerProps {\n // Styling\n className?: string;\n style?: React.CSSProperties;\n \n // Data binding\n 'data-*'?: string;\n \n // Event handlers\n onClick?: () => void;\n onChange?: (value: any) => void;\n \n // Accessibility\n 'aria-*'?: string;\n role?: string;\n tabIndex?: number;\n \n // Component-specific props\n [key: string]: any;\n}\n```\n\n### Dynamic Props\n```tsx\n// Variable-bound props\nconst dynamicLayer: ComponentLayer = {\n id: 'dynamic-1',\n type: 'Button',\n name: 'Dynamic Button',\n props: {\n disabled: '${isLoading}',\n className: '${buttonStyle}',\n children: '${buttonText}'\n },\n children: []\n};\n```\n\n## Layer Manipulation\n\n### Adding Layers\n```tsx\nfunction addLayer(parentId: string, newLayer: ComponentLayer) {\n const parent = findLayerById(parentId);\n if (parent && Array.isArray(parent.children)) {\n parent.children.push(newLayer);\n }\n}\n```\n\n### Removing Layers\n```tsx\nfunction removeLayer(layerId: string) {\n const parent = findParentLayer(layerId);\n if (parent && Array.isArray(parent.children)) {\n parent.children = parent.children.filter(child => \n typeof child === 'object' && child.id !== layerId\n );\n }\n}\n```\n\n### Moving Layers\n```tsx\nfunction moveLayer(\n layerId: string, \n newParentId: string, \n index?: number\n) {\n const layer = findLayerById(layerId);\n const newParent = findLayerById(newParentId);\n \n // Remove from current parent\n removeLayer(layerId);\n \n // Add to new parent\n if (newParent && Array.isArray(newParent.children)) {\n if (index !== undefined) {\n newParent.children.splice(index, 0, layer);\n } else {\n newParent.children.push(layer);\n }\n }\n}\n```\n\n### Duplicating Layers\n```tsx\nfunction duplicateLayer(layerId: string): ComponentLayer {\n const original = findLayerById(layerId);\n \n function deepClone(layer: ComponentLayer): ComponentLayer {\n return {\n ...layer,\n id: generateUniqueId(),\n children: Array.isArray(layer.children)\n ? layer.children.map(child => \n typeof child === 'string' ? child : deepClone(child)\n )\n : layer.children\n };\n }\n \n return deepClone(original);\n}\n```\n\n## Layer Validation\n\n### Schema Validation\n```tsx\nfunction validateLayer(layer: ComponentLayer): ValidationResult {\n const errors: string[] = [];\n \n // Required fields\n if (!layer.id) errors.push('Layer must have an id');\n if (!layer.type) errors.push('Layer must have a type');\n if (!layer.name) errors.push('Layer must have a name');\n \n // ID uniqueness\n if (isDuplicateId(layer.id)) {\n errors.push(`Duplicate layer id: ${layer.id}`);\n }\n \n // Component type validation\n if (!isValidComponentType(layer.type)) {\n errors.push(`Invalid component type: ${layer.type}`);\n }\n \n // Recursive validation for children\n if (Array.isArray(layer.children)) {\n layer.children.forEach(child => {\n if (typeof child === 'object') {\n const childValidation = validateLayer(child);\n errors.push(...childValidation.errors);\n }\n });\n }\n \n return {\n isValid: errors.length === 0,\n errors\n };\n}\n```\n\n## Layer Utilities\n\n### Tree Traversal\n```tsx\nfunction traverseLayer(\n layer: ComponentLayer,\n callback: (layer: ComponentLayer, depth: number) => void,\n depth = 0\n) {\n callback(layer, depth);\n \n if (Array.isArray(layer.children)) {\n layer.children.forEach(child => {\n if (typeof child === 'object') {\n traverseLayer(child, callback, depth + 1);\n }\n });\n }\n}\n```\n\n### Layer Search\n```tsx\nfunction findLayerById(layerId: string, root: ComponentLayer): ComponentLayer | null {\n if (root.id === layerId) return root;\n \n if (Array.isArray(root.children)) {\n for (const child of root.children) {\n if (typeof child === 'object') {\n const found = findLayerById(layerId, child);\n if (found) return found;\n }\n }\n }\n \n return null;\n}\n\nfunction findLayersByType(type: string, root: ComponentLayer): ComponentLayer[] {\n const results: ComponentLayer[] = [];\n \n traverseLayer(root, (layer) => {\n if (layer.type === type) {\n results.push(layer);\n }\n });\n \n return results;\n}\n```\n\n## Best Practices\n\n- **Unique IDs** - Ensure every layer has a unique identifier\n- **Meaningful Names** - Use descriptive names for editor clarity\n- **Proper Nesting** - Follow semantic HTML structure where possible\n- **Consistent Props** - Use consistent property naming conventions\n- **Validation** - Always validate layer structure before rendering\n- **Performance** - Keep layer trees reasonably shallow for performance" + "children": "## ComponentLayer Interface\n\nEvery element in UI Builder is represented as a `ComponentLayer` with this structure:\n\n```tsx\ninterface ComponentLayer {\n id: string; // Unique identifier\n type: string; // Component type from registry\n name?: string; // Optional display name for editor\n props: Record; // Component properties\n children: ComponentLayer[] | string; // Child layers or text content\n}\n```\n\n### Core Fields\n\n- **`id`**: Required unique identifier for each layer\n- **`type`**: Must match a key in your component registry (e.g., 'Button', 'div', 'Card')\n- **`name`**: Optional display name shown in the layers panel\n- **`props`**: Object containing all component properties (className, variant, etc.)\n- **`children`**: Either an array of child layers or a string for text content" + }, + { + "id": "layer-structure-examples", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Basic Layer Examples\n\n### Simple Text Layer\n```tsx\nconst textLayer: ComponentLayer = {\n id: 'heading-1',\n type: 'h1',\n name: 'Page Title',\n props: {\n className: 'text-3xl font-bold text-center'\n },\n children: 'Welcome to My App'\n};\n```\n\n### Button with Icon\n```tsx\nconst buttonLayer: ComponentLayer = {\n id: 'cta-button',\n type: 'Button',\n name: 'CTA Button',\n props: {\n variant: 'default',\n size: 'lg',\n className: 'w-full max-w-sm'\n },\n children: [\n {\n id: 'button-text',\n type: 'span',\n name: 'Button Text',\n props: {},\n children: 'Get Started'\n },\n {\n id: 'button-icon',\n type: 'Icon',\n name: 'Arrow Icon',\n props: {\n iconName: 'ArrowRight',\n size: 'medium'\n },\n children: []\n }\n ]\n};\n```" + }, + { + "id": "layer-structure-hierarchy", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Hierarchical Structure\n\nLayers form a tree structure where containers hold other layers:\n\n```tsx\nconst cardLayer: ComponentLayer = {\n id: 'product-card',\n type: 'div',\n name: 'Product Card',\n props: {\n className: 'bg-white rounded-lg shadow-md p-6'\n },\n children: [\n {\n id: 'card-header',\n type: 'div',\n name: 'Header',\n props: { className: 'mb-4' },\n children: [\n {\n id: 'product-title',\n type: 'h3',\n name: 'Product Title',\n props: { className: 'text-xl font-semibold' },\n children: 'Amazing Product'\n },\n {\n id: 'product-badge',\n type: 'Badge',\n name: 'Status Badge',\n props: { variant: 'secondary' },\n children: 'New'\n }\n ]\n },\n {\n id: 'card-content',\n type: 'div',\n name: 'Content',\n props: { className: 'space-y-3' },\n children: [\n {\n id: 'description',\n type: 'p',\n name: 'Description',\n props: { className: 'text-gray-600' },\n children: 'This product will change your life.'\n },\n {\n id: 'price',\n type: 'div',\n name: 'Price Container',\n props: { className: 'flex items-center justify-between' },\n children: [\n {\n id: 'price-text',\n type: 'span',\n name: 'Price',\n props: { className: 'text-2xl font-bold text-green-600' },\n children: '$99.99'\n },\n {\n id: 'buy-button',\n type: 'Button',\n name: 'Buy Button',\n props: { variant: 'default', size: 'sm' },\n children: 'Add to Cart'\n }\n ]\n }\n ]\n }\n ]\n};\n```" + }, + { + "id": "layer-structure-types", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Layer Types\n\n### Container Layers\nLayers that hold and organize other layers:\n\n```tsx\n// Flex container\n{\n id: 'nav-container',\n type: 'div',\n name: 'Navigation',\n props: {\n className: 'flex items-center justify-between p-4'\n },\n children: [/* nav items */]\n}\n\n// Grid container\n{\n id: 'grid-layout',\n type: 'div',\n name: 'Image Grid',\n props: {\n className: 'grid grid-cols-1 md:grid-cols-3 gap-6'\n },\n children: [/* grid items */]\n}\n```\n\n### Content Layers\nLayers that display content:\n\n```tsx\n// Text content\n{\n id: 'paragraph-1',\n type: 'p',\n name: 'Description',\n props: {\n className: 'text-base leading-relaxed'\n },\n children: 'Your content here...'\n}\n\n// Rich content\n{\n id: 'article-content',\n type: 'Markdown',\n name: 'Article Body',\n props: {},\n children: '# Article Title\\n\\nThis is **markdown** content.'\n}\n\n// Images\n{\n id: 'hero-image',\n type: 'img',\n name: 'Hero Image',\n props: {\n src: '/hero.jpg',\n alt: 'Hero image',\n className: 'w-full h-64 object-cover'\n },\n children: []\n}\n```\n\n### Interactive Layers\nLayers that users can interact with:\n\n```tsx\n// Buttons\n{\n id: 'submit-btn',\n type: 'Button',\n name: 'Submit Button',\n props: {\n type: 'submit',\n variant: 'default'\n },\n children: 'Submit Form'\n}\n\n// Form inputs\n{\n id: 'email-input',\n type: 'Input',\n name: 'Email Field',\n props: {\n type: 'email',\n placeholder: 'Enter your email',\n className: 'w-full'\n },\n children: []\n}\n```" + }, + { + "id": "layer-structure-children", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Children Patterns\n\nLayers can have different types of children:\n\n### Text Children\nFor simple text content:\n```tsx\n{\n id: 'simple-text',\n type: 'p',\n name: 'Paragraph',\n props: {},\n children: 'This is simple text content' // string\n}\n```\n\n### Component Children\nFor nested components:\n```tsx\n{\n id: 'container',\n type: 'div',\n name: 'Container',\n props: {},\n children: [ // array of ComponentLayer objects\n {\n id: 'child-1',\n type: 'span',\n name: 'First Child',\n props: {},\n children: 'Hello'\n },\n {\n id: 'child-2', \n type: 'span',\n name: 'Second Child',\n props: {},\n children: 'World'\n }\n ]\n}\n```\n\n### Empty Children\nFor self-closing elements:\n```tsx\n{\n id: 'line-break',\n type: 'br',\n name: 'Line Break',\n props: {},\n children: [] // empty array\n}\n```" + }, + { + "id": "layer-structure-best-practices", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Best Practices\n\n### Unique IDs\nEnsure every layer has a unique `id`:\n```tsx\n// ✅ Good - unique IDs\n{ id: 'header-logo', type: 'img', ... }\n{ id: 'nav-menu', type: 'nav', ... }\n{ id: 'footer-copyright', type: 'p', ... }\n\n// ❌ Bad - duplicate IDs\n{ id: 'button', type: 'Button', ... }\n{ id: 'button', type: 'Button', ... } // Duplicate!\n```\n\n### Meaningful Names\nUse descriptive names for the layers panel:\n```tsx\n// ✅ Good - descriptive names\n{ id: 'hero-cta', name: 'Hero Call-to-Action', type: 'Button', ... }\n{ id: 'product-grid', name: 'Product Grid', type: 'div', ... }\n\n// ❌ Bad - generic names\n{ id: 'button-1', name: 'Button', type: 'Button', ... }\n{ id: 'div-2', name: 'div', type: 'div', ... }\n```\n\n### Component Dependencies\nMake sure all referenced component types exist in your registry:\n```tsx\n// If your Button has span children, include span in registry\nconst registry = {\n ...primitiveComponentDefinitions, // includes 'span'\n Button: { /* your button definition */ }\n};\n```\n\n### Semantic Structure\nFollow HTML semantic structure where possible:\n```tsx\n// ✅ Good - semantic structure\n{\n id: 'article',\n type: 'article',\n name: 'Blog Post',\n children: [\n { id: 'title', type: 'h1', name: 'Title', ... },\n { id: 'meta', type: 'div', name: 'Meta Info', ... },\n { id: 'content', type: 'div', name: 'Content', ... }\n ]\n}\n```" + }, + { + "id": "layer-structure-next-steps", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Related Topics\n\n- **[Component Registry](/docs/component-registry)** - Learn how to define which components are available\n- **[Variable Binding](/docs/variable-binding)** - Make your layers dynamic with variables\n- **[Rendering Pages](/docs/rendering-pages)** - Render layers without the editor\n- **[Persistence](/docs/persistence)** - Save and load layer structures" } ] } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/page-theming.ts b/app/docs/docs-data/docs-page-layers/page-theming.ts deleted file mode 100644 index 9f3cb56..0000000 --- a/app/docs/docs-data/docs-page-layers/page-theming.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ComponentLayer } from "@/components/ui/ui-builder/types"; - -export const PAGE_THEMING_LAYER = { - "id": "page-theming", - "type": "div", - "name": "Page Theming", - "props": { - "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", - "data-group": "rendering" - }, - "children": [ - { - "type": "span", - "children": "Page Theming", - "id": "page-theming-title", - "name": "Text", - "props": { - "className": "text-4xl" - } - }, - { - "id": "page-theming-intro", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "Apply consistent theming across UI Builder pages. Configure colors, typography, spacing, and design tokens for cohesive visual experiences." - }, - { - "id": "page-theming-content", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "## Theme Configuration\n\n```tsx\nconst theme = {\n colors: {\n primary: '#3B82F6',\n secondary: '#64748B',\n background: '#FFFFFF',\n text: '#1F2937'\n },\n typography: {\n fontFamily: 'Inter, sans-serif',\n sizes: {\n xs: '0.75rem',\n sm: '0.875rem',\n base: '1rem',\n lg: '1.125rem'\n }\n },\n spacing: {\n xs: '0.25rem',\n sm: '0.5rem',\n md: '1rem',\n lg: '1.5rem'\n }\n};\n\n\n```\n\n## CSS Variables\n\n```css\n:root {\n --color-primary: 59 130 246;\n --color-secondary: 100 116 139;\n --color-background: 255 255 255;\n --color-text: 31 41 55;\n \n --font-family: 'Inter', sans-serif;\n --font-size-base: 1rem;\n \n --spacing-unit: 0.25rem;\n}\n```\n\n## Dark Mode Support\n\n```tsx\nconst darkTheme = {\n colors: {\n primary: '#60A5FA',\n background: '#1F2937',\n text: '#F9FAFB'\n }\n};\n\nfunction ThemedUIBuilder() {\n const [isDark, setIsDark] = useState(false);\n \n return (\n \n );\n}\n```\n\n## Dynamic Theming\n\n```tsx\nfunction DynamicTheming() {\n const [theme, setTheme] = useState(defaultTheme);\n \n const updateThemeColor = (property, value) => {\n setTheme(prev => ({\n ...prev,\n colors: {\n ...prev.colors,\n [property]: value\n }\n }));\n };\n \n return (\n
\n \n \n
\n );\n}\n```" - } - ] - } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/pages-panel.ts b/app/docs/docs-data/docs-page-layers/pages-panel.ts deleted file mode 100644 index 0acf364..0000000 --- a/app/docs/docs-data/docs-page-layers/pages-panel.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { ComponentLayer } from "@/components/ui/ui-builder/types"; - -export const PAGES_PANEL_LAYER = { - "id": "pages-panel", - "type": "div", - "name": "Pages Panel", - "props": { - "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", - "data-group": "editor-features" - }, - "children": [ - { - "type": "span", - "children": "Pages Panel", - "id": "pages-panel-title", - "name": "Text", - "props": { - "className": "text-4xl" - } - }, - { - "id": "pages-panel-intro", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "The pages panel allows users to manage multiple pages within a single project. Create, organize, and navigate between different layouts and views using the layer store's page management system." - }, - { - "id": "pages-panel-demo", - "type": "div", - "name": "div", - "props": {}, - "children": [ - { - "id": "pages-panel-badge", - "type": "Badge", - "name": "Badge", - "props": { - "variant": "default", - "className": "rounded rounded-b-none" - }, - "children": [ - { - "id": "pages-panel-badge-text", - "type": "span", - "name": "span", - "props": {}, - "children": "Multi-Page Editor" - } - ] - }, - { - "id": "pages-panel-demo-frame", - "type": "div", - "name": "div", - "props": { - "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" - }, - "children": [ - { - "id": "pages-panel-iframe", - "type": "iframe", - "name": "iframe", - "props": { - "src": "http://localhost:3000/examples/editor/immutable-pages", - "title": "Pages Panel Demo", - "className": "aspect-square md:aspect-video w-full" - }, - "children": [] - } - ] - } - ] - }, - { - "id": "pages-panel-content", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "## Page Management\n\n### How Pages Work\n\nIn UI Builder, pages are essentially top-level component layers. Each page is a root component (like `div`, `main`, or any container) that serves as the foundation for a complete layout:\n\n```tsx\n// Example page structure\nconst initialLayers = [\n {\n id: \"homepage\",\n type: \"div\", // Any component can be a page\n name: \"Home Page\",\n props: {\n className: \"min-h-screen bg-background\"\n },\n children: [\n // Page content components\n ]\n },\n {\n id: \"about-page\", \n type: \"main\",\n name: \"About Us\",\n props: {\n className: \"container mx-auto py-8\"\n },\n children: [\n // About page content\n ]\n }\n];\n\n\n```\n\n### Page Creation and Management\n\nControl page creation through UIBuilder props:\n\n```tsx\n {\n // Save pages to your backend\n savePagesToDatabase(pages);\n }}\n/>\n```\n\n### Page Operations\n\nThe layer store provides methods for page management:\n\n```tsx\n// Access page management functions\nconst {\n pages, // Array of all pages\n selectedPageId, // Currently selected page ID\n addPage, // Create a new page\n removePage, // Delete a page\n duplicatePage, // Clone an existing page\n updatePage // Update page properties\n} = useLayerStore();\n\n// Example: Adding a new page programmatically\nconst createNewPage = () => {\n const newPage = {\n id: generateId(),\n type: \"div\",\n name: \"New Page\",\n props: {\n className: \"min-h-screen p-4\"\n },\n children: []\n };\n \n addPage(newPage);\n};\n```\n\n### Page Navigation\n\nUsers can navigate between pages through:\n\n- **Page List** - Click on any page to switch to it\n- **Page Tabs** - Quick switching between open pages\n- **Page Context Menu** - Right-click for page options\n- **Keyboard Shortcuts** - Fast navigation with hotkeys\n\n### Page Properties\n\nEach page supports the same properties as any component:\n\n```tsx\n// Page with custom properties\nconst customPage = {\n id: \"landing-page\",\n type: \"main\", // Use semantic HTML elements\n name: \"Landing Page\",\n props: {\n className: \"bg-gradient-to-b from-blue-50 to-white min-h-screen\",\n \"data-page-type\": \"landing\", // Custom attributes\n role: \"main\" // Accessibility\n },\n children: [\n // Page content structure\n ]\n};\n```\n\n## Multi-Page Project Structure\n\n### Shared Components Across Pages\n\nCreate reusable components that can be used across multiple pages:\n\n```tsx\nconst componentRegistry = {\n ...primitiveComponentDefinitions,\n // Shared header component\n SiteHeader: {\n component: SiteHeader,\n schema: z.object({\n title: z.string().default(\"My Site\"),\n showNavigation: z.boolean().default(true)\n }),\n from: \"@/components/site-header\"\n },\n // Shared footer component\n SiteFooter: {\n component: SiteFooter,\n schema: z.object({\n year: z.number().default(new Date().getFullYear())\n }),\n from: \"@/components/site-footer\"\n }\n};\n```\n\n### Global Variables\n\nUse variables to share data across all pages:\n\n```tsx\nconst globalVariables = [\n {\n id: \"site-title\",\n name: \"siteTitle\",\n type: \"string\",\n defaultValue: \"My Website\"\n },\n {\n id: \"brand-color\",\n name: \"brandColor\", \n type: \"string\",\n defaultValue: \"#3b82f6\"\n }\n];\n\n\n```\n\n### Page Templates\n\nCreate template pages that can be duplicated:\n\n```tsx\n// Template page with common structure\nconst pageTemplate = {\n id: \"page-template\",\n type: \"div\",\n name: \"Page Template\",\n props: {\n className: \"min-h-screen flex flex-col\"\n },\n children: [\n {\n id: \"header-section\",\n type: \"SiteHeader\",\n name: \"Header\",\n props: { title: { __variableRef: \"site-title\" } },\n children: []\n },\n {\n id: \"main-content\",\n type: \"main\",\n name: \"Main Content\",\n props: {\n className: \"flex-1 container mx-auto py-8\"\n },\n children: [\n // Template content\n ]\n },\n {\n id: \"footer-section\",\n type: \"SiteFooter\",\n name: \"Footer\", \n props: {},\n children: []\n }\n ]\n};\n```\n\n## Page Configuration Panel\n\nThe page configuration panel provides:\n\n- **Page Properties** - Edit page name and metadata\n- **Page Settings** - Configure page-specific options\n- **Duplicate Page** - Clone the current page\n- **Delete Page** - Remove the page (if multiple pages exist)\n\n## Responsive Pages\n\nPages automatically support responsive design:\n\n```tsx\n// Responsive page layout\nconst responsivePage = {\n id: \"responsive-page\",\n type: \"div\",\n name: \"Responsive Layout\",\n props: {\n className: \"min-h-screen p-4 md:p-8 lg:p-12\"\n },\n children: [\n {\n id: \"content-grid\",\n type: \"div\",\n name: \"Content Grid\",\n props: {\n className: \"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4\"\n },\n children: [\n // Responsive grid content\n ]\n }\n ]\n};\n```\n\n## Benefits of Multi-Page Support\n\n- **Organized Content** - Separate different sections into dedicated pages\n- **Modular Design** - Build reusable components that work across pages\n- **Efficient Workflow** - Edit multiple pages in the same session\n- **Consistent Branding** - Share variables and components across pages\n- **Easy Navigation** - Switch between pages without losing work" - } - ] - } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/panel-configuration.ts b/app/docs/docs-data/docs-page-layers/panel-configuration.ts index d6ffeff..41a69f2 100644 --- a/app/docs/docs-data/docs-page-layers/panel-configuration.ts +++ b/app/docs/docs-data/docs-page-layers/panel-configuration.ts @@ -6,7 +6,7 @@ export const PANEL_CONFIGURATION_LAYER = { "name": "Panel Configuration", "props": { "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", - "data-group": "editor-features" + "data-group": "advanced-configuration" }, "children": [ { @@ -23,61 +23,70 @@ export const PANEL_CONFIGURATION_LAYER = { "type": "Markdown", "name": "Markdown", "props": {}, - "children": "Configure the editor's panel system to match your workflow. Control the layout, content, and behavior of the main UI Builder panels through the `panelConfig` prop for a customized editing experience." + "children": "Customize the UI Builder's interface by configuring panels, tab labels, and adding your own components to the editor. The `panelConfig` prop gives you complete control over the editor's layout and functionality." + }, + { + "id": "panel-configuration-overview", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Understanding Panel Configuration\n\nUI Builder's interface consists of several configurable panels:\n\n- **Left Panel (Page Config)**: Contains tabs for Layers, Appearance, and Variables\n- **Center Panel (Editor)**: The main canvas and preview area\n- **Right Panel (Properties)**: Component property editing forms\n- **Navigation Bar**: Top navigation and controls\n\nThe `panelConfig` prop allows you to:\n- ✅ **Customize tab labels** in the left panel\n- ✅ **Replace tab content** with your own components\n- ✅ **Add new tabs** with custom functionality\n- ✅ **Remove unwanted tabs** for simplified interfaces\n- ✅ **Override entire panels** with custom implementations\n\n## Interactive Demo\n\nTry the different configurations below to see how `panelConfig` works:" }, { "id": "panel-configuration-demo", "type": "div", - "name": "div", - "props": {}, + "name": "Panel Configuration Demo", + "props": { + "className": "w-full border rounded-lg overflow-hidden bg-white shadow-sm" + }, "children": [ { - "id": "panel-configuration-badge", - "type": "Badge", - "name": "Badge", + "id": "demo-iframe", + "type": "iframe", + "name": "Demo Iframe", "props": { - "variant": "default", - "className": "rounded rounded-b-none" + "src": "/examples/editor/panel-config", + "className": "w-full h-[600px] border-none", + "title": "Panel Configuration Demo" }, - "children": [ - { - "id": "panel-configuration-badge-text", - "type": "span", - "name": "span", - "props": {}, - "children": "Customizable Layout" - } - ] - }, - { - "id": "panel-configuration-demo-frame", - "type": "div", - "name": "div", - "props": { - "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" - }, - "children": [ - { - "id": "panel-configuration-iframe", - "type": "iframe", - "name": "iframe", - "props": { - "src": "http://localhost:3000/examples/editor", - "title": "Panel Configuration Demo", - "className": "aspect-square md:aspect-video w-full" - }, - "children": [] - } - ] + "children": [] } ] }, { - "id": "panel-configuration-content", + "id": "basic-usage", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Basic Usage\n\n### Default Configuration\n\nBy default, UI Builder includes three tabs in the left panel:\n\n```tsx\nimport UIBuilder from '@/components/ui/ui-builder';\n\n// Uses default panel configuration\n\n```\n\nThis gives you:\n- **Layers**: Component tree and page management\n- **Appearance**: Tailwind theme and styling controls \n- **Data**: Variable management and binding\n\n### Custom Tab Labels\n\nRename tabs to match your workflow:\n\n```tsx\nimport UIBuilder, { defaultConfigTabsContent } from '@/components/ui/ui-builder';\n\nconst customPanelConfig = {\n pageConfigPanelTabsContent: {\n layers: { \n title: \"Structure\", \n content: defaultConfigTabsContent().layers.content \n },\n appearance: { \n title: \"Design\", \n content: defaultConfigTabsContent().appearance?.content \n },\n data: { \n title: \"Variables\", \n content: defaultConfigTabsContent().data?.content \n }\n }\n};\n\n\n```\n\n### Minimal Configuration\n\nShow only essential panels:\n\n```tsx\nconst minimalPanelConfig = {\n pageConfigPanelTabsContent: {\n layers: { \n title: \"Structure\", \n content: defaultConfigTabsContent().layers.content \n }\n // Only show the layers tab, hide appearance and data\n }\n};\n\n\n```" + }, + { + "id": "custom-panel-content", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Custom Panel Content\n\nReplace default panel content with your own React components:\n\n### Custom Appearance Panel\n\nCreate a branded design panel:\n\n```tsx\nconst CustomAppearancePanel = () => (\n
\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
\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.name}\n
\n
\n {source.connected \n ? `Connected • ${source.recordCount} records`\n : 'Disconnected'\n }\n
\n
\n ))}\n
\n \n {/* Add New 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 \n \n
\n \n
\n \n \n
\n \n
\n \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 \n \n
\n
\n\n {/* Simple Code Dialog */}\n {showCodeDialog && (\n
\n
\n
\n

Generated Code

\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 \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 \n \n addPageLayer('New Page')}\n >\n \n New Page\n \n
\n\n {/* Center Section - Preview Mode Toggle */}\n
\n Preview:\n \n
\n\n {/* Right Section - Actions */}\n
\n \n \n \n \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 \n \n
\n
\n);\n\nconst panelConfig = {\n navBar: customNavBar\n};\n```\n\n" + }, + { + "id": "use-cases", "type": "Markdown", "name": "Markdown", "props": {}, - "children": "## Available Panels\n\nUI Builder consists of four main panels that can be customized:\n\n### Core Panels\n- **Navigation Bar** - Top toolbar with editor controls\n- **Page Config Panel** - Left panel with Layers, Appearance, and Data tabs\n- **Editor Panel** - Center canvas for visual editing\n- **Props Panel** - Right panel for component property editing\n\n## Basic Panel Configuration\n\nUse the `panelConfig` prop to customize any panel:\n\n```tsx\nimport UIBuilder from '@/components/ui/ui-builder';\nimport { NavBar } from '@/components/ui/ui-builder/internal/components/nav';\nimport LayersPanel from '@/components/ui/ui-builder/internal/layers-panel';\nimport EditorPanel from '@/components/ui/ui-builder/internal/editor-panel';\nimport PropsPanel from '@/components/ui/ui-builder/internal/props-panel';\n\n,\n \n // Custom editor panel\n editorPanel: ,\n \n // Custom props panel \n propsPanel: ,\n \n // Custom page config panel\n pageConfigPanel: \n }}\n/>\n```\n\n## Page Config Panel Tabs\n\nThe most common customization is modifying the left panel tabs:\n\n```tsx\nimport { VariablesPanel } from '@/components/ui/ui-builder/internal/variables-panel';\nimport { TailwindThemePanel } from '@/components/ui/ui-builder/internal/tailwind-theme-panel';\nimport { ConfigPanel } from '@/components/ui/ui-builder/internal/config-panel';\n\nconst customTabsContent = {\n // Required: Layers tab\n layers: { \n title: \"Structure\", \n content: \n },\n \n // Optional: Appearance tab\n appearance: { \n title: \"Styling\", \n content: (\n
\n \n \n \n
\n )\n },\n \n // Optional: Data tab\n data: { \n title: \"Variables\", \n content: \n },\n \n // Add completely custom tabs\n assets: {\n title: \"Assets\",\n content: \n }\n};\n\n\n```\n\n## Default Panel Configuration\n\nThis is the default panel setup (equivalent to not providing `panelConfig`):\n\n```tsx\nimport { \n defaultConfigTabsContent, \n getDefaultPanelConfigValues \n} from '@/components/ui/ui-builder';\n\n// Default tabs content\nconst defaultTabs = defaultConfigTabsContent();\n// Returns:\n// {\n// layers: { title: \"Layers\", content: },\n// appearance: { title: \"Appearance\", content: },\n// data: { title: \"Data\", content: }\n// }\n\n// Default panel values\nconst defaultPanels = getDefaultPanelConfigValues(defaultTabs);\n// Returns:\n// {\n// navBar: ,\n// pageConfigPanel: ,\n// editorPanel: ,\n// propsPanel: \n// }\n```\n\n## Custom Navigation Bar\n\nReplace the default navigation with your own:\n\n```tsx\nconst MyCustomNavBar = () => {\n const showLeftPanel = useEditorStore(state => state.showLeftPanel);\n const toggleLeftPanel = useEditorStore(state => state.toggleLeftPanel);\n const showRightPanel = useEditorStore(state => state.showRightPanel);\n const toggleRightPanel = useEditorStore(state => state.toggleRightPanel);\n \n return (\n \n );\n};\n\n\n }}\n/>\n```\n\n## Custom Editor Panel\n\nReplace the canvas area with custom functionality:\n\n```tsx\nconst MyCustomEditor = ({ className }) => {\n const selectedPageId = useLayerStore(state => state.selectedPageId);\n const findLayerById = useLayerStore(state => state.findLayerById);\n const currentPage = findLayerById(selectedPageId);\n \n return (\n
\n {/* Custom toolbar */}\n
\n \n \n \n \n
\n \n {/* Custom canvas */}\n
\n
\n {currentPage && (\n \n )}\n
\n
\n
\n );\n};\n\n\n }}\n/>\n```\n\n## Responsive Panel Behavior\n\nUI Builder automatically handles responsive layouts:\n\n### Desktop Layout\n- **Three panels** side by side using ResizablePanelGroup\n- **Resizable handles** between panels\n- **Collapsible panels** via editor store state\n\n### Mobile Layout\n- **Single panel view** with bottom navigation\n- **Panel switcher** at the bottom\n- **Full-screen panels** for better mobile experience\n\n### Panel Visibility Control\n\n```tsx\n// Control panel visibility programmatically\nconst MyPanelController = () => {\n const { \n showLeftPanel, \n showRightPanel,\n toggleLeftPanel, \n toggleRightPanel \n } = useEditorStore();\n \n return (\n
\n \n \n
\n );\n};\n```\n\n## Integration with Editor State\n\nPanels integrate with the editor state management:\n\n```tsx\n// Access editor state in custom panels\nconst MyCustomPanel = () => {\n const componentRegistry = useEditorStore(state => state.registry);\n const allowPagesCreation = useEditorStore(state => state.allowPagesCreation);\n const allowVariableEditing = useEditorStore(state => state.allowVariableEditing);\n \n const selectedLayerId = useLayerStore(state => state.selectedLayerId);\n const pages = useLayerStore(state => state.pages);\n const variables = useLayerStore(state => state.variables);\n \n return (\n
\n

Custom Panel

\n

Registry has {Object.keys(componentRegistry).length} components

\n

Current page: {pages.find(p => p.id === selectedLayerId)?.name}

\n

Variables: {variables.length}

\n
\n );\n};\n```\n\n## Best Practices\n\n### Panel Design\n- **Follow existing patterns** for consistency\n- **Use proper overflow handling** (`overflow-y-auto`) for scrollable content\n- **Include proper padding/spacing** (`px-4 py-2`)\n- **Respect theme variables** for colors and spacing\n\n### State Management\n- **Use editor and layer stores** for state access\n- **Don't duplicate state** - use the existing stores\n- **Subscribe to specific slices** to avoid unnecessary re-renders\n- **Use proper cleanup** in useEffect hooks\n\n### Performance\n- **Memoize expensive components** with React.memo\n- **Use virtualization** for large lists\n- **Debounce rapid updates** when needed\n- **Minimize re-renders** by careful state subscription\n\n### Accessibility\n- **Provide proper ARIA labels** for custom controls\n- **Ensure keyboard navigation** works correctly\n- **Use semantic HTML** where possible\n- **Test with screen readers** for complex interactions" + "children": "## Common Use Cases\n\n### White-Label Applications\n\nCustomize the interface to match your brand:\n\n```tsx\nconst brandedPanelConfig = {\n pageConfigPanelTabsContent: {\n layers: { \n title: \"Components\", \n content: defaultConfigTabsContent().layers.content \n },\n appearance: { \n title: \"Brand Kit\", \n content: \n },\n data: { \n title: \"Content\", \n content: \n }\n }\n};\n```\n\n### Specialized Workflows\n\nTailor the interface for specific use cases:\n\n```tsx\n// Email template builder\nconst emailBuilderConfig = {\n pageConfigPanelTabsContent: {\n layers: { title: \"Blocks\", content: defaultConfigTabsContent().layers.content },\n appearance: { title: \"Styling\", content: },\n data: { title: \"Merge Tags\", content: },\n preview: { title: \"Preview\", content: }\n }\n};\n\n// Landing page builder\nconst landingPageConfig = {\n pageConfigPanelTabsContent: {\n layers: { title: \"Sections\", content: defaultConfigTabsContent().layers.content },\n appearance: { title: \"Theme\", content: },\n data: { title: \"A/B Tests\", content: },\n analytics: { title: \"Analytics\", content: }\n }\n};\n```\n\n### Simplified Interfaces\n\nHide complexity for non-technical users:\n\n```tsx\n// Content editor - no technical options\nconst contentEditorConfig = {\n pageConfigPanelTabsContent: {\n layers: { title: \"Content\", content: defaultConfigTabsContent().layers.content }\n // Hide appearance and data tabs\n }\n};\n\n// Designer interface - focus on visual\nconst designerConfig = {\n pageConfigPanelTabsContent: {\n layers: { title: \"Layers\", content: defaultConfigTabsContent().layers.content },\n appearance: { title: \"Design\", content: }\n // Hide data/variables tab\n }\n};\n```\n\n### Integration with External Tools\n\nConnect with your existing systems:\n\n```tsx\nconst integratedConfig = {\n pageConfigPanelTabsContent: {\n layers: { title: \"Structure\", content: defaultConfigTabsContent().layers.content },\n appearance: { title: \"Styling\", content: defaultConfigTabsContent().appearance?.content },\n data: { title: \"Data\", content: defaultConfigTabsContent().data?.content },\n cms: { title: \"CMS\", content: },\n assets: { title: \"Assets\", content: }\n }\n};\n```" } ] } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/persistence.ts b/app/docs/docs-data/docs-page-layers/persistence.ts index 0c849ac..7fee5a7 100644 --- a/app/docs/docs-data/docs-page-layers/persistence.ts +++ b/app/docs/docs-data/docs-page-layers/persistence.ts @@ -23,14 +23,77 @@ export const PERSISTENCE_LAYER = { "type": "Markdown", "name": "Markdown", "props": {}, - "children": "Learn how UI Builder manages state and provides flexible persistence options for your layouts and variables. Save to databases, manage auto-save behavior, and handle state changes with simple, powerful APIs." + "children": "UI Builder provides flexible state management and persistence options for your layouts and variables. Choose between automatic local storage, custom database integration, or complete manual control based on your application's needs." }, { - "id": "persistence-content", + "id": "persistence-state-overview", "type": "Markdown", "name": "Markdown", "props": {}, - "children": "## Understanding UI Builder State\n\nUI Builder manages two main types of state:\n- **Layers**: The component hierarchy and structure\n- **Variables**: Dynamic data that can be bound to component properties\n\n## Local Storage Persistence\n\nBy default, UI Builder automatically saves state to browser local storage:\n\n```tsx\n// Default behavior - auto-saves to localStorage\n\n\n// Disable local storage persistence\n\n```\n\n## Database Integration\n\nFor production applications, you'll want to save state to your database:\n\n```tsx\nimport { useState, useEffect } from 'react';\nimport UIBuilder from '@/components/ui/ui-builder';\nimport { ComponentLayer, Variable } from '@/components/ui/ui-builder/types';\n\nfunction DatabaseIntegratedBuilder({ userId }: { userId: string }) {\n const [initialLayers, setInitialLayers] = useState();\n const [initialVariables, setInitialVariables] = useState();\n const [isLoading, setIsLoading] = useState(true);\n\n // Load initial state from database\n useEffect(() => {\n async function loadUserLayout() {\n try {\n const response = await fetch(`/api/layouts/${userId}`);\n const data = await response.json();\n \n setInitialLayers(data.layers || []);\n setInitialVariables(data.variables || []);\n } catch (error) {\n console.error('Failed to load layout:', error);\n // Fallback to empty state\n setInitialLayers([]);\n setInitialVariables([]);\n } finally {\n setIsLoading(false);\n }\n }\n\n loadUserLayout();\n }, [userId]);\n\n // Save layers to database\n const handleLayersChange = async (updatedLayers: ComponentLayer[]) => {\n try {\n await fetch(`/api/layouts/${userId}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers: updatedLayers })\n });\n } catch (error) {\n console.error('Failed to save layers:', error);\n }\n };\n\n // Save variables to database\n const handleVariablesChange = async (updatedVariables: Variable[]) => {\n try {\n await fetch(`/api/variables/${userId}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ variables: updatedVariables })\n });\n } catch (error) {\n console.error('Failed to save variables:', error);\n }\n };\n\n if (isLoading) {\n return
Loading your layout...
;\n }\n\n return (\n \n );\n}\n```\n\n## Debounced Auto-Save\n\nTo avoid excessive API calls, implement debounced saving:\n\n```tsx\nimport { useCallback } from 'react';\nimport { debounce } from 'lodash';\n\nfunction AutoSaveBuilder() {\n // Debounced save function - waits 2 seconds after last change\n const debouncedSave = useCallback(\n debounce(async (layers: ComponentLayer[]) => {\n try {\n await fetch('/api/layouts/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers })\n });\n console.log('Auto-saved successfully');\n } catch (error) {\n console.error('Auto-save failed:', error);\n }\n }, 2000),\n []\n );\n\n const debouncedSaveVariables = useCallback(\n debounce(async (variables: Variable[]) => {\n try {\n await fetch('/api/variables/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ variables })\n });\n console.log('Variables auto-saved successfully');\n } catch (error) {\n console.error('Variables auto-save failed:', error);\n }\n }, 2000),\n []\n );\n\n return (\n \n );\n}\n```\n\n## Manual Save with UI Feedback\n\nProvide users with explicit save controls:\n\n```tsx\nimport { useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Badge } from '@/components/ui/badge';\n\nfunction ManualSaveBuilder() {\n const [currentLayers, setCurrentLayers] = useState([]);\n const [currentVariables, setCurrentVariables] = useState([]);\n const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);\n const [isSaving, setIsSaving] = useState(false);\n\n const handleLayersChange = (layers: ComponentLayer[]) => {\n setCurrentLayers(layers);\n setHasUnsavedChanges(true);\n };\n\n const handleVariablesChange = (variables: Variable[]) => {\n setCurrentVariables(variables);\n setHasUnsavedChanges(true);\n };\n\n const handleSave = async () => {\n setIsSaving(true);\n try {\n await Promise.all([\n fetch('/api/layouts/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers: currentLayers })\n }),\n fetch('/api/variables/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ variables: currentVariables })\n })\n ]);\n \n setHasUnsavedChanges(false);\n alert('Saved successfully!');\n } catch (error) {\n console.error('Save failed:', error);\n alert('Save failed. Please try again.');\n } finally {\n setIsSaving(false);\n }\n };\n\n return (\n
\n {/* Save Controls */}\n
\n \n \n {hasUnsavedChanges && (\n Unsaved Changes\n )}\n
\n \n {/* Builder */}\n
\n \n
\n
\n );\n}\n```\n\n## Data Format\n\nUI Builder saves data in a simple, readable JSON format:\n\n```json\n{\n \"layers\": [\n {\n \"id\": \"page-1\",\n \"type\": \"div\",\n \"name\": \"Page 1\",\n \"props\": {\n \"className\": \"p-4 bg-white\"\n },\n \"children\": [\n {\n \"id\": \"button-1\",\n \"type\": \"Button\",\n \"name\": \"Submit Button\",\n \"props\": {\n \"variant\": \"default\",\n \"children\": { \"__variableRef\": \"buttonText\" }\n },\n \"children\": []\n }\n ]\n }\n ],\n \"variables\": [\n {\n \"id\": \"buttonText\",\n \"name\": \"Button Text\",\n \"type\": \"string\",\n \"defaultValue\": \"Click Me!\"\n }\n ]\n}\n```\n\n## API Route Examples\n\nHere are example API routes for Next.js:\n\n### Save Layout API Route\n\n```tsx\n// app/api/layouts/[userId]/route.ts\nimport { NextRequest, NextResponse } from 'next/server';\n\nexport async function PUT(\n request: NextRequest,\n { params }: { params: { userId: string } }\n) {\n try {\n const { layers } = await request.json();\n const { userId } = params;\n \n // Save to your database\n await saveUserLayout(userId, layers);\n \n return NextResponse.json({ success: true });\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to save layout' },\n { status: 500 }\n );\n }\n}\n\nexport async function GET(\n request: NextRequest,\n { params }: { params: { userId: string } }\n) {\n try {\n const { userId } = params;\n const layout = await getUserLayout(userId);\n \n return NextResponse.json(layout);\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to load layout' },\n { status: 500 }\n );\n }\n}\n```\n\n### Save Variables API Route\n\n```tsx\n// app/api/variables/[userId]/route.ts\nimport { NextRequest, NextResponse } from 'next/server';\n\nexport async function PUT(\n request: NextRequest,\n { params }: { params: { userId: string } }\n) {\n try {\n const { variables } = await request.json();\n const { userId } = params;\n \n await saveUserVariables(userId, variables);\n \n return NextResponse.json({ success: true });\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to save variables' },\n { status: 500 }\n );\n }\n}\n```\n\n## Error Handling & Recovery\n\nImplement robust error handling for persistence:\n\n```tsx\nfunction RobustBuilder() {\n const [lastSavedState, setLastSavedState] = useState(null);\n const [saveError, setSaveError] = useState(null);\n\n const handleSaveWithRetry = async (layers: ComponentLayer[], retries = 3) => {\n for (let i = 0; i < retries; i++) {\n try {\n await fetch('/api/layouts/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers })\n });\n \n setLastSavedState(layers);\n setSaveError(null);\n return;\n } catch (error) {\n if (i === retries - 1) {\n setSaveError('Failed to save after multiple attempts');\n console.error('Save failed after retries:', error);\n } else {\n // Wait before retry\n await new Promise(resolve => setTimeout(resolve, 1000));\n }\n }\n }\n };\n\n const handleRecovery = () => {\n if (lastSavedState) {\n // Restore to last saved state\n window.location.reload();\n }\n };\n\n return (\n
\n {saveError && (\n
\n Save Error: {saveError}\n \n
\n )}\n \n \n
\n );\n}\n```\n\n## Best Practices\n\n1. **Always handle save errors gracefully** - Show user feedback and provide recovery options\n2. **Use debouncing for auto-save** - Avoid overwhelming your API with requests\n3. **Validate data before saving** - Ensure the data structure is correct\n4. **Provide manual save controls** - Give users explicit control over when data is saved\n5. **Consider offline support** - Store changes locally when the network is unavailable\n6. **Implement proper loading states** - Show users when data is being loaded or saved\n7. **Use proper error boundaries** - Prevent save errors from crashing the entire editor" + "children": "## Understanding UI Builder State\n\nUI Builder manages two main types of state:\n- **Pages & Layers**: The component hierarchy, structure, and configuration\n- **Variables**: Dynamic data definitions that can be bound to component properties\n\nBoth are managed independently and can be persisted using different strategies." + }, + { + "id": "persistence-local-storage", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Local Storage Persistence\n\nBy default, UI Builder automatically saves state to browser local storage:\n\n```tsx\n// Default behavior - auto-saves to localStorage\n\n\n// Disable local storage persistence\n\n```\n\n**When to use:** Development, prototyping, or single-user applications where browser storage is sufficient.\n\n**Limitations:** Data is tied to the browser/device and can be cleared by the user." + }, + { + "id": "persistence-database-integration", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Database Integration\n\nFor production applications, integrate with your database using the initialization and callback props:\n\n```tsx\nimport UIBuilder from '@/components/ui/ui-builder';\nimport type { ComponentLayer, Variable, LayerChangeHandler, VariableChangeHandler } from '@/components/ui/ui-builder/types';\n\nfunction DatabaseIntegratedBuilder({ userId }: { userId: string }) {\n const [initialLayers, setInitialLayers] = useState();\n const [initialVariables, setInitialVariables] = useState();\n const [isLoading, setIsLoading] = useState(true);\n\n // Load initial state from database\n useEffect(() => {\n async function loadUserLayout() {\n try {\n const [layoutRes, variablesRes] = await Promise.all([\n fetch(`/api/layouts/${userId}`),\n fetch(`/api/variables/${userId}`)\n ]);\n \n const layoutData = await layoutRes.json();\n const variablesData = await variablesRes.json();\n \n setInitialLayers(layoutData.layers || []);\n setInitialVariables(variablesData.variables || []);\n } catch (error) {\n console.error('Failed to load layout:', error);\n setInitialLayers([]);\n setInitialVariables([]);\n } finally {\n setIsLoading(false);\n }\n }\n\n loadUserLayout();\n }, [userId]);\n\n // Save layers to database\n const handleLayersChange: LayerChangeHandler = async (updatedLayers) => {\n try {\n await fetch(`/api/layouts/${userId}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers: updatedLayers })\n });\n } catch (error) {\n console.error('Failed to save layers:', error);\n }\n };\n\n // Save variables to database\n const handleVariablesChange: VariableChangeHandler = async (updatedVariables) => {\n try {\n await fetch(`/api/variables/${userId}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ variables: updatedVariables })\n });\n } catch (error) {\n console.error('Failed to save variables:', error);\n }\n };\n\n if (isLoading) {\n return
Loading your layout...
;\n }\n\n return (\n \n );\n}\n```" + }, + { + "id": "persistence-props-reference", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Persistence-Related Props\n\n### Core Persistence Props\n\n- **`persistLayerStore`** (`boolean`, default: `true`): Controls localStorage persistence\n- **`initialLayers`** (`ComponentLayer[]`): Initial pages/layers to load\n- **`onChange`** (`LayerChangeHandler`): Callback when layers change\n- **`initialVariables`** (`Variable[]`): Initial variables to load\n- **`onVariablesChange`** (`VariableChangeHandler`): Callback when variables change\n\n### Permission Control Props\n\nControl what users can modify to prevent unwanted changes:\n\n- **`allowVariableEditing`** (`boolean`, default: `true`): Allow variable creation/editing\n- **`allowPagesCreation`** (`boolean`, default: `true`): Allow new page creation\n- **`allowPagesDeletion`** (`boolean`, default: `true`): Allow page deletion\n\n```tsx\n// Read-only editor for content review\n\n```" + }, + { + "id": "persistence-working-examples", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Working Examples\n\nExplore these working examples in the project:\n\n- **[Basic Example](/examples/basic)**: Simple setup with localStorage persistence\n- **[Editor Example](/examples/editor)**: Full editor with custom configuration\n- **[Immutable Pages](/examples/editor/immutable-pages)**: Read-only pages with `allowPagesCreation={false}` and `allowPagesDeletion={false}`\n\nThe examples demonstrate different persistence patterns you can adapt for your use case." + }, + { + "id": "persistence-debounced-save", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Debounced Auto-Save\n\nAvoid excessive API calls with debounced saving:\n\n```tsx\nimport { useCallback } from 'react';\nimport { debounce } from 'lodash';\n\nfunction AutoSaveBuilder() {\n // Debounce saves to reduce API calls\n const debouncedSaveLayers = useCallback(\n debounce(async (layers: ComponentLayer[]) => {\n try {\n await fetch('/api/layouts/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers })\n });\n } catch (error) {\n console.error('Auto-save failed:', error);\n }\n }, 2000), // 2 second delay\n []\n );\n\n const debouncedSaveVariables = useCallback(\n debounce(async (variables: Variable[]) => {\n try {\n await fetch('/api/variables/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ variables })\n });\n } catch (error) {\n console.error('Variables auto-save failed:', error);\n }\n }, 2000),\n []\n );\n\n return (\n \n );\n}\n```" + }, + { + "id": "persistence-data-format", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Data Format\n\nUI Builder saves data as plain JSON with a predictable structure:\n\n```json\n{\n \"layers\": [\n {\n \"id\": \"page-1\",\n \"type\": \"div\",\n \"name\": \"Page 1\",\n \"props\": {\n \"className\": \"p-4 bg-white\"\n },\n \"children\": [\n {\n \"id\": \"button-1\",\n \"type\": \"Button\",\n \"name\": \"Submit Button\",\n \"props\": {\n \"variant\": \"default\",\n \"children\": {\n \"__variableRef\": \"buttonText\"\n }\n },\n \"children\": []\n }\n ]\n }\n ],\n \"variables\": [\n {\n \"id\": \"buttonText\",\n \"name\": \"Button Text\",\n \"type\": \"string\",\n \"defaultValue\": \"Click Me!\"\n }\n ]\n}\n```\n\n**Variable References**: Component properties bound to variables use `{ \"__variableRef\": \"variableId\" }` format." + }, + { + "id": "persistence-api-routes", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Next.js API Route Examples\n\n### Layout API Route\n\n```tsx\n// app/api/layouts/[userId]/route.ts\nimport { NextRequest, NextResponse } from 'next/server';\n\nexport async function GET(\n request: NextRequest,\n { params }: { params: { userId: string } }\n) {\n try {\n const layout = await getUserLayout(params.userId);\n return NextResponse.json({ layers: layout });\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to load layout' },\n { status: 500 }\n );\n }\n}\n\nexport async function PUT(\n request: NextRequest,\n { params }: { params: { userId: string } }\n) {\n try {\n const { layers } = await request.json();\n await saveUserLayout(params.userId, layers);\n return NextResponse.json({ success: true });\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to save layout' },\n { status: 500 }\n );\n }\n}\n```\n\n### Variables API Route \n\n```tsx\n// app/api/variables/[userId]/route.ts\nexport async function PUT(\n request: NextRequest,\n { params }: { params: { userId: string } }\n) {\n try {\n const { variables } = await request.json();\n await saveUserVariables(params.userId, variables);\n return NextResponse.json({ success: true });\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to save variables' },\n { status: 500 }\n );\n }\n}\n```" + }, + { + "id": "persistence-error-handling", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Error Handling & Recovery\n\nImplement robust error handling for production applications:\n\n```tsx\nfunction RobustBuilder() {\n const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'error' | 'success'>('idle');\n const [lastError, setLastError] = useState(null);\n\n const handleSaveWithRetry = async (layers: ComponentLayer[], retries = 3) => {\n setSaveStatus('saving');\n \n for (let i = 0; i < retries; i++) {\n try {\n await fetch('/api/layouts/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers })\n });\n \n setSaveStatus('success');\n setLastError(null);\n return;\n } catch (error) {\n if (i === retries - 1) {\n setSaveStatus('error');\n setLastError('Failed to save after multiple attempts');\n } else {\n // Wait before retry with exponential backoff\n await new Promise(resolve => \n setTimeout(resolve, Math.pow(2, i) * 1000)\n );\n }\n }\n }\n };\n\n return (\n
\n {/* Status Bar */}\n
\n
\n {saveStatus === 'saving' && (\n Saving...\n )}\n {saveStatus === 'success' && (\n Saved\n )}\n {saveStatus === 'error' && (\n Save Failed\n )}\n
\n \n {lastError && (\n \n )}\n
\n \n {/* Builder */}\n
\n \n
\n
\n );\n}\n```" + }, + { + "id": "persistence-best-practices", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Best Practices\n\n### 1. **Choose the Right Persistence Strategy**\n- **localStorage**: Development, prototyping, single-user apps\n- **Database**: Production, multi-user, collaborative editing\n- **Hybrid**: localStorage for drafts, database for published versions\n\n### 2. **Implement Proper Error Handling**\n- Always handle save failures gracefully\n- Provide user feedback for save status\n- Implement retry logic with exponential backoff\n- Offer recovery options when saves fail\n\n### 3. **Optimize API Performance**\n- Use debouncing to reduce API calls (2-5 second delays)\n- Consider batching layer and variable saves\n- Implement optimistic updates for better UX\n- Use proper HTTP status codes and error responses\n\n### 4. **Control User Permissions**\n- Use `allowVariableEditing={false}` for content-only editing\n- Set `allowPagesCreation={false}` for fixed page structures \n- Implement role-based access control in your API routes\n\n### 5. **Plan for Scale**\n- Consider data size limits (layers can grow large)\n- Implement pagination for large datasets\n- Use database indexing on user IDs and timestamps\n- Consider caching frequently accessed layouts" } ] } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/props-panel-customization.ts b/app/docs/docs-data/docs-page-layers/props-panel-customization.ts deleted file mode 100644 index 546d689..0000000 --- a/app/docs/docs-data/docs-page-layers/props-panel-customization.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { ComponentLayer } from "@/components/ui/ui-builder/types"; - -export const PROPS_PANEL_CUSTOMIZATION_LAYER = { - "id": "props-panel-customization", - "type": "div", - "name": "Props Panel Customization", - "props": { - "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", - "data-group": "editor-features" - }, - "children": [ - { - "type": "span", - "children": "Props Panel Customization", - "id": "props-panel-customization-title", - "name": "Text", - "props": { - "className": "text-4xl" - } - }, - { - "id": "props-panel-customization-intro", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "Customize the properties panel to create intuitive, context-aware editing experiences. Design custom field types through AutoForm field overrides and create specialized property editors for your components." - }, - { - "id": "props-panel-customization-demo", - "type": "div", - "name": "div", - "props": {}, - "children": [ - { - "id": "props-panel-customization-badge", - "type": "Badge", - "name": "Badge", - "props": { - "variant": "default", - "className": "rounded rounded-b-none" - }, - "children": [ - { - "id": "props-panel-customization-badge-text", - "type": "span", - "name": "span", - "props": {}, - "children": "Custom Property Forms" - } - ] - }, - { - "id": "props-panel-customization-demo-frame", - "type": "div", - "name": "div", - "props": { - "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" - }, - "children": [ - { - "id": "props-panel-customization-iframe", - "type": "iframe", - "name": "iframe", - "props": { - "src": "http://localhost:3000/examples/editor", - "title": "Props Panel Customization Demo", - "className": "aspect-square md:aspect-video w-full" - }, - "children": [] - } - ] - } - ] - }, - { - "id": "props-panel-customization-content", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "## Field Overrides System\n\nUI Builder uses AutoForm to generate property forms from Zod schemas. You can customize individual fields using the `fieldOverrides` property in your component registry:\n\n```tsx\nimport { z } from 'zod';\nimport { classNameFieldOverrides, childrenAsTextareaFieldOverrides } from '@/lib/ui-builder/registry/form-field-overrides';\n\nconst MyCard = {\n component: Card,\n schema: z.object({\n title: z.string().default('Card Title'),\n description: z.string().optional(),\n imageUrl: z.string().optional(),\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: '@/components/ui/card',\n fieldOverrides: {\n // Use built-in className editor with Tailwind autocomplete\n className: (layer) => classNameFieldOverrides(layer),\n \n // Use textarea for description\n description: {\n fieldType: 'textarea',\n inputProps: {\n placeholder: 'Enter card description...',\n rows: 3\n }\n },\n \n // Custom image picker\n imageUrl: {\n fieldType: 'input',\n inputProps: {\n type: 'url',\n placeholder: 'https://example.com/image.jpg'\n },\n description: 'URL of the image to display'\n },\n \n // Use textarea for children content\n children: (layer) => childrenAsTextareaFieldOverrides(layer)\n }\n};\n```\n\n## Built-in Field Overrides\n\nUI Builder provides several pre-built field overrides for common use cases:\n\n### Common Field Overrides\n\n```tsx\nimport { \n commonFieldOverrides,\n classNameFieldOverrides,\n childrenFieldOverrides,\n childrenAsTextareaFieldOverrides \n} from '@/lib/ui-builder/registry/form-field-overrides';\n\n// Apply both className and children overrides\nfieldOverrides: commonFieldOverrides()\n\n// Or use individual overrides\nfieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer),\n children: (layer) => childrenFieldOverrides(layer)\n}\n```\n\n### className Field Override\n\nProvides intelligent Tailwind CSS class suggestions:\n\n```tsx\nfieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer)\n}\n\n// Features:\n// - Autocomplete suggestions\n// - Tailwind class validation\n// - Responsive class support\n// - Common pattern suggestions\n```\n\n### children Field Override\n\nSmart handling for component children:\n\n```tsx\nfieldOverrides: {\n // Standard children editor\n children: (layer) => childrenFieldOverrides(layer),\n \n // Or force textarea for text content\n children: (layer) => childrenAsTextareaFieldOverrides(layer)\n}\n\n// Features:\n// - String content as textarea\n// - Component children as visual editor\n// - Variable binding support\n```\n\n## AutoForm Field Configuration\n\nCustomize how AutoForm renders your fields:\n\n### Basic Field Types\n\n```tsx\nfieldOverrides: {\n // Text input with placeholder\n title: {\n inputProps: {\n placeholder: 'Enter title...',\n maxLength: 100\n }\n },\n \n // Number input with constraints\n count: {\n inputProps: {\n min: 0,\n max: 999,\n step: 1\n }\n },\n \n // Textarea with custom rows\n description: {\n fieldType: 'textarea',\n inputProps: {\n rows: 4,\n placeholder: 'Describe your component...'\n }\n },\n \n // Color input\n backgroundColor: {\n fieldType: 'input',\n inputProps: {\n type: 'color'\n }\n },\n \n // URL input with validation\n link: {\n fieldType: 'input',\n inputProps: {\n type: 'url',\n placeholder: 'https://example.com'\n }\n }\n}\n```\n\n### Advanced Field Configuration\n\n```tsx\nfieldOverrides: {\n // Custom field with description and label\n apiEndpoint: {\n inputProps: {\n placeholder: '/api/data',\n pattern: '^/api/.*'\n },\n description: 'Relative API endpoint path',\n label: 'API Endpoint'\n },\n \n // Hidden field (for internal use)\n internalId: {\n isHidden: true\n },\n \n // Field with custom validation message\n email: {\n inputProps: {\n type: 'email',\n placeholder: 'user@example.com'\n },\n description: 'Valid email address required'\n }\n}\n```\n\n## Custom Field Components\n\nCreate completely custom field editors:\n\n```tsx\n// Custom spacing control component\nconst SpacingControl = ({ value, onChange }) => {\n const [spacing, setSpacing] = useState(value || { top: 0, right: 0, bottom: 0, left: 0 });\n \n const updateSpacing = (side, newValue) => {\n const updated = { ...spacing, [side]: newValue };\n setSpacing(updated);\n onChange(updated);\n };\n \n return (\n
\n
\n updateSpacing('top', parseInt(e.target.value))}\n className=\"text-center p-1 border rounded\"\n placeholder=\"T\"\n />\n
\n updateSpacing('left', parseInt(e.target.value))}\n className=\"text-center p-1 border rounded\"\n placeholder=\"L\"\n />\n
\n updateSpacing('right', parseInt(e.target.value))}\n className=\"text-center p-1 border rounded\"\n placeholder=\"R\"\n />\n
\n updateSpacing('bottom', parseInt(e.target.value))}\n className=\"text-center p-1 border rounded\"\n placeholder=\"B\"\n />\n
\n );\n};\n\n// Use custom component in field override\nfieldOverrides: {\n spacing: {\n renderParent: ({ children, ...props }) => (\n \n )\n }\n}\n```\n\n## Component-Specific Customizations\n\nTailor field overrides to specific component types:\n\n### Button Component\n\n```tsx\nconst ButtonComponent = {\n component: Button,\n schema: z.object({\n variant: z.enum(['default', 'destructive', 'outline', 'secondary', 'ghost', 'link']).default('default'),\n size: z.enum(['default', 'sm', 'lg', 'icon']).default('default'),\n disabled: z.boolean().optional(),\n children: z.any().optional(),\n onClick: z.string().optional(),\n className: z.string().optional()\n }),\n from: '@/components/ui/button',\n fieldOverrides: {\n // Use common overrides for basic props\n ...commonFieldOverrides(),\n \n // Custom click handler editor\n onClick: {\n inputProps: {\n placeholder: 'console.log(\"Button clicked\")',\n family: 'monospace'\n },\n description: 'JavaScript code to execute on click',\n label: 'Click Handler'\n },\n \n // Enhanced variant selector with descriptions\n variant: {\n description: 'Visual style of the button'\n }\n }\n};\n```\n\n### Image Component\n\n```tsx\nconst ImageComponent = {\n component: 'img',\n schema: z.object({\n src: z.string().url(),\n alt: z.string(),\n width: z.coerce.number().optional(),\n height: z.coerce.number().optional(),\n className: z.string().optional()\n }),\n fieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer),\n \n // Image URL with preview\n src: {\n inputProps: {\n type: 'url',\n placeholder: 'https://example.com/image.jpg'\n },\n description: 'URL of the image to display',\n // Note: Custom preview would require a custom render component\n },\n \n // Alt text with guidance\n alt: {\n inputProps: {\n placeholder: 'Describe the image for accessibility'\n },\n description: 'Alternative text for screen readers'\n },\n \n // Dimensions with constraints\n width: {\n inputProps: {\n min: 1,\n max: 2000,\n step: 1\n }\n },\n \n height: {\n inputProps: {\n min: 1,\n max: 2000,\n step: 1\n }\n }\n }\n};\n```\n\n## Variable Binding Integration\n\nField overrides work seamlessly with variable binding:\n\n```tsx\n// Component with variable-bindable properties\nconst UserCard = {\n component: UserCard,\n schema: z.object({\n name: z.string().default(''),\n email: z.string().email().optional(),\n avatar: z.string().url().optional(),\n role: z.enum(['admin', 'user', 'guest']).default('user')\n }),\n from: '@/components/user-card',\n fieldOverrides: {\n name: {\n inputProps: {\n placeholder: 'User full name'\n },\n description: 'Can be bound to user data variable'\n },\n \n email: {\n inputProps: {\n type: 'email',\n placeholder: 'user@example.com'\n },\n description: 'Bind to user email variable for dynamic content'\n },\n \n avatar: {\n inputProps: {\n type: 'url',\n placeholder: 'https://example.com/avatar.jpg'\n },\n description: 'Profile picture URL - can be bound to user avatar variable'\n }\n }\n};\n\n// Variables for binding\nconst userVariables = [\n { id: 'user-name', name: 'currentUserName', type: 'string', defaultValue: 'John Doe' },\n { id: 'user-email', name: 'currentUserEmail', type: 'string', defaultValue: 'john@example.com' },\n { id: 'user-avatar', name: 'currentUserAvatar', type: 'string', defaultValue: '/default-avatar.png' }\n];\n```\n\n## Best Practices\n\n### Field Override Design\n- **Use built-in overrides** for common properties like `className` and `children`\n- **Provide helpful placeholders** and descriptions\n- **Match field types** to the expected data (url, email, number, etc.)\n- **Include validation hints** in descriptions\n\n### User Experience\n- **Group related fields** logically\n- **Use appropriate input types** for better mobile experience\n- **Provide clear labels** and descriptions\n- **Test with real content** to ensure usability\n\n### Performance\n- **Memoize field override functions** to prevent unnecessary re-renders\n- **Use simple field overrides** when possible instead of custom components\n- **Debounce rapid input changes** for expensive operations\n\n### Accessibility\n- **Provide proper labels** for all form fields\n- **Include helpful descriptions** for complex fields\n- **Ensure keyboard navigation** works correctly\n- **Use semantic form elements** where appropriate" - } - ] - } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/props-panel.ts b/app/docs/docs-data/docs-page-layers/props-panel.ts deleted file mode 100644 index 6f063a7..0000000 --- a/app/docs/docs-data/docs-page-layers/props-panel.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { ComponentLayer } from "@/components/ui/ui-builder/types"; - -export const PROPS_PANEL_LAYER = { - "id": "props-panel", - "type": "div", - "name": "Props Panel", - "props": { - "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", - "data-group": "editor-features" - }, - "children": [ - { - "type": "span", - "children": "Props Panel", - "id": "props-panel-title", - "name": "Text", - "props": { - "className": "text-4xl" - } - }, - { - "id": "props-panel-intro", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "The props panel provides an intuitive interface for editing component properties. It automatically generates appropriate controls based on component Zod schemas using AutoForm, with support for custom field overrides." - }, - { - "id": "props-panel-demo", - "type": "div", - "name": "div", - "props": {}, - "children": [ - { - "id": "props-panel-badge", - "type": "Badge", - "name": "Badge", - "props": { - "variant": "default", - "className": "rounded rounded-b-none" - }, - "children": [ - { - "id": "props-panel-badge-text", - "type": "span", - "name": "span", - "props": {}, - "children": "Auto-Generated Forms" - } - ] - }, - { - "id": "props-panel-demo-frame", - "type": "div", - "name": "div", - "props": { - "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" - }, - "children": [ - { - "id": "props-panel-iframe", - "type": "iframe", - "name": "iframe", - "props": { - "src": "http://localhost:3000/examples/editor", - "title": "Props Panel Demo", - "className": "aspect-square md:aspect-video w-full" - }, - "children": [] - } - ] - } - ] - }, - { - "id": "props-panel-content", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "## Auto-Generated Controls\n\nThe props panel automatically creates appropriate input controls based on component Zod schemas:\n\n```tsx\nimport { z } from 'zod';\nimport { Button } from '@/components/ui/button';\n\n// Component registration with Zod schema\nconst componentRegistry = {\n Button: {\n component: Button,\n schema: z.object({\n variant: z.enum(['default', 'destructive', 'outline', 'secondary', 'ghost', 'link']).default('default'),\n size: z.enum(['default', 'sm', 'lg', 'icon']).default('default'),\n disabled: z.boolean().optional(),\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n from: '@/components/ui/button',\n defaultChildren: 'Click me'\n }\n};\n```\n\n## Supported Field Types\n\nAutoForm automatically generates controls for these Zod types:\n\n### Basic Types\n- **`z.string()`** - Text input\n- **`z.number()`** - Number input with step controls\n- **`z.boolean()`** - Checkbox toggle\n- **`z.date()`** - Date picker\n- **`z.enum()`** - Select dropdown\n\n### Advanced Types\n- **`z.array()`** - Array input with add/remove buttons\n- **`z.object()`** - Nested object editor\n- **`z.union()`** - Multiple type selector\n- **`z.optional()`** - Optional field with toggle\n\n### Examples\n\n```tsx\nconst advancedSchema = z.object({\n // Text with validation\n title: z.string().min(1, 'Title is required').max(100, 'Too long'),\n \n // Number with constraints\n count: z.coerce.number().min(0).max(100).default(1),\n \n // Enum for dropdowns\n size: z.enum(['sm', 'md', 'lg']).default('md'),\n \n // Optional boolean\n enabled: z.boolean().optional(),\n \n // Date input\n publishDate: z.coerce.date().optional(),\n \n // Array of objects\n items: z.array(z.object({\n name: z.string(),\n value: z.string()\n })).default([]),\n \n // Nested object\n config: z.object({\n theme: z.enum(['light', 'dark']),\n autoSave: z.boolean().default(true)\n }).optional()\n});\n```\n\n## Field Overrides\n\nCustomize the auto-generated form fields using `fieldOverrides`:\n\n```tsx\nimport { classNameFieldOverrides, childrenAsTextareaFieldOverrides } from '@/lib/ui-builder/registry/form-field-overrides';\n\nconst MyComponent = {\n component: MyComponent,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n color: z.string().default('#000000'),\n description: z.string().optional(),\n }),\n from: '@/components/my-component',\n fieldOverrides: {\n // Use built-in className editor with Tailwind suggestions\n className: (layer) => classNameFieldOverrides(layer),\n \n // Use textarea for children instead of default input\n children: (layer) => childrenAsTextareaFieldOverrides(layer),\n \n // Custom color picker\n color: {\n fieldType: 'input',\n inputProps: {\n type: 'color',\n className: 'w-full h-10'\n }\n },\n \n // Custom textarea with placeholder\n description: {\n fieldType: 'textarea',\n inputProps: {\n placeholder: 'Enter description...',\n rows: 3\n }\n }\n }\n};\n```\n\n### Built-in Field Overrides\n\nUI Builder provides several pre-built field overrides:\n\n```tsx\nimport { \n commonFieldOverrides,\n classNameFieldOverrides, \n childrenAsTextareaFieldOverrides,\n childrenFieldOverrides\n} from '@/lib/ui-builder/registry/form-field-overrides';\n\n// Apply common overrides (className + children)\nfieldOverrides: commonFieldOverrides()\n\n// Or individual overrides\nfieldOverrides: {\n className: (layer) => classNameFieldOverrides(layer),\n children: (layer) => childrenFieldOverrides(layer)\n}\n```\n\n## Variable Binding\n\nThe props panel supports variable binding for dynamic content:\n\n```tsx\n// Component with variable-bound property\nconst buttonWithVariable = {\n id: 'dynamic-button',\n type: 'Button',\n props: {\n children: { __variableRef: 'button-text-var' }, // Bound to variable\n variant: 'primary' // Static value\n }\n};\n\n// Variable definition\nconst variables = [\n {\n id: 'button-text-var',\n name: 'buttonText',\n type: 'string',\n defaultValue: 'Click me!'\n }\n];\n```\n\nVariable-bound fields show:\n- **Variable icon** indicating the binding\n- **Variable name** instead of the raw value\n- **Quick unbind** option to convert back to static value\n\n## Panel Features\n\n### Component Actions\n- **Duplicate Component** - Clone the selected component\n- **Delete Component** - Remove the component from the page\n- **Component Type** - Shows the current component type\n\n### Form Validation\n- **Real-time validation** using Zod schema constraints\n- **Error messages** displayed inline with fields\n- **Required field indicators** for mandatory properties\n\n### Responsive Design\n- **Mobile-friendly** interface with collapsible sections\n- **Touch-optimized** controls for mobile editing\n- **Adaptive layout** based on screen size\n\n## Working with Complex Components\n\n### Nested Objects\n```tsx\nconst complexSchema = z.object({\n layout: z.object({\n direction: z.enum(['row', 'column']),\n gap: z.number().default(4),\n align: z.enum(['start', 'center', 'end'])\n }),\n styling: z.object({\n background: z.string().optional(),\n border: z.boolean().default(false),\n rounded: z.boolean().default(true)\n })\n});\n```\n\n### Array Fields\n```tsx\nconst listSchema = z.object({\n items: z.array(z.object({\n label: z.string(),\n value: z.string(),\n enabled: z.boolean().default(true)\n })).default([])\n});\n```\n\n## Integration with Layer Store\n\nThe props panel integrates directly with the layer store:\n\n```tsx\n// Access props panel state\nconst selectedLayerId = useLayerStore(state => state.selectedLayerId);\nconst findLayerById = useLayerStore(state => state.findLayerById);\nconst updateLayer = useLayerStore(state => state.updateLayer);\n\n// Props panel automatically updates when:\n// - A component is selected\n// - Component properties change\n// - Variables are updated\n```\n\n## Best Practices\n\n### Schema Design\n- **Use descriptive property names** that map to actual component props\n- **Provide sensible defaults** using `.default()`\n- **Add validation** with `.min()`, `.max()`, and custom refinements\n- **Use enums** for predefined options\n\n### Field Overrides\n- **Use built-in overrides** for common props like `className` and `children`\n- **Provide helpful placeholders** and labels\n- **Consider user experience** when choosing input types\n- **Test with real content** to ensure fields work as expected\n\n### Performance\n- **Memoize field overrides** to prevent unnecessary re-renders\n- **Use specific field types** rather than generic inputs\n- **Debounce rapid changes** for better performance" - } - ] - } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/quick-start.ts b/app/docs/docs-data/docs-page-layers/quick-start.ts index 68163ee..4737978 100644 --- a/app/docs/docs-data/docs-page-layers/quick-start.ts +++ b/app/docs/docs-data/docs-page-layers/quick-start.ts @@ -23,21 +23,28 @@ export const QUICK_START_LAYER = { "type": "Markdown", "name": "Markdown", "props": {}, - "children": "Get up and running with UI Builder in minutes. This guide will walk you through installation and creating your first visual editor." + "children": "Get up and running with UI Builder in minutes. This guide covers installation, basic setup, and your first working editor." + }, + { + "id": "quick-start-compatibility", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Compatibility Notes\n\n⚠️ **Tailwind 4 + React 19**: Migration coming soon. Currently blocked by 3rd party component compatibility. If using latest shadcn/ui CLI fails, try: `npx shadcn@2.1.8 add ...`\n\n⚠️ **Server Components**: Not supported. RSC can't be re-rendered client-side for live preview. A separate RSC renderer for final page rendering is possible." }, { "id": "quick-start-install", "type": "Markdown", "name": "Markdown", "props": {}, - "children": "## Installation\n\nIf you are using shadcn/ui in your project, you can install the component directly from the registry:\n\n```bash\nnpx shadcn@latest add https://raw.githubusercontent.com/olliethedev/ui-builder/main/registry/block-registry.json\n```\n\nOr you can start a new project with the UI Builder:\n\n```bash\nnpx shadcn@latest init https://raw.githubusercontent.com/olliethedev/ui-builder/main/registry/block-registry.json\n```\n\n**Note:** You need to use [style variables](https://ui.shadcn.com/docs/theming) to have page theming working correctly.\n\n### Fixing Dependencies\n\nAdd dev dependencies, since there currently seems to be an issue with shadcn/ui not installing them from the registry:\n\n```bash\nnpm install -D @types/lodash.template @tailwindcss/typography @types/react-syntax-highlighter tailwindcss-animate @types/object-hash\n```" + "children": "## Installation\n\nIf you are using shadcn/ui in your project, install the component directly from the registry:\n\n```bash\nnpx shadcn@latest add https://raw.githubusercontent.com/olliethedev/ui-builder/main/registry/block-registry.json\n```\n\nOr start a new project with UI Builder:\n\n```bash\nnpx shadcn@latest init https://raw.githubusercontent.com/olliethedev/ui-builder/main/registry/block-registry.json\n```\n\n**Note:** You need to use [style variables](https://ui.shadcn.com/docs/theming) to have page theming working correctly.\n\n### Fix Dependencies\n\nAdd dev dependencies (current shadcn/ui registry limitation):\n\n```bash\nnpm install -D @types/lodash.template @tailwindcss/typography @types/react-syntax-highlighter tailwindcss-animate @types/object-hash\n```" }, { - "id": "quick-start-basic-usage", + "id": "quick-start-basic-setup", "type": "Markdown", "name": "Markdown", "props": {}, - "children": "## Basic Example\n\nTo use the UI Builder, you **must** provide a component registry:\n\n```tsx\nimport z from \"zod\";\nimport UIBuilder from \"@/components/ui/ui-builder\";\nimport { Button } from \"@/components/ui/button\";\nimport { ComponentRegistry, ComponentLayer } from \"@/components/ui/ui-builder/types\";\nimport { commonFieldOverrides, classNameFieldOverrides, childrenAsTextareaFieldOverrides } from \"@/lib/ui-builder/registry/form-field-overrides\";\n\n// Define your component registry\nconst myComponentRegistry: ComponentRegistry = {\n Button: {\n component: Button,\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n variant: z\n .enum([\n \"default\",\n \"destructive\",\n \"outline\",\n \"secondary\",\n \"ghost\",\n \"link\",\n ])\n .default(\"default\"),\n size: z.enum([\"default\", \"sm\", \"lg\", \"icon\"]).default(\"default\"),\n }),\n from: \"@/components/ui/button\",\n defaultChildren: [\n {\n id: \"button-text\",\n type: \"span\",\n name: \"span\",\n props: {},\n children: \"Hello World\",\n } satisfies ComponentLayer,\n ],\n fieldOverrides: commonFieldOverrides()\n },\n span: {\n schema: z.object({\n className: z.string().optional(),\n children: z.any().optional(),\n }),\n fieldOverrides: {\n className:(layer)=> classNameFieldOverrides(layer),\n children: (layer)=> childrenAsTextareaFieldOverrides(layer)\n },\n defaultChildren: \"Text\"\n },\n};\n\nexport function App() {\n return (\n \n );\n}\n```\n\n**Important:** Make sure to include definitions for all component types referenced in your `defaultChildren`. In this example, the Button's `defaultChildren` references a `span` component, so we include `span` in our registry." + "children": "## Basic Setup\n\nThe minimal setup requires just a component registry:\n\n```tsx\nimport UIBuilder from \"@/components/ui/ui-builder\";\nimport { primitiveComponentDefinitions } from \"@/lib/ui-builder/registry/primitive-component-definitions\";\nimport { complexComponentDefinitions } from \"@/lib/ui-builder/registry/complex-component-definitions\";\n\nconst componentRegistry = {\n ...primitiveComponentDefinitions, // div, span, img, etc.\n ...complexComponentDefinitions, // Button, Badge, Card, etc.\n};\n\nexport function App() {\n return (\n \n );\n}\n```\n\nThis gives you a full visual editor with pre-built shadcn/ui components." }, { "id": "quick-start-example", @@ -76,7 +83,7 @@ export const QUICK_START_LAYER = { "type": "iframe", "name": "iframe", "props": { - "src": "http://localhost:3000/examples/basic", + "src": "/examples/basic", "title": "Quick Start Example", "className": "aspect-square md:aspect-video" }, @@ -85,6 +92,81 @@ export const QUICK_START_LAYER = { ] } ] + }, + { + "id": "quick-start-with-state", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Adding State Management\n\nFor real applications, you'll want to control the initial state and persist changes:\n\n```tsx\nimport UIBuilder from \"@/components/ui/ui-builder\";\nimport { ComponentLayer, Variable } from \"@/components/ui/ui-builder/types\";\n\n// Initial page structure\nconst initialLayers: ComponentLayer[] = [\n {\n id: \"welcome-page\",\n type: \"div\",\n name: \"Welcome Page\",\n props: {\n className: \"p-8 min-h-screen flex flex-col gap-6\",\n },\n children: [\n {\n id: \"title\",\n type: \"h1\",\n name: \"Page Title\",\n props: { \n className: \"text-4xl font-bold text-center\",\n },\n children: \"Welcome to UI Builder!\",\n },\n {\n id: \"cta-button\",\n type: \"Button\",\n name: \"CTA Button\",\n props: {\n variant: \"default\",\n className: \"mx-auto w-fit\",\n },\n children: [{\n id: \"button-text\",\n type: \"span\",\n name: \"Button Text\",\n props: {},\n children: \"Get Started\",\n }],\n },\n ],\n },\n];\n\n// Variables for dynamic content\nconst initialVariables: Variable[] = [\n {\n id: \"welcome-msg\",\n name: \"welcomeMessage\",\n type: \"string\",\n defaultValue: \"Welcome to UI Builder!\"\n }\n];\n\nexport function AppWithState() {\n const handleLayersChange = (updatedLayers: ComponentLayer[]) => {\n // Save to database, localStorage, etc.\n console.log(\"Layers updated:\", updatedLayers);\n };\n\n const handleVariablesChange = (updatedVariables: Variable[]) => {\n // Save to database, localStorage, etc.\n console.log(\"Variables updated:\", updatedVariables);\n };\n\n return (\n \n );\n}\n```" + }, + { + "id": "quick-start-key-props", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## UIBuilder Props Reference\n\n### Required Props\n- **`componentRegistry`** - Maps component names to their definitions (see **Components Intro**)\n\n### Optional Props\n- **`initialLayers`** - Set initial page structure (e.g., from database)\n- **`onChange`** - Callback when pages change (for persistence)\n- **`initialVariables`** - Set initial variables for dynamic content \n- **`onVariablesChange`** - Callback when variables change\n- **`panelConfig`** - Customize editor panels (see **Panel Configuration**)\n- **`persistLayerStore`** - Enable localStorage persistence (default: `true`)\n- **`allowVariableEditing`** - Allow users to edit variables (default: `true`) \n- **`allowPagesCreation`** - Allow users to create pages (default: `true`)\n- **`allowPagesDeletion`** - Allow users to delete pages (default: `true`)\n\n**Note**: Only `componentRegistry` is required. All other props are optional and have sensible defaults." + }, + { + "id": "quick-start-rendering", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Rendering Without the Editor\n\nTo display pages in production without the editor interface, use `LayerRenderer`:\n\n```tsx\nimport LayerRenderer from \"@/components/ui/ui-builder/layer-renderer\";\n\n// Basic rendering\nexport function MyPage({ page }) {\n return (\n \n );\n}\n\n// With variables for dynamic content\nexport function DynamicPage({ page, userData }) {\n const variableValues = {\n \"welcome-msg\": `Welcome back, ${userData.name}!`\n };\n \n return (\n \n );\n}\n```\n\n🎯 **Try it**: Check out the **[Renderer Demo](/examples/renderer)** and **[Variables Demo](/examples/renderer/variables)** to see LayerRenderer in action." + }, + { + "id": "quick-start-advanced-demo", + "type": "div", + "name": "div", + "props": {}, + "children": [ + { + "id": "advanced-demo-badge", + "type": "Badge", + "name": "Badge", + "props": { + "variant": "outline", + "className": "rounded rounded-b-none" + }, + "children": [ + { + "id": "advanced-demo-badge-text", + "type": "span", + "name": "span", + "props": {}, + "children": "Full Featured Editor" + } + ] + }, + { + "id": "advanced-demo-frame", + "type": "div", + "name": "div", + "props": { + "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" + }, + "children": [ + { + "id": "advanced-demo-iframe", + "type": "iframe", + "name": "iframe", + "props": { + "src": "/examples/editor", + "title": "Full Featured Editor Demo", + "className": "aspect-square md:aspect-video" + }, + "children": [] + } + ] + } + ] + }, + { + "id": "quick-start-next-steps", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Next Steps\n\nNow that you have UI Builder running, explore these key areas:\n\n### Essential Concepts\n- **Components Intro** - Understand the component registry system\n- **Variables** - Add dynamic content with typed variables\n- **Rendering Pages** - Use LayerRenderer in your production app\n\n### Customization\n- **Custom Components** - Add your own React components to the registry\n- **Panel Configuration** - Customize the editor interface for your users\n\n### Advanced Use Cases\n- **Variable Binding** - Auto-bind components to system data\n- **Immutable Pages** - Create locked templates for consistency" } ] } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/read-only-mode.ts b/app/docs/docs-data/docs-page-layers/read-only-mode.ts index 42af8d3..9bedcaf 100644 --- a/app/docs/docs-data/docs-page-layers/read-only-mode.ts +++ b/app/docs/docs-data/docs-page-layers/read-only-mode.ts @@ -3,7 +3,7 @@ import { ComponentLayer } from "@/components/ui/ui-builder/types"; export const READ_ONLY_MODE_LAYER = { "id": "read-only-mode", "type": "div", - "name": "Read Only Mode", + "name": "Editing Restrictions", "props": { "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", "data-group": "data-variables" @@ -11,7 +11,7 @@ export const READ_ONLY_MODE_LAYER = { "children": [ { "type": "span", - "children": "Read Only Mode", + "children": "Editing Restrictions", "id": "read-only-mode-title", "name": "Text", "props": { @@ -23,14 +23,35 @@ export const READ_ONLY_MODE_LAYER = { "type": "Markdown", "name": "Markdown", "props": {}, - "children": "Control editing capabilities in UI Builder by restricting specific operations like variable editing, page creation, and page deletion. This enables read-only modes perfect for production environments, previews, and restricted editing scenarios." + "children": "Control editing capabilities in UI Builder by restricting specific operations like variable editing, page creation, and page deletion. Perfect for production environments, content-only editing, and role-based access control." }, { "id": "read-only-mode-content", "type": "Markdown", "name": "Markdown", "props": {}, - "children": "## Controlling Edit Permissions\n\nUI Builder provides granular control over editing capabilities through specific props:\n\n```tsx\n\n```\n\n### Available Permission Controls\n\n| Prop | Default | Description |\n|------|---------|-------------|\n| `allowVariableEditing` | `true` | Controls variable add/edit/delete operations |\n| `allowPagesCreation` | `true` | Controls ability to create new pages |\n| `allowPagesDeletion` | `true` | Controls ability to delete existing pages |\n\n## Read-Only Variable Mode\n\n### Disabling Variable Editing\n\nWhen `allowVariableEditing={false}`, the Variables panel becomes read-only:\n\n```tsx\nfunction ReadOnlyVariablesExample() {\n const systemVariables = [\n {\n id: 'company-name',\n name: 'companyName',\n type: 'string',\n defaultValue: 'Acme Corp'\n },\n {\n id: 'brand-color',\n name: 'brandColor',\n type: 'string',\n defaultValue: '#3b82f6'\n }\n ];\n\n return (\n \n );\n}\n```\n\n### What's Disabled in Read-Only Variable Mode\n\n- ❌ **Add Variable** button is hidden\n- ❌ **Edit Variable** buttons are hidden on variable cards\n- ❌ **Delete Variable** buttons are hidden on variable cards\n- ✅ **Variable binding** still works in props panel\n- ✅ **Variable values** can still be overridden in LayerRenderer\n- ✅ **Immutable bindings** remain enforced\n\n## Read-Only Pages Mode\n\n### Restricting Page Operations\n\n```tsx\nfunction RestrictedPagesExample() {\n const templatePages = [\n {\n id: 'home-template',\n type: 'div',\n name: 'Home Template',\n props: { className: 'p-4' },\n children: []\n },\n {\n id: 'about-template',\n type: 'div',\n name: 'About Template',\n props: { className: 'p-4' },\n children: []\n }\n ];\n\n return (\n \n );\n}\n```\n\n### Page Restriction Effects\n\nWith `allowPagesCreation={false}`:\n- ❌ **Add Page** functionality is disabled\n- ✅ **Page content editing** remains available\n- ✅ **Page switching** between existing pages works\n\nWith `allowPagesDeletion={false}`:\n- ❌ **Delete Page** buttons are hidden in props panel\n- ✅ **Page content editing** remains available\n- ✅ **Page duplication** may still work (creates duplicates, doesn't delete)\n\n## Complete Read-Only Mode\n\n### Fully Restricted Editor\n\nFor maximum restrictions, disable all editing capabilities:\n\n```tsx\nfunction FullyReadOnlyEditor() {\n return (\n \n );\n}\n```\n\n### What Still Works in Full Read-Only Mode\n\n- ✅ **Component selection** and navigation\n- ✅ **Visual editing** of component properties\n- ✅ **Layer manipulation** (add, remove, reorder components)\n- ✅ **Variable binding** in props panel\n- ✅ **Theme configuration** in appearance panel\n- ✅ **Code generation** and export\n- ✅ **Undo/Redo** operations\n\n## Use Cases and Patterns\n\n### Production Preview Mode\n\n```tsx\nfunction ProductionPreview({ templateId }) {\n const [template, setTemplate] = useState(null);\n const [variables, setVariables] = useState([]);\n\n useEffect(() => {\n // Load template and variables from API\n Promise.all([\n fetch(`/api/templates/${templateId}`).then(r => r.json()),\n fetch(`/api/templates/${templateId}/variables`).then(r => r.json())\n ]).then(([templateData, variableData]) => {\n setTemplate(templateData);\n setVariables(variableData);\n });\n }, [templateId]);\n\n if (!template) return
Loading...
;\n\n return (\n
\n
\n

Template Preview

\n

Read-only mode - variables locked

\n
\n \n \n
\n );\n}\n```\n\n### Role-Based Editing Restrictions\n\n```tsx\nfunction RoleBasedEditor({ user, template }) {\n const canEditVariables = user.role === 'admin' || user.role === 'developer';\n const canManagePages = user.role === 'admin';\n\n return (\n \n );\n}\n```\n\n### Content Editor Mode\n\n```tsx\nfunction ContentEditorMode() {\n // Content editors can modify component content but not structure\n return (\n \n );\n}\n```\n\n### Environment-Based Restrictions\n\n```tsx\nfunction EnvironmentAwareEditor() {\n const isProduction = process.env.NODE_ENV === 'production';\n const isDevelopment = process.env.NODE_ENV === 'development';\n \n return (\n \n );\n}\n```\n\n## Rendering Without Editor\n\n### Using LayerRenderer for Display-Only\n\nFor pure display without any editing interface, use `LayerRenderer`:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\n\nfunction DisplayOnlyPage({ pageData, variables, userValues }) {\n return (\n \n );\n}\n```\n\n### LayerRenderer vs. Restricted UIBuilder\n\n| Feature | LayerRenderer | Restricted UIBuilder |\n|---------|---------------|----------------------|\n| Bundle size | Smaller (no editor) | Larger (full editor) |\n| Performance | Faster (no editor overhead) | Slower (editor present) |\n| Editing UI | None | Present but restricted |\n| Variable binding | ✅ | ✅ |\n| Code generation | ❌ | ✅ |\n| Visual editing | ❌ | ✅ (limited) |\n\n## Programmatic Control\n\n### Dynamic Permission Updates\n\n```tsx\nfunction DynamicPermissionsEditor() {\n const [permissions, setPermissions] = useState({\n allowVariableEditing: false,\n allowPagesCreation: false,\n allowPagesDeletion: false\n });\n\n const enableEditMode = () => {\n setPermissions({\n allowVariableEditing: true,\n allowPagesCreation: true,\n allowPagesDeletion: true\n });\n };\n\n const enableReadOnlyMode = () => {\n setPermissions({\n allowVariableEditing: false,\n allowPagesCreation: false,\n allowPagesDeletion: false\n });\n };\n\n return (\n
\n
\n \n \n
\n \n \n
\n );\n}\n```\n\n### Feature Flag Integration\n\n```tsx\nfunction FeatureFlagEditor({ featureFlags }) {\n return (\n \n );\n}\n```\n\n## Security Considerations\n\n### Variable Security\n\n```tsx\n// Secure sensitive variables from editing\nconst secureVariables = [\n {\n id: 'api-key',\n name: 'apiKey',\n type: 'string',\n defaultValue: 'sk_live_...'\n },\n {\n id: 'user-permissions',\n name: 'userPermissions',\n type: 'string',\n defaultValue: 'read-only'\n }\n];\n\nfunction SecureEditor() {\n return (\n \n );\n}\n```\n\n### Input Validation\n\n```tsx\nfunction ValidatedEditor({ initialData }) {\n // Validate and sanitize data before passing to UI Builder\n const sanitizedPages = sanitizePageData(initialData.pages);\n const validatedVariables = validateVariables(initialData.variables);\n \n return (\n \n );\n}\n```\n\n## Best Practices\n\n### Choosing the Right Restrictions\n\n- **Use `allowVariableEditing={false}`** for production deployments\n- **Use `allowPagesCreation={false}`** for content-only editing\n- **Use `allowPagesDeletion={false}`** to prevent accidental page loss\n- **Use `LayerRenderer`** for pure display without editing needs\n\n### User Experience Considerations\n\n- **Provide clear feedback** about restricted functionality\n- **Use role-based restrictions** rather than blanket restrictions\n- **Consider progressive permissions** (unlock features as users gain trust)\n- **Document restriction reasons** for transparency\n\n### Performance Optimization\n\n- **Use `LayerRenderer`** when editing isn't needed\n- **Minimize editor bundle** in production builds\n- **Cache restricted configurations** to avoid re-computation\n- **Consider server-side rendering** for display-only scenarios" + "children": "## Permission Control Props\n\nUI Builder provides three boolean props to control editing permissions:\n\n```tsx\n\n```\n\n| Prop | Default | Description |\n|------|---------|-------------|\n| `allowVariableEditing` | `true` | Controls variable add/edit/delete in Variables panel |\n| `allowPagesCreation` | `true` | Controls ability to create new pages |\n| `allowPagesDeletion` | `true` | Controls ability to delete existing pages |\n\n## Interactive Demo\n\nExperience all read-only modes in one interactive demo:\n\n- **Live Demo:** [/examples/editor/read-only-mode](/examples/editor/read-only-mode)\n- **Features:** Switch between different permission levels in real-time\n- **Modes:** Full editing, content-only, no variables, and full read-only\n- **What to try:** Toggle between modes to see UI changes and restrictions" + }, + { + "id": "demo-iframe", + "type": "iframe", + "name": "Read-Only Demo Iframe", + "props": { + "src": "/examples/editor/read-only-mode", + "width": "100%", + "height": "600", + "frameBorder": "0", + "className": "w-full border border-gray-200 rounded-lg shadow-sm mb-6", + "title": "UI Builder Read-Only Mode Interactive Demo" + }, + "children": [] + }, + { + "id": "read-only-mode-content-continued", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Common Use Cases\n\n### Content-Only Editing\n\nAllow content editing while preventing structural changes:\n\n```tsx\n\n```\n\n**Use case:** Content teams updating copy, images, and variable content without changing page layouts.\n\n### Production Preview Mode\n\nLock down all structural changes:\n\n```tsx\n\n```\n\n**Use case:** Previewing templates in production with system-controlled variables.\n\n### Role-Based Access Control\n\nDifferent permissions based on user roles:\n\n```tsx\nfunction RoleBasedEditor({ user, template }) {\n const canEditVariables = user.role === 'admin' || user.role === 'developer';\n const canManagePages = user.role === 'admin';\n\n return (\n \n );\n}\n```\n\n## Variable Binding in Templates\n\nWhen creating templates with variable references, use the correct format:\n\n```tsx\n// ✅ Correct: Variable binding in component props\nconst templateLayer: ComponentLayer = {\n id: \"title\",\n type: \"span\",\n name: \"Title\",\n props: {\n className: \"text-2xl font-bold\",\n children: { __variableRef: \"pageTitle\" } // Correct format\n },\n children: []\n};\n\n// ❌ Incorrect: Variable binding directly in children\nconst badTemplate: ComponentLayer = {\n id: \"title\",\n type: \"span\",\n name: \"Title\",\n props: {\n className: \"text-2xl font-bold\"\n },\n children: { __variableRef: \"pageTitle\" } // Wrong - causes TypeScript errors\n};\n```\n\n**Key points:**\n- Variable references go in the `props` object\n- Use `__variableRef` (without quotes) as the property name\n- The value is the variable ID as a string\n- Set `children: []` when using variable binding\n\n## Additional Examples\n\n### Fixed Pages Example\n\nSee the immutable pages example that demonstrates locked page structure:\n\n- **Live Demo:** [/examples/editor/immutable-pages](/examples/editor/immutable-pages)\n- **Implementation:** Uses `allowPagesCreation={false}` and `allowPagesDeletion={false}`\n- **What's locked:** Page creation and deletion\n- **What works:** Content editing, component manipulation, theme changes\n\n### Variable Read-Only Example\n\nSee the immutable bindings example that demonstrates locked variables:\n\n- **Live Demo:** [/examples/editor/immutable-bindings](/examples/editor/immutable-bindings)\n- **Implementation:** Uses `allowVariableEditing={false}`\n- **What's locked:** Variable creation, editing, and deletion\n- **What works:** Variable binding in props panel, visual component editing\n\n## What's Still Available in Read-Only Mode\n\nEven with restrictions enabled, users can still:\n\n✅ **Visual Component Editing:** Add, remove, and modify components on the canvas \n✅ **Props Panel:** Configure component properties and bind to existing variables \n✅ **Appearance Panel:** Modify themes and styling \n✅ **Layer Navigation:** Select and organize components in the layers panel \n✅ **Undo/Redo:** Full history navigation \n✅ **Code Generation:** Export React code \n\n## When to Use LayerRenderer Instead\n\nFor pure display without any editing interface, use `LayerRenderer`:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\n\nfunction DisplayOnlyPage({ pageData, variables, userValues }) {\n return (\n \n );\n}\n```\n\n**Choose LayerRenderer when:**\n- No editing interface needed\n- Smaller bundle size required \n- Better performance for display-only scenarios\n- Rendering with dynamic data at runtime\n\n**Choose restricted UIBuilder when:**\n- Some editing capabilities needed\n- Code generation features required\n- Visual interface helps with content understanding\n- Fine-grained permission control needed\n\n## Implementation Pattern\n\n```tsx\nfunction ConfigurableEditor({ \n template, \n user, \n environment \n}) {\n const permissions = {\n allowVariableEditing: environment !== 'production' && user.canEditVariables,\n allowPagesCreation: user.role === 'admin',\n allowPagesDeletion: user.role === 'admin'\n };\n\n return (\n \n );\n}\n```\n\n## Best Practices\n\n### Security Considerations\n\n- **Validate data server-side:** Client-side restrictions are for UX, not security\n- **Sanitize inputs:** Always validate and sanitize layer data and variables\n- **Use immutable bindings:** For system variables that must never change\n- **Implement proper authentication:** Control access at the application level\n\n### User Experience\n\n- **Provide clear feedback:** Show users what's restricted and why\n- **Progressive permissions:** Unlock features as users gain trust/experience\n- **Contextual help:** Explain restrictions in context\n- **Consistent behavior:** Apply restrictions predictably across the interface\n\n### Performance\n\n- **Use LayerRenderer for display-only:** Smaller bundle, better performance\n- **Cache configurations:** Avoid re-computing permissions on every render\n- **Optimize initial data:** Only load necessary variables and pages\n- **Consider lazy loading:** Load restricted features only when needed" } ] } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/rendering-pages.ts b/app/docs/docs-data/docs-page-layers/rendering-pages.ts index 6dd4d15..d9b8ccb 100644 --- a/app/docs/docs-data/docs-page-layers/rendering-pages.ts +++ b/app/docs/docs-data/docs-page-layers/rendering-pages.ts @@ -23,7 +23,7 @@ export const RENDERING_PAGES_LAYER = { "type": "Markdown", "name": "Markdown", "props": {}, - "children": "Render UI Builder pages without the editor interface using the LayerRenderer component. Perfect for displaying your designs in production with full variable binding and dynamic content support." + "children": "Render UI Builder pages in production using the `LayerRenderer` component. Display your designed pages without the editor interface, with full support for dynamic content through variables." }, { "id": "rendering-pages-demo", @@ -45,7 +45,7 @@ export const RENDERING_PAGES_LAYER = { "type": "span", "name": "span", "props": {}, - "children": "Live Rendering Demo" + "children": "Basic Rendering Demo" } ] }, @@ -124,7 +124,7 @@ export const RENDERING_PAGES_LAYER = { "type": "Markdown", "name": "Markdown", "props": {}, - "children": "## Basic Rendering\n\nUse the `LayerRenderer` component to render UI Builder pages without the editor:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\nimport { ComponentLayer, ComponentRegistry } from '@/components/ui/ui-builder/types';\n\n// Your component registry (same as used in UIBuilder)\nconst myComponentRegistry: ComponentRegistry = {\n // Your component definitions\n};\n\n// Page data (from UIBuilder or database)\nconst page: ComponentLayer = {\n id: \"my-page\",\n type: \"div\",\n name: \"My Page\",\n props: {\n className: \"p-4\"\n },\n children: [\n // Your page structure\n ]\n};\n\nfunction MyRenderedPage() {\n return (\n \n );\n}\n```\n\n## Rendering with Variables\n\nThe real power of LayerRenderer comes from variable binding - same page structure with different data:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\nimport { ComponentLayer, Variable } from '@/components/ui/ui-builder/types';\n\n// Define variables for dynamic content\nconst variables: Variable[] = [\n {\n id: \"userName\",\n name: \"User Name\",\n type: \"string\",\n defaultValue: \"John Doe\"\n },\n {\n id: \"userAge\",\n name: \"User Age\", \n type: \"number\",\n defaultValue: 25\n },\n {\n id: \"isActive\",\n name: \"Is Active\",\n type: \"boolean\",\n defaultValue: true\n }\n];\n\n// Page with variable bindings\nconst pageWithVariables: ComponentLayer = {\n id: \"user-profile\",\n type: \"div\",\n props: {\n className: \"p-6 bg-white rounded-lg shadow\"\n },\n children: [\n {\n id: \"welcome-text\",\n type: \"h1\",\n props: {\n className: \"text-2xl font-bold\",\n children: { __variableRef: \"userName\" } // Bound to userName variable\n },\n children: []\n },\n {\n id: \"age-text\",\n type: \"p\",\n props: {\n children: { __variableRef: \"userAge\" } // Bound to userAge variable\n },\n children: []\n }\n ]\n};\n\n// Override variable values at runtime\nconst variableValues = {\n userName: \"Jane Smith\", // Override default\n userAge: 30, // Override default\n isActive: false // Override default\n};\n\nfunction DynamicUserProfile() {\n return (\n \n );\n}\n```\n\n## Multi-Tenant Applications\n\nPerfect for white-label applications where each customer gets customized branding:\n\n```tsx\nfunction CustomerDashboard({ customerId }: { customerId: string }) {\n const [pageData, setPageData] = useState(null);\n const [customerVariables, setCustomerVariables] = useState({});\n \n useEffect(() => {\n async function loadCustomerPage() {\n // Load the page structure (same for all customers)\n const pageResponse = await fetch('/api/templates/dashboard');\n const page = await pageResponse.json();\n \n // Load customer-specific variable values\n const varsResponse = await fetch(`/api/customers/${customerId}/branding`);\n const variables = await varsResponse.json();\n \n setPageData(page);\n setCustomerVariables(variables);\n }\n \n loadCustomerPage();\n }, [customerId]);\n \n if (!pageData) return
Loading...
;\n \n return (\n \n );\n}\n```\n\n## Server-Side Rendering (SSR)\n\nLayerRenderer works with Next.js SSR for better performance and SEO:\n\n```tsx\n// pages/page/[id].tsx or app/page/[id]/page.tsx\nimport { GetServerSideProps } from 'next';\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\n\ninterface PageProps {\n page: ComponentLayer;\n variables: Variable[];\n variableValues: Record;\n}\n\n// Server-side data fetching\nexport const getServerSideProps: GetServerSideProps = async ({ params }) => {\n const pageId = params?.id as string;\n \n // Fetch page data from your database\n const [page, variables, userData] = await Promise.all([\n getPageById(pageId),\n getPageVariables(pageId),\n getCurrentUserData() // For personalization\n ]);\n \n const variableValues = {\n userName: userData.name,\n userEmail: userData.email,\n // Inject real data into variables\n };\n \n return {\n props: {\n page,\n variables,\n variableValues\n }\n };\n};\n\n// Component renders on server\nfunction ServerRenderedPage({ page, variables, variableValues }: PageProps) {\n return (\n \n );\n}\n\nexport default ServerRenderedPage;\n```\n\n## Real-Time Data Integration\n\nBind to live data sources for dynamic, real-time interfaces:\n\n```tsx\nfunction LiveDashboard() {\n const [liveData, setLiveData] = useState({\n activeUsers: 0,\n revenue: 0,\n conversionRate: 0\n });\n \n // Subscribe to real-time updates\n useEffect(() => {\n const socket = new WebSocket('ws://localhost:8080/analytics');\n \n socket.onmessage = (event) => {\n const data = JSON.parse(event.data);\n setLiveData(data);\n };\n \n return () => socket.close();\n }, []);\n \n return (\n \n );\n}\n```\n\n## A/B Testing & Feature Flags\n\nUse boolean variables for conditional rendering:\n\n```tsx\nfunction ABTestPage({ userId }: { userId: string }) {\n const [experimentFlags, setExperimentFlags] = useState({});\n \n useEffect(() => {\n // Determine which experiment variant user should see\n async function getExperimentFlags() {\n const response = await fetch(`/api/experiments/${userId}`);\n const flags = await response.json();\n setExperimentFlags(flags);\n }\n \n getExperimentFlags();\n }, [userId]);\n \n return (\n \n );\n}\n```\n\n## LayerRenderer Props Reference\n\n- **`page`** (required): The ComponentLayer to render\n- **`componentRegistry`** (required): Registry of available components\n- **`className`**: CSS class for the root container\n- **`variables`**: Array of Variable definitions for the page\n- **`variableValues`**: Object mapping variable IDs to runtime values\n- **`editorConfig`**: Internal editor configuration (rarely needed)\n\n## Performance Optimization\n\nOptimize rendering performance for large pages:\n\n```tsx\n// Memoize the renderer to prevent unnecessary re-renders\nconst MemoizedRenderer = React.memo(LayerRenderer, (prevProps, nextProps) => {\n return (\n prevProps.page === nextProps.page &&\n JSON.stringify(prevProps.variableValues) === JSON.stringify(nextProps.variableValues)\n );\n});\n\n// Use in your component\nfunction OptimizedPage() {\n return (\n \n );\n}\n```\n\n## Error Handling\n\nHandle rendering errors gracefully:\n\n```tsx\nimport { ErrorBoundary } from 'react-error-boundary';\n\nfunction ErrorFallback({ error }: { error: Error }) {\n return (\n
\n

Something went wrong

\n

{error.message}

\n \n
\n );\n}\n\nfunction SafeRenderedPage() {\n return (\n \n \n \n );\n}\n```\n\n## Best Practices\n\n1. **Always use the same componentRegistry** in both UIBuilder and LayerRenderer\n2. **Validate variable values** before passing to LayerRenderer to prevent runtime errors\n3. **Handle loading states** while fetching page data and variables\n4. **Use memoization** for expensive variable calculations\n5. **Implement error boundaries** to gracefully handle rendering failures\n6. **Consider caching** page data and variable values for better performance\n7. **Test with different variable combinations** to ensure your pages are robust" + "children": "## Basic Usage\n\nUse the `LayerRenderer` component to display UI Builder pages without the editor interface:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\nimport { ComponentLayer, ComponentRegistry } from '@/components/ui/ui-builder/types';\n\n// Your component registry (same as used in UIBuilder)\nconst myComponentRegistry: ComponentRegistry = {\n // Your component definitions...\n};\n\n// Page data from UIBuilder or database\nconst page: ComponentLayer = {\n id: \"welcome-page\",\n type: \"div\",\n name: \"Welcome Page\",\n props: {\n className: \"p-6 max-w-4xl mx-auto\"\n },\n children: [\n {\n id: \"title\",\n type: \"h1\",\n name: \"Title\",\n props: {\n className: \"text-3xl font-bold mb-4\"\n },\n children: \"Welcome to My App\"\n },\n {\n id: \"description\",\n type: \"p\",\n name: \"Description\",\n props: {\n className: \"text-gray-600\"\n },\n children: \"This page was built with UI Builder.\"\n }\n ]\n};\n\nfunction MyRenderedPage() {\n return (\n \n );\n}\n```\n\n## Rendering with Variables\n\nMake your pages dynamic by binding component properties to variables:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\nimport { Variable } from '@/components/ui/ui-builder/types';\n\n// Define your variables\nconst variables: Variable[] = [\n {\n id: \"userName\",\n name: \"User Name\",\n type: \"string\",\n defaultValue: \"Guest\"\n },\n {\n id: \"userAge\",\n name: \"User Age\",\n type: \"number\",\n defaultValue: 25\n },\n {\n id: \"showWelcomeMessage\",\n name: \"Show Welcome Message\",\n type: \"boolean\",\n defaultValue: true\n }\n];\n\n// Page with variable bindings (created in UIBuilder)\nconst pageWithVariables: ComponentLayer = {\n id: \"user-profile\",\n type: \"div\",\n props: {\n className: \"p-6 bg-white rounded-lg shadow\"\n },\n children: [\n {\n id: \"welcome-message\",\n type: \"h2\",\n props: {\n className: \"text-2xl font-bold mb-2\",\n children: { __variableRef: \"userName\" } // Bound to userName variable\n },\n children: []\n },\n {\n id: \"age-display\",\n type: \"p\",\n props: {\n className: \"text-gray-600\",\n children: { __variableRef: \"userAge\" } // Bound to userAge variable\n },\n children: []\n }\n ]\n};\n\n// Provide runtime values for variables\nconst variableValues = {\n userName: \"Jane Smith\",\n userAge: 28,\n showWelcomeMessage: true\n};\n\nfunction DynamicUserProfile() {\n return (\n \n );\n}\n```\n\n## Production Integration\n\nIntegrate with your data sources to create personalized experiences:\n\n```tsx\nfunction CustomerPage({ customerId }: { customerId: string }) {\n const [pageData, setPageData] = useState(null);\n const [customerData, setCustomerData] = useState({});\n \n useEffect(() => {\n async function loadData() {\n // Load page structure from your CMS/database\n const pageResponse = await fetch('/api/pages/customer-dashboard');\n const page = await pageResponse.json();\n \n // Load customer-specific data\n const customerResponse = await fetch(`/api/customers/${customerId}`);\n const customer = await customerResponse.json();\n \n setPageData(page);\n setCustomerData(customer);\n }\n \n loadData();\n }, [customerId]);\n \n if (!pageData) return
Loading...
;\n \n return (\n \n );\n}\n```\n\n## Performance Optimization\n\nOptimize rendering performance for production:\n\n```tsx\n// Memoize the renderer to prevent unnecessary re-renders\nconst MemoizedRenderer = React.memo(LayerRenderer, (prevProps, nextProps) => {\n return (\n prevProps.page === nextProps.page &&\n JSON.stringify(prevProps.variableValues) === JSON.stringify(nextProps.variableValues)\n );\n});\n\n// Use in your component\nfunction OptimizedPage() {\n return (\n \n );\n}\n```\n\n## Error Handling\n\nHandle rendering errors gracefully in production:\n\n```tsx\nimport { ErrorBoundary } from 'react-error-boundary';\n\nfunction ErrorFallback({ error }: { error: Error }) {\n return (\n
\n

Page failed to load

\n

{error.message}

\n \n
\n );\n}\n\nfunction SafeRenderedPage() {\n return (\n \n \n \n );\n}\n```\n\n## LayerRenderer Props\n\n- **`page`** (required): The `ComponentLayer` to render\n- **`componentRegistry`** (required): Registry mapping component types to their definitions\n- **`className`**: Optional CSS class for the root container\n- **`variables`**: Array of `Variable` definitions available for binding\n- **`variableValues`**: Object mapping variable IDs to runtime values (overrides defaults)\n- **`editorConfig`**: Internal editor configuration (rarely needed in production)\n\n## Best Practices\n\n1. **Use the same `componentRegistry`** in both `UIBuilder` and `LayerRenderer`\n2. **Validate variable values** before passing to LayerRenderer to prevent runtime errors\n3. **Handle loading states** while fetching page data and variables\n4. **Implement error boundaries** to gracefully handle rendering failures\n5. **Cache page data** when possible for better performance\n6. **Memoize expensive variable calculations** to avoid unnecessary re-computations\n7. **Test variable bindings** thoroughly to ensure robustness across different data scenarios" } ] } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/variable-binding.ts b/app/docs/docs-data/docs-page-layers/variable-binding.ts index fe9df8c..bbfe66c 100644 --- a/app/docs/docs-data/docs-page-layers/variable-binding.ts +++ b/app/docs/docs-data/docs-page-layers/variable-binding.ts @@ -23,14 +23,61 @@ export const VARIABLE_BINDING_LAYER = { "type": "Markdown", "name": "Markdown", "props": {}, - "children": "Variable binding connects dynamic data to component properties, enabling interfaces that update automatically when variable values change. Learn how to bind variables through the UI and programmatically." + "children": "Learn how to connect variables to component properties through the UI and programmatically. This page focuses on the binding mechanics—see **Variables** for fundamentals and **Data Binding** for external data integration." + }, + { + "id": "variable-binding-demo", + "type": "div", + "name": "div", + "props": {}, + "children": [ + { + "id": "variable-binding-badge", + "type": "Badge", + "name": "Badge", + "props": { + "variant": "default", + "className": "rounded rounded-b-none" + }, + "children": [ + { + "id": "variable-binding-badge-text", + "type": "span", + "name": "span", + "props": {}, + "children": "Live Variable Binding Example" + } + ] + }, + { + "id": "variable-binding-demo-frame", + "type": "div", + "name": "div", + "props": { + "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" + }, + "children": [ + { + "id": "variable-binding-iframe", + "type": "iframe", + "name": "iframe", + "props": { + "src": "/examples/renderer/variables", + "title": "Variable Binding Demo", + "className": "aspect-square md:aspect-video w-full" + }, + "children": [] + } + ] + } + ] }, { "id": "variable-binding-content", "type": "Markdown", "name": "Markdown", "props": {}, - "children": "## How Variable Binding Works\n\nVariable binding in UI Builder replaces static property values with dynamic references to variables. When a component renders, these references are resolved to actual values.\n\n### Binding Structure\n\nWhen bound, a component property stores a variable reference object:\n\n```tsx\n// Before binding - static value\nconst button = {\n props: {\n children: 'Click me',\n disabled: false\n }\n};\n\n// After binding - variable references\nconst button = {\n props: {\n children: { __variableRef: 'button-text-var' },\n disabled: { __variableRef: 'is-loading-var' }\n }\n};\n```\n\n## Binding Variables Through the UI\n\n### Step-by-Step Binding Process\n\n1. **Select a component** in the editor canvas\n2. **Open the Properties panel** (right sidebar)\n3. **Find the property** you want to bind\n4. **Click the link icon** (🔗) next to the property field\n5. **Choose a variable** from the dropdown menu\n6. **The property is now bound** and shows the variable info\n\n### Visual Indicators\n\nBound properties are visually distinct in the props panel:\n\n- **Link icon** indicates the property supports binding\n- **Variable card** shows when a property is bound\n- **Variable name and type** are displayed\n- **Current value** shows the variable's default/resolved value\n- **Unlink button** allows unbinding (if not immutable)\n- **Lock icon** indicates immutable bindings\n\n### Unbinding Variables\n\nTo remove a variable binding:\n\n1. **Select the component** with bound properties\n2. **Find the bound property** in the props panel\n3. **Click the unlink icon** (🔗⛌) next to the variable card\n4. **Property reverts** to its default schema value\n\n**Note:** Immutable bindings (marked with 🔒) cannot be unbound through the UI.\n\n## Variable Resolution at Runtime\n\n### In LayerRenderer\n\nWhen rendering pages, variable references are resolved to actual values:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\n\n// Page with variable bindings\nconst pageWithBindings = {\n id: 'welcome-page',\n type: 'div',\n props: { className: 'p-4' },\n children: [\n {\n id: 'welcome-button',\n type: 'Button',\n props: {\n children: { __variableRef: 'welcome-message' },\n disabled: { __variableRef: 'is-loading' }\n }\n }\n ]\n};\n\n// Variables definition\nconst variables = [\n {\n id: 'welcome-message',\n name: 'welcomeMessage',\n type: 'string',\n defaultValue: 'Welcome!'\n },\n {\n id: 'is-loading',\n name: 'isLoading',\n type: 'boolean',\n defaultValue: false\n }\n];\n\n// Runtime values override defaults\nconst variableValues = {\n 'welcome-message': 'Hello, Jane!',\n 'is-loading': true\n};\n\nfunction MyPage() {\n return (\n \n );\n}\n\n// Renders as:\n// \n```\n\n### Resolution Process\n\n1. **Scan component props** for variable reference objects\n2. **Look up variable by ID** in the variables array\n3. **Use runtime value** from `variableValues` if provided\n4. **Fall back to default value** from variable definition\n5. **Replace reference** with resolved value\n6. **Pass resolved props** to React component\n\n## Automatic Variable Binding\n\n### Default Variable Bindings\n\nComponents can automatically bind to variables when added to the canvas:\n\n```tsx\nconst componentRegistry = {\n UserCard: {\n component: UserCard,\n schema: z.object({\n userId: z.string(),\n displayName: z.string(),\n avatarUrl: z.string().optional(),\n isOnline: z.boolean().default(false)\n }),\n from: '@/components/ui/user-card',\n defaultVariableBindings: [\n {\n propName: 'userId',\n variableId: 'current-user-id',\n immutable: true // Cannot be unbound\n },\n {\n propName: 'displayName',\n variableId: 'current-user-name',\n immutable: false // Can be changed\n },\n {\n propName: 'isOnline',\n variableId: 'user-online-status',\n immutable: true\n }\n ]\n }\n};\n```\n\n### Immutable Bindings\n\nImmutable bindings provide several benefits:\n\n- **System consistency** - Critical data cannot be accidentally unbound\n- **Security** - User permissions and IDs remain locked\n- **Branding** - Company logos and colors stay consistent\n- **Template integrity** - Essential bindings are preserved\n\n```tsx\n// Example: Brand-consistent button component\nconst BrandButton = {\n component: Button,\n schema: z.object({\n children: z.string(),\n style: z.object({\n backgroundColor: z.string(),\n color: z.string()\n }).optional()\n }),\n defaultVariableBindings: [\n {\n propName: 'style.backgroundColor',\n variableId: 'brand-primary-color',\n immutable: true // Locked to brand colors\n },\n {\n propName: 'style.color',\n variableId: 'brand-text-color',\n immutable: true\n }\n // children prop is left unbound for flexibility\n ]\n};\n```\n\n## Variable Binding in Code Generation\n\nWhen generating React code, variable bindings are converted to prop references:\n\n```tsx\n// Original component layer with bindings\nconst buttonLayer = {\n type: 'Button',\n props: {\n children: { __variableRef: 'button-text' },\n disabled: { __variableRef: 'is-disabled' },\n variant: 'primary' // Static value\n }\n};\n\n// Generated React code\ninterface PageProps {\n variables: {\n buttonText: string;\n isDisabled: boolean;\n };\n}\n\nconst Page = ({ variables }: PageProps) => {\n return (\n \n );\n};\n```\n\n## Managing Variable Bindings Programmatically\n\n### Using Layer Store Methods\n\n```tsx\nimport { useLayerStore } from '@/lib/ui-builder/store/layer-store';\n\nfunction CustomBindingControl() {\n const bindPropToVariable = useLayerStore((state) => state.bindPropToVariable);\n const unbindPropFromVariable = useLayerStore((state) => state.unbindPropFromVariable);\n const isBindingImmutable = useLayerStore((state) => state.isBindingImmutable);\n\n const handleBind = () => {\n // Bind a component's 'title' prop to a variable\n bindPropToVariable('button-123', 'title', 'page-title-var');\n };\n\n const handleUnbind = () => {\n // Check if binding is immutable first\n if (!isBindingImmutable('button-123', 'title')) {\n unbindPropFromVariable('button-123', 'title');\n }\n };\n\n return (\n
\n \n \n
\n );\n}\n```\n\n### Variable Reference Detection\n\n```tsx\nimport { isVariableReference } from '@/lib/ui-builder/utils/variable-resolver';\n\n// Check if a prop value is a variable reference\nconst propValue = layer.props.children;\n\nif (isVariableReference(propValue)) {\n console.log('Bound to variable:', propValue.__variableRef);\n} else {\n console.log('Static value:', propValue);\n}\n```\n\n## Advanced Binding Patterns\n\n### Conditional Property Binding\n\n```tsx\n// Use boolean variables to control component behavior\nconst variables = [\n {\n id: 'show-avatar',\n name: 'showAvatar',\n type: 'boolean',\n defaultValue: true\n },\n {\n id: 'user-role',\n name: 'userRole',\n type: 'string',\n defaultValue: 'user'\n }\n];\n\n// Bind to component properties\nconst userCard = {\n type: 'UserCard',\n props: {\n showAvatar: { __variableRef: 'show-avatar' },\n role: { __variableRef: 'user-role' }\n }\n};\n```\n\n### Multi-Component Binding\n\n```tsx\n// Bind the same variable to multiple components\nconst themeVariable = {\n id: 'current-theme',\n name: 'currentTheme',\n type: 'string',\n defaultValue: 'light'\n};\n\n// Multiple components can reference the same variable\nconst header = {\n type: 'Header',\n props: {\n theme: { __variableRef: 'current-theme' }\n }\n};\n\nconst sidebar = {\n type: 'Sidebar',\n props: {\n theme: { __variableRef: 'current-theme' }\n }\n};\n\nconst footer = {\n type: 'Footer',\n props: {\n theme: { __variableRef: 'current-theme' }\n }\n};\n```\n\n## Variable Binding Best Practices\n\n### Design Patterns\n\n- **Use meaningful variable names** that clearly indicate their purpose\n- **Group related variables** (e.g., user data, theme settings, feature flags)\n- **Set appropriate default values** for better editor preview experience\n- **Document variable purposes** in component registry definitions\n- **Use immutable bindings** for system-critical or brand-related data\n\n### Performance Considerations\n\n- **Variable resolution is optimized** through memoization in the rendering process\n- **Only bound properties** are processed during variable resolution\n- **Static values** are passed through without processing overhead\n- **Variable updates** trigger efficient re-renders only for affected components\n\n### Debugging Tips\n\n```tsx\n// Check variable bindings in browser dev tools\nconst layer = useLayerStore.getState().findLayerById('my-component');\nconsole.log('Layer props:', layer?.props);\n\n// Verify variable resolution\nimport { resolveVariableReferences } from '@/lib/ui-builder/utils/variable-resolver';\n\nconst resolved = resolveVariableReferences(\n layer.props,\n variables,\n variableValues\n);\nconsole.log('Resolved props:', resolved);\n```\n\n## Troubleshooting Common Issues\n\n### Variable Not Found\n\n- **Check variable ID** matches exactly in both definition and reference\n- **Verify variable exists** in the variables array\n- **Ensure variable scope** (editor vs. renderer) includes the needed variable\n\n### Binding Not Working\n\n- **Confirm variable reference format** uses `{ __variableRef: 'variable-id' }`\n- **Check variable type compatibility** with component prop expectations\n- **Verify component schema** allows the property to be bound\n\n### Immutable Binding Issues\n\n- **Check defaultVariableBindings** configuration in component registry\n- **Verify immutable flag** is set correctly for auto-bound properties\n- **Use layer store methods** to check binding immutability programmatically" + "children": "## How Variable Binding Works\n\nVariable binding replaces static property values with dynamic references. When bound, a component property stores a variable reference object:\n\n```tsx\n// Before binding - static value\nconst button = {\n props: {\n children: 'Click me',\n disabled: false\n }\n};\n\n// After binding - variable references \nconst button = {\n props: {\n children: { __variableRef: 'button-text-var' },\n disabled: { __variableRef: 'is-loading-var' }\n }\n};\n```\n\n💡 **See it in action**: The demo above shows variable bindings with real-time value resolution from the [working example](/examples/renderer/variables).\n\n## Binding Variables Through the UI\n\n### Step-by-Step Binding Process\n\n1. **Select a component** in the editor canvas\n2. **Open the Properties panel** (right sidebar) \n3. **Find the property** you want to bind\n4. **Click the link icon** (🔗) next to the property field\n5. **Choose a variable** from the dropdown menu\n6. **The property is now bound** and shows the variable info\n\n### Visual Indicators in the Props Panel\n\nBound properties are visually distinct:\n\n- **Link icon** (🔗) indicates the property supports binding\n- **Variable card** displays when a property is bound\n- **Variable name and type** are shown (e.g., `userName` • `string`)\n- **Current value** shows the variable's resolved value\n- **Unlink button** (🔗⛌) allows unbinding (if not immutable)\n- **Lock icon** (🔒) indicates immutable bindings that cannot be changed\n\n### Unbinding Variables\n\nTo remove a variable binding:\n\n1. **Select the component** with bound properties\n2. **Find the bound property** in the props panel\n3. **Click the unlink icon** next to the variable card\n4. **Property reverts** to its schema default value\n\n**Note**: Immutable bindings (🔒) cannot be unbound through the UI.\n\n## Working Example: Variable Bindings in Action\n\nHere's the actual structure from our live demo showing real variable bindings:\n\n```tsx\n// Page structure with variable bindings\nconst page: ComponentLayer = {\n id: \"variables-demo-page\",\n type: \"div\",\n props: {\n className: \"max-w-4xl mx-auto p-8 space-y-8\"\n },\n children: [\n {\n id: \"page-title\",\n type: \"h1\", \n props: {\n className: \"text-4xl font-bold text-gray-900\",\n children: { __variableRef: \"pageTitle\" } // ← Variable binding\n }\n },\n {\n id: \"user-name\",\n type: \"span\",\n props: {\n className: \"text-gray-900\",\n children: { __variableRef: \"userName\" } // ← Another binding\n }\n },\n {\n id: \"primary-button\",\n type: \"Button\",\n props: {\n variant: \"default\",\n children: { __variableRef: \"buttonText\" }, // ← Button text binding\n disabled: { __variableRef: \"isLoading\" } // ← Boolean binding\n }\n }\n ]\n};\n\n// Variables that match the bindings\nconst variables: Variable[] = [\n {\n id: \"pageTitle\",\n name: \"Page Title\",\n type: \"string\",\n defaultValue: \"UI Builder Variables Demo\"\n },\n {\n id: \"userName\",\n name: \"User Name\", \n type: \"string\",\n defaultValue: \"John Doe\"\n },\n {\n id: \"buttonText\",\n name: \"Primary Button Text\",\n type: \"string\", \n defaultValue: \"Click Me!\"\n },\n {\n id: \"isLoading\",\n name: \"Loading State\",\n type: \"boolean\",\n defaultValue: false\n }\n];\n```\n\n## Automatic Variable Binding\n\n### Default Variable Bindings\n\nComponents can automatically bind to variables when added to the canvas:\n\n```tsx\nconst componentRegistry = {\n UserProfile: {\n component: UserProfile,\n schema: z.object({\n userId: z.string().default(\"user_123\"),\n displayName: z.string().default(\"John Doe\"),\n email: z.string().email().default(\"john@example.com\")\n }),\n from: \"@/components/ui/user-profile\",\n defaultVariableBindings: [\n {\n propName: \"userId\",\n variableId: \"current_user_id\",\n immutable: true // Cannot be unbound\n },\n {\n propName: \"displayName\", \n variableId: \"current_user_name\",\n immutable: false // Can be changed\n }\n ]\n }\n};\n```\n\n### Immutable Bindings\n\nImmutable bindings prevent accidental unbinding of critical data:\n\n- **System data**: User IDs, tenant IDs, session info\n- **Security**: Permissions, access levels, authentication state\n- **Branding**: Company logos, colors, brand consistency\n- **Template integrity**: Essential bindings in white-label scenarios\n\n```tsx\n// Example: Brand-consistent component with locked bindings\nconst BrandedButton = {\n component: Button,\n schema: z.object({\n text: z.string().default(\"Click Me\"),\n brandColor: z.string().default(\"#3b82f6\"),\n companyName: z.string().default(\"Acme Corp\")\n }),\n defaultVariableBindings: [\n {\n propName: \"brandColor\",\n variableId: \"company_brand_color\",\n immutable: true // 🔒 Locked to maintain brand consistency\n },\n {\n propName: \"companyName\",\n variableId: \"company_name\", \n immutable: true // 🔒 Company identity protected\n }\n // text prop left unbound for content flexibility\n ]\n};\n```\n\n## Variable Resolution\n\nAt runtime, variable references are resolved to actual values:\n\n```tsx\n// Variable reference in component props\nconst buttonProps = {\n children: { __variableRef: 'welcome-message' },\n disabled: { __variableRef: 'is-loading' }\n};\n\n// Variables definition\nconst variables = [\n {\n id: 'welcome-message',\n name: 'welcomeMessage',\n type: 'string',\n defaultValue: 'Welcome!'\n },\n {\n id: 'is-loading',\n name: 'isLoading', \n type: 'boolean',\n defaultValue: false\n }\n];\n\n// Runtime values override defaults\nconst variableValues = {\n 'welcome-message': 'Hello, Jane!',\n 'is-loading': true\n};\n\n// Resolution process:\n// 1. Find variable by ID → 'welcome-message'\n// 2. Use runtime value if provided → 'Hello, Jane!'\n// 3. Fall back to default if no runtime value → 'Welcome!'\n// 4. Final resolved props: { children: 'Hello, Jane!', disabled: true }\n```\n\n> 📚 **See More**: Learn about [Data Binding](/docs/data-binding) for external data integration and [Rendering Pages](/docs/rendering-pages) for LayerRenderer usage.\n\n## Managing Bindings Programmatically\n\n### Using Layer Store Methods\n\n```tsx\nimport { useLayerStore } from '@/lib/ui-builder/store/layer-store';\n\nfunction CustomBindingControl() {\n const bindPropToVariable = useLayerStore((state) => state.bindPropToVariable);\n const unbindPropFromVariable = useLayerStore((state) => state.unbindPropFromVariable);\n const isBindingImmutable = useLayerStore((state) => state.isBindingImmutable);\n\n const handleBind = () => {\n // Bind a component's 'title' prop to a variable\n bindPropToVariable('button-123', 'title', 'page-title-var');\n };\n\n const handleUnbind = () => {\n // Check if binding is immutable first\n if (!isBindingImmutable('button-123', 'title')) {\n unbindPropFromVariable('button-123', 'title');\n }\n };\n\n return (\n
\n \n \n
\n );\n}\n```\n\n### Variable Reference Detection\n\n```tsx\nimport { isVariableReference } from '@/lib/ui-builder/utils/variable-resolver';\n\n// Check if a prop value is a variable reference\nconst propValue = layer.props.children;\n\nif (isVariableReference(propValue)) {\n console.log('Bound to variable:', propValue.__variableRef);\n} else {\n console.log('Static value:', propValue);\n}\n```\n\n## Binding Best Practices\n\n### Design Patterns\n\n- **Use meaningful variable names** that clearly indicate their purpose\n- **Set appropriate default values** for better editor preview experience \n- **Use immutable bindings** for system-critical or brand-related data\n- **Group related variables** with consistent naming patterns\n- **Bind the same variable** to multiple components for consistency\n\n### UI/UX Considerations\n\n- **Visual indicators** help users understand which properties are bound\n- **Immutable bindings** should be clearly marked to avoid user confusion\n- **Unbinding** should revert to sensible default values from the schema\n- **Variable cards** provide clear information about bound variables\n\n### Performance Tips\n\n- **Variable resolution is optimized** through memoization in the rendering process\n- **Only bound properties** are processed during variable resolution\n- **Static values** pass through without processing overhead\n- **Variable updates** trigger efficient re-renders only for affected components\n\n## Troubleshooting Binding Issues\n\n### Variable Not Found\n- **Check variable ID** matches exactly in both definition and reference\n- **Verify variable exists** in the variables array\n- **Ensure variable scope** includes the needed variable in your context\n\n### Binding Not Working\n- **Confirm variable reference format** uses `{ __variableRef: 'variable-id' }`\n- **Check variable type compatibility** with component prop expectations\n- **Verify component schema** allows the property to be bound\n\n### Immutable Binding Issues\n- **Check defaultVariableBindings** configuration in component registry\n- **Verify immutable flag** is set correctly for auto-bound properties\n- **Use layer store methods** to check binding immutability programmatically\n\n```tsx\n// Debug variable bindings in browser dev tools\nconst layer = useLayerStore.getState().findLayerById('my-component');\nconsole.log('Layer props:', layer?.props);\n\n// Verify variable resolution\nimport { resolveVariableReferences } from '@/lib/ui-builder/utils/variable-resolver';\n\nconst resolved = resolveVariableReferences(\n layer.props,\n variables,\n variableValues\n);\nconsole.log('Resolved props:', resolved);\n```\n\n> 🔄 **Next Steps**: Now that you understand variable binding mechanics, explore [Data Binding](/docs/data-binding) to connect external data sources and [Variables](/docs/variables) for variable management fundamentals." } ] } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/variables-panel.ts b/app/docs/docs-data/docs-page-layers/variables-panel.ts deleted file mode 100644 index 230fb06..0000000 --- a/app/docs/docs-data/docs-page-layers/variables-panel.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { ComponentLayer } from "@/components/ui/ui-builder/types"; - -export const VARIABLES_PANEL_LAYER = { - "id": "variables-panel", - "type": "div", - "name": "Variables Panel", - "props": { - "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", - "data-group": "editor-features" - }, - "children": [ - { - "type": "span", - "children": "Variables Panel", - "id": "variables-panel-title", - "name": "Text", - "props": { - "className": "text-4xl" - } - }, - { - "id": "variables-panel-intro", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "The variables panel provides a visual interface for creating and managing dynamic data in your layouts. Define variables that can be bound to component properties for data-driven, personalized interfaces." - }, - { - "id": "variables-panel-demo", - "type": "div", - "name": "div", - "props": {}, - "children": [ - { - "id": "variables-panel-badge", - "type": "Badge", - "name": "Badge", - "props": { - "variant": "default", - "className": "rounded rounded-b-none" - }, - "children": [ - { - "id": "variables-panel-badge-text", - "type": "span", - "name": "span", - "props": {}, - "children": "Live Example" - } - ] - }, - { - "id": "variables-panel-demo-frame", - "type": "div", - "name": "div", - "props": { - "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" - }, - "children": [ - { - "id": "variables-panel-iframe", - "type": "iframe", - "name": "iframe", - "props": { - "src": "http://localhost:3000/examples/renderer/variables", - "title": "Variables Panel Demo", - "className": "aspect-square md:aspect-video w-full" - }, - "children": [] - } - ] - } - ] - }, - { - "id": "variables-panel-content", - "type": "Markdown", - "name": "Markdown", - "props": {}, - "children": "## Variable Types\n\nUI Builder supports three core variable types:\n\n```tsx\n// String variable for text content\n{\n id: 'user-name',\n name: 'userName',\n type: 'string',\n defaultValue: 'John Doe'\n}\n\n// Number variable for counts, prices, etc.\n{\n id: 'product-count',\n name: 'productCount', \n type: 'number',\n defaultValue: 42\n}\n\n// Boolean variable for toggles, features flags\n{\n id: 'show-header',\n name: 'showHeader',\n type: 'boolean',\n defaultValue: true\n}\n```\n\n## Creating Variables\n\n### Through UI Builder Props\n\n```tsx\nconst initialVariables = [\n {\n id: 'welcome-msg',\n name: 'welcomeMessage',\n type: 'string',\n defaultValue: 'Welcome to our site!'\n },\n {\n id: 'user-age',\n name: 'userAge',\n type: 'number', \n defaultValue: 25\n },\n {\n id: 'is-premium',\n name: 'isPremiumUser',\n type: 'boolean',\n defaultValue: false\n }\n];\n\n {\n // Save variables to your backend\n saveVariables(variables);\n }}\n allowVariableEditing={true} // Allow users to edit variables\n/>\n```\n\n### Through the Variables Panel\n\nUsers can create variables directly in the editor:\n\n1. **Click \"Add Variable\"** in the variables panel\n2. **Choose variable type** (string, number, boolean)\n3. **Set name and default value**\n4. **Variable is immediately available** for binding\n\n## Variable Binding\n\n### Binding to Component Properties\n\nBind variables to any component property in the props panel:\n\n```tsx\n// Component with variable binding\nconst buttonComponent = {\n id: 'welcome-button',\n type: 'Button',\n props: {\n children: { __variableRef: 'welcome-msg' }, // Bound to variable\n disabled: { __variableRef: 'is-premium' }, // Boolean binding\n className: 'px-4 py-2' // Static value\n }\n};\n```\n\n### Variable Reference Format\n\nVariable bindings use a special reference format:\n\n```tsx\n// Variable reference object\n{ __variableRef: 'variable-id' }\n\n// Examples\nprops: {\n title: { __variableRef: 'page-title' }, // String variable\n count: { __variableRef: 'item-count' }, // Number variable\n visible: { __variableRef: 'show-banner' } // Boolean variable\n}\n```\n\n## Default Variable Bindings\n\nComponents can automatically bind to variables when added:\n\n```tsx\nconst UserProfile = {\n component: UserProfile,\n schema: z.object({\n name: z.string().default(''),\n email: z.string().default(''),\n avatar: z.string().optional()\n }),\n from: '@/components/user-profile',\n // Automatically bind these props to variables\n defaultVariableBindings: [\n { \n propName: 'name', \n variableId: 'current-user-name',\n immutable: false // Can be unbound by user\n },\n { \n propName: 'email', \n variableId: 'current-user-email',\n immutable: true // Cannot be unbound\n }\n ]\n};\n```\n\n## Variable Resolution\n\n### Runtime Values\n\nWhen rendering with `LayerRenderer`, override variable values:\n\n```tsx\n// Variables defined in editor\nconst editorVariables = [\n { id: 'user-name', name: 'userName', type: 'string', defaultValue: 'Guest' },\n { id: 'user-age', name: 'userAge', type: 'number', defaultValue: 0 }\n];\n\n// Runtime variable values\nconst runtimeValues = {\n 'user-name': 'Alice Johnson', // Override with real user data\n 'user-age': 28\n};\n\n\n```\n\n### Variable Resolution Process\n\n1. **Editor displays** default values during editing\n2. **Renderer uses** runtime values when provided\n3. **Falls back** to default values if runtime value missing\n4. **Type safety** ensures values match variable types\n\n## Use Cases\n\n### Personalized Content\n\n```tsx\n// User-specific variables\nconst userVariables = [\n { id: 'user-first-name', name: 'firstName', type: 'string', defaultValue: 'User' },\n { id: 'user-last-name', name: 'lastName', type: 'string', defaultValue: '' },\n { id: 'user-points', name: 'loyaltyPoints', type: 'number', defaultValue: 0 }\n];\n\n// Components bound to user data\nconst welcomeSection = {\n type: 'div',\n props: { className: 'welcome-section' },\n children: [\n {\n type: 'span',\n props: {\n children: { __variableRef: 'user-first-name' }\n }\n }\n ]\n};\n```\n\n### Feature Flags\n\n```tsx\n// Boolean variables for feature toggles\nconst featureFlags = [\n { id: 'show-beta-features', name: 'showBetaFeatures', type: 'boolean', defaultValue: false },\n { id: 'enable-notifications', name: 'enableNotifications', type: 'boolean', defaultValue: true }\n];\n\n// Conditionally show components\nconst betaFeature = {\n type: 'div',\n props: {\n className: 'beta-feature',\n style: { \n display: { __variableRef: 'show-beta-features' } ? 'block' : 'none'\n }\n }\n};\n```\n\n### Multi-Tenant Branding\n\n```tsx\n// Brand-specific variables\nconst brandVariables = [\n { id: 'company-name', name: 'companyName', type: 'string', defaultValue: 'Your Company' },\n { id: 'brand-color', name: 'primaryColor', type: 'string', defaultValue: '#3b82f6' },\n { id: 'logo-url', name: 'logoUrl', type: 'string', defaultValue: '/default-logo.png' }\n];\n\n// Components using brand variables\nconst header = {\n type: 'header',\n children: [\n {\n type: 'img',\n props: {\n src: { __variableRef: 'logo-url' },\n alt: { __variableRef: 'company-name' }\n }\n }\n ]\n};\n```\n\n## Variable Management\n\n### Panel Controls\n\n- **Add Variable** - Create new variables\n- **Edit Variable** - Modify name, type, or default value\n- **Delete Variable** - Remove unused variables\n- **Search Variables** - Find variables by name\n\n### Variable Validation\n\n- **Unique names** - Prevent duplicate variable names\n- **Type checking** - Ensure values match declared types\n- **Usage tracking** - Show which components use each variable\n- **Orphan detection** - Identify unused variables\n\n### Variable Panel Configuration\n\n```tsx\n { // Handle variable changes\n console.log('Variables updated:', vars);\n }}\n/>\n```\n\n## Best Practices\n\n### Naming Conventions\n- **Use camelCase** for variable names (`userName`, not `user_name`)\n- **Be descriptive** (`currentUserEmail` vs `email`)\n- **Group related variables** (`user*`, `brand*`, `feature*`)\n\n### Type Selection\n- **Use strings** for text, URLs, IDs, and enum-like values\n- **Use numbers** for counts, measurements, and calculations \n- **Use booleans** for flags, toggles, and conditional display\n\n### Organization\n- **Start with core variables** that many components will use\n- **Group by purpose** (user data, branding, features)\n- **Document variable purpose** in your codebase\n- **Plan for runtime data** structure when designing variables\n\n### Performance\n- **Avoid excessive variables** that aren't actually needed\n- **Use immutable bindings** for system-level data\n- **Cache runtime values** when possible to reduce re-renders" - } - ] - } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/variables.ts b/app/docs/docs-data/docs-page-layers/variables.ts index e2f4198..841b01e 100644 --- a/app/docs/docs-data/docs-page-layers/variables.ts +++ b/app/docs/docs-data/docs-page-layers/variables.ts @@ -23,7 +23,7 @@ export const VARIABLES_LAYER = { "type": "Markdown", "name": "Markdown", "props": {}, - "children": "Variables are typed data containers that enable dynamic, data-driven interfaces in UI Builder. They allow you to bind component properties to values that can change at runtime, enabling personalization, theming, and reusable templates." + "children": "**Variables are the key to creating dynamic, data-driven interfaces with UI Builder.** Instead of hardcoding static values into your components, variables allow you to bind component properties to dynamic data that can change at runtime.\n\nThis transforms static designs into powerful applications with:\n- **Personalized content** that adapts to user data\n- **Reusable templates** that work across different contexts \n- **Multi-tenant applications** with customized branding per client\n- **A/B testing** and feature flags through boolean variables" }, { "id": "variables-demo", @@ -62,7 +62,7 @@ export const VARIABLES_LAYER = { "type": "iframe", "name": "iframe", "props": { - "src": "http://localhost:3000/examples/renderer/variables", + "src": "/examples/renderer/variables", "title": "Variables Demo", "className": "aspect-square md:aspect-video w-full" }, @@ -77,7 +77,7 @@ export const VARIABLES_LAYER = { "type": "Markdown", "name": "Markdown", "props": {}, - "children": "## Variable Types\n\nUI Builder supports three primitive variable types:\n\n```tsx\n// String variable for text content\nconst userNameVar: Variable = {\n id: 'user-name-var',\n name: 'userName',\n type: 'string',\n defaultValue: 'John Doe'\n};\n\n// Number variable for counts, prices, quantities\nconst itemCountVar: Variable = {\n id: 'item-count-var',\n name: 'itemCount', \n type: 'number',\n defaultValue: 42\n};\n\n// Boolean variable for feature flags, toggles\nconst showBannerVar: Variable = {\n id: 'show-banner-var',\n name: 'showBanner',\n type: 'boolean',\n defaultValue: true\n};\n```\n\n## Creating Variables\n\n### Through UIBuilder Props\n\nDefine initial variables when initializing the editor:\n\n```tsx\nimport UIBuilder from '@/components/ui/ui-builder';\nimport { Variable } from '@/components/ui/ui-builder/types';\n\nconst initialVariables: Variable[] = [\n {\n id: 'welcome-msg',\n name: 'welcomeMessage',\n type: 'string',\n defaultValue: 'Welcome to our site!'\n },\n {\n id: 'user-age',\n name: 'userAge',\n type: 'number', \n defaultValue: 25\n },\n {\n id: 'is-premium',\n name: 'isPremiumUser',\n type: 'boolean',\n defaultValue: false\n }\n];\n\nfunction App() {\n return (\n {\n // Save variables to your backend\n console.log('Variables updated:', variables);\n }}\n allowVariableEditing={true} // Allow users to edit variables\n componentRegistry={myComponentRegistry}\n />\n );\n}\n```\n\n### Through the Variables Panel\n\nUsers can create and manage variables directly in the editor:\n\n1. **Navigate to the \"Data\" tab** in the left panel\n2. **Click \"Add Variable\"** to create a new variable\n3. **Choose variable type** (string, number, boolean)\n4. **Set name and default value**\n5. **Variable is immediately available** for binding in the props panel\n\n## Variable Binding\n\n### Binding Through Props Panel\n\nVariables are bound to component properties through the UI:\n\n1. **Select a component** in the editor\n2. **Open the props panel** (right panel)\n3. **Click the link icon** next to any property field\n4. **Choose a variable** from the dropdown menu\n5. **The property is now bound** to the variable\n\n### Binding Structure\n\nWhen bound, component props store a variable reference:\n\n```tsx\n// Internal structure when a prop is bound to a variable\nconst buttonLayer: ComponentLayer = {\n id: 'my-button',\n type: 'Button',\n props: {\n children: { __variableRef: 'welcome-msg' }, // Bound to variable\n disabled: { __variableRef: 'is-loading' }, // Bound to variable\n variant: 'default' // Static value\n },\n children: []\n};\n```\n\n### Immutable Bindings\n\nUse `defaultVariableBindings` to automatically bind variables when components are added:\n\n```tsx\nconst componentRegistry = {\n UserProfile: {\n component: UserProfile,\n schema: z.object({\n userId: z.string(),\n displayName: z.string(),\n }),\n from: '@/components/ui/user-profile',\n // Automatically bind user data when component is added\n defaultVariableBindings: [\n { \n propName: 'userId', \n variableId: 'current-user-id', \n immutable: true // Cannot be unbound in UI\n },\n { \n propName: 'displayName', \n variableId: 'current-user-name', \n immutable: false // Can be changed by users\n }\n ]\n }\n};\n```\n\n## Runtime Variable Resolution\n\n### In LayerRenderer\n\nVariables are resolved when rendering pages:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\nimport { Variable } from '@/components/ui/ui-builder/types';\n\n// Define variables\nconst variables: Variable[] = [\n {\n id: 'user-name',\n name: 'userName',\n type: 'string',\n defaultValue: 'Anonymous'\n },\n {\n id: 'user-age',\n name: 'userAge',\n type: 'number',\n defaultValue: 0\n }\n];\n\n// Override variable values at runtime\nconst variableValues = {\n 'user-name': 'Jane Smith',\n 'user-age': 30\n};\n\nfunction MyPage() {\n return (\n \n );\n}\n```\n\n### Variable Resolution Process\n\n1. **Component props are scanned** for variable references\n2. **Variable references are resolved** using provided `variableValues` or defaults\n3. **Resolved values are passed** to React components\n4. **Components render** with dynamic data\n\n## Managing Variables\n\n### Read-Only Variables\n\nControl whether users can edit variables in the UI:\n\n```tsx\n\n```\n\n### Variable Change Handling\n\nRespond to variable changes in the editor:\n\n```tsx\nfunction App() {\n const handleVariablesChange = (variables: Variable[]) => {\n // Persist to backend\n fetch('/api/variables', {\n method: 'POST',\n body: JSON.stringify(variables)\n });\n };\n\n return (\n \n );\n}\n```\n\n## Code Generation\n\nVariables are included in generated React code:\n\n```tsx\n// Generated component with variables\ninterface PageProps {\n variables: {\n userName: string;\n userAge: number;\n showWelcome: boolean;\n };\n}\n\nconst Page = ({ variables }: PageProps) => {\n return (\n
\n \n Age: {variables.userAge}\n
\n );\n};\n```\n\n## Use Cases\n\n### Personalization\n\n```tsx\n// Variables for user-specific content\nconst userVariables = [\n { id: 'user-name', name: 'userName', type: 'string', defaultValue: 'User' },\n { id: 'user-avatar', name: 'userAvatar', type: 'string', defaultValue: '/default-avatar.png' },\n { id: 'is-premium', name: 'isPremiumUser', type: 'boolean', defaultValue: false }\n];\n```\n\n### Feature Flags\n\n```tsx\n// Variables for conditional features\nconst featureFlags = [\n { id: 'show-beta-feature', name: 'showBetaFeature', type: 'boolean', defaultValue: false },\n { id: 'enable-dark-mode', name: 'enableDarkMode', type: 'boolean', defaultValue: true }\n];\n```\n\n### Multi-tenant Branding\n\n```tsx\n// Variables for client-specific branding\nconst brandingVariables = [\n { id: 'company-name', name: 'companyName', type: 'string', defaultValue: 'Acme Corp' },\n { id: 'primary-color', name: 'primaryColor', type: 'string', defaultValue: '#3b82f6' },\n { id: 'logo-url', name: 'logoUrl', type: 'string', defaultValue: '/default-logo.png' }\n];\n```\n\n### Dynamic Content\n\n```tsx\n// Variables for content management\nconst contentVariables = [\n { id: 'page-title', name: 'pageTitle', type: 'string', defaultValue: 'Welcome' },\n { id: 'product-count', name: 'productCount', type: 'number', defaultValue: 0 },\n { id: 'show-special-offer', name: 'showSpecialOffer', type: 'boolean', defaultValue: false }\n];\n```\n\n## Best Practices\n\n- **Use descriptive names** for variables (e.g., `userName` not `u`)\n- **Choose appropriate types** for your data\n- **Set meaningful default values** for better preview experience\n- **Use immutable bindings** for system-critical data\n- **Group related variables** with consistent naming patterns\n- **Document variable purposes** in your component registry" + "children": "## Variable Types\n\nUI Builder supports three typed variables:\n\n```tsx\ninterface Variable {\n id: string; // Unique identifier\n name: string; // Display name (becomes property name in generated code)\n type: 'string' | 'number' | 'boolean';\n defaultValue: string | number | boolean; // Must match the type\n}\n\n// Examples:\nconst stringVar: Variable = {\n id: 'page-title',\n name: 'pageTitle',\n type: 'string',\n defaultValue: 'Welcome to UI Builder'\n};\n\nconst numberVar: Variable = {\n id: 'user-age',\n name: 'userAge', \n type: 'number',\n defaultValue: 25\n};\n\nconst booleanVar: Variable = {\n id: 'is-loading',\n name: 'isLoading',\n type: 'boolean',\n defaultValue: false\n};\n```\n\n💡 **See it in action**: The demo above shows all three types with real-time variable binding and runtime value overrides.\n\n## Creating Variables\n\n### Via Initial Variables Prop\n\nSet up variables when initializing the UIBuilder:\n\n```tsx\nimport UIBuilder from '@/components/ui/ui-builder';\nimport { Variable } from '@/components/ui/ui-builder/types';\n\nconst initialVariables: Variable[] = [\n {\n id: 'welcome-msg',\n name: 'welcomeMessage',\n type: 'string',\n defaultValue: 'Welcome to our site!'\n },\n {\n id: 'user-count',\n name: 'userCount',\n type: 'number', \n defaultValue: 0\n },\n {\n id: 'show-banner',\n name: 'showBanner',\n type: 'boolean',\n defaultValue: true\n }\n];\n\nfunction App() {\n return (\n {\n // Persist variable definitions to your backend\n console.log('Variables updated:', variables);\n }}\n />\n );\n}\n```\n\n### Via the Data Panel\n\nUsers can create variables directly in the editor:\n\n1. **Navigate to the \"Data\" tab** in the left panel\n2. **Click \"Add Variable\"** to create a new variable\n3. **Choose variable type** (string, number, boolean)\n4. **Set name and default value**\n5. **Variable is immediately available** for binding in the props panel\n\n## Using Variables\n\nVariables can be bound to component properties in two ways:\n\n### Manual Binding\nUsers can bind variables to component properties in the props panel by clicking the link icon next to any field.\n\n### Automatic Binding \nComponents can be configured to automatically bind to specific variables when added:\n\n```tsx\nconst componentRegistry = {\n UserProfile: {\n component: UserProfile,\n schema: z.object({\n userId: z.string(),\n displayName: z.string(),\n }),\n from: '@/components/ui/user-profile',\n // Automatically bind user data when component is added\n defaultVariableBindings: [\n { \n propName: 'userId', \n variableId: 'current-user-id', \n immutable: true // Cannot be unbound in UI\n },\n { \n propName: 'displayName', \n variableId: 'current-user-name', \n immutable: false // Can be changed by users\n }\n ]\n }\n};\n```\n\n**Immutable bindings** prevent users from unbinding critical variables for system data, branding consistency, and template integrity.\n\n> 💡 **Learn more**: See [Variable Binding](/docs/variable-binding) for detailed binding mechanics and [Data Binding](/docs/data-binding) for connecting to external data sources.\n\n## Variable Management\n\n### Controlling Variable Editing\n\nControl whether users can edit variables in the UI:\n\n```tsx\n\n```\n\nWhen `allowVariableEditing` is `false`:\n- Variables panel becomes read-only\n- \"Add Variable\" button is hidden\n- Edit/delete buttons on individual variables are hidden\n- Variable values can still be overridden at runtime during rendering\n\n### Variable Change Handling\n\nRespond to variable definition changes in the editor:\n\n```tsx\nfunction App() {\n const handleVariablesChange = (variables: Variable[]) => {\n // Persist variable definitions to backend\n fetch('/api/variables', {\n method: 'POST',\n body: JSON.stringify(variables)\n });\n };\n\n return (\n \n );\n}\n```\n\n## Common Use Cases\n\n### Personalization\n\n```tsx\n// Variables for user-specific content\nconst userVariables: Variable[] = [\n { id: 'user-name', name: 'userName', type: 'string', defaultValue: 'User' },\n { id: 'user-avatar', name: 'userAvatar', type: 'string', defaultValue: '/default-avatar.png' },\n { id: 'is-premium', name: 'isPremiumUser', type: 'boolean', defaultValue: false }\n];\n```\n\n### Feature Flags\n\n```tsx\n// Variables for conditional features\nconst featureFlags: Variable[] = [\n { id: 'show-beta-feature', name: 'showBetaFeature', type: 'boolean', defaultValue: false },\n { id: 'enable-dark-mode', name: 'enableDarkMode', type: 'boolean', defaultValue: true }\n];\n```\n\n### Multi-tenant Branding\n\n```tsx\n// Variables for client-specific branding\nconst brandingVariables: Variable[] = [\n { id: 'company-name', name: 'companyName', type: 'string', defaultValue: 'Acme Corp' },\n { id: 'primary-color', name: 'primaryColor', type: 'string', defaultValue: '#3b82f6' },\n { id: 'logo-url', name: 'logoUrl', type: 'string', defaultValue: '/default-logo.png' }\n];\n```\n\n## Best Practices\n\n- **Use descriptive names** for variables (e.g., `userName` not `u`)\n- **Choose appropriate types** for your data (string for text, number for counts, boolean for flags)\n- **Set meaningful default values** for better preview experience in the editor\n- **Use immutable bindings** for system-critical data that shouldn't be unbound\n- **Group related variables** with consistent naming patterns\n- **Keep variable names simple** - they become property names in generated code\n- **Separate variable definitions from values** - define structure in the editor, inject data at runtime\n\n## Variable Workflow Summary\n\n1. **Define Variables**: Create variable definitions in the editor or via `initialVariables`\n2. **Bind to Components**: Link component properties to variables in the props panel\n3. **Save Structure**: Store the page structure and variable definitions (via `onChange` and `onVariablesChange`)\n4. **Render with Data**: Use `LayerRenderer` with `variableValues` to inject real data at runtime\n\nThis workflow enables the separation of content structure from actual data, making your UI Builder pages truly dynamic and reusable.\n\n> 📚 **Next Steps**: Learn about [Variable Binding](/docs/variable-binding) for detailed binding mechanics, [Data Binding](/docs/data-binding) for external data integration, and [Rendering Pages](/docs/rendering-pages) for runtime usage." } ] } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/layout.tsx b/app/docs/layout.tsx index 4ed24c6..354ee66 100644 --- a/app/docs/layout.tsx +++ b/app/docs/layout.tsx @@ -1,9 +1,43 @@ +import { Suspense } from "react"; import { ThemeProvider } from "next-themes"; +import { AppSidebar } from "@/app/platform/app-sidebar"; +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar"; +import { DocBreadcrumbs } from "../platform/doc-breadcrumbs"; -export default function DocsLayout({ children }: { children: React.ReactNode }) { +export const metadata = { + title: "Documentation - UI Builder", + description: "Everything you need to know about building UIs with our drag-and-drop builder.", +}; + +export default function DocsLayout({ + children +}: { + children: React.ReactNode, +}) { return ( - {children} + + + + + {children} + + ); +} + +function DocsHeader() { + return ( +
+ + }> + + +
+ ); } \ No newline at end of file diff --git a/app/docs/page.tsx b/app/docs/page.tsx new file mode 100644 index 0000000..26deecb --- /dev/null +++ b/app/docs/page.tsx @@ -0,0 +1,59 @@ +import Link from "next/link"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { MENU_DATA } from "@/app/docs/docs-data/data"; +import { ArrowRightIcon } from "lucide-react"; + +export default function DocsPage() { + return ( +
+
+
+

Documentation

+

+ Everything you need to know about building UIs with our drag-and-drop builder. +

+
+ +
+ {MENU_DATA.map((section) => ( + + + {section.title} + + {getCardDescription(section.title)} + + + +
+ {section.items?.map((item) => ( + + {item.title} + + + ))} +
+
+
+ ))} +
+
+
+ ); +} + +function getCardDescription(title: string): string { + const descriptions: Record = { + "Core": "Get started with the fundamentals of the UI builder", + "Component System": "Learn about components, customization, and configuration", + "Editor Features": "Explore the powerful editor panels and features", + "Data & Variables": "Master data binding and variable management", + "Layout & Persistence": "Understand structure and state management", + "Rendering": "Learn how to render and theme your pages" + }; + + return descriptions[title] || "Explore this section"; +} \ No newline at end of file diff --git a/app/examples/editor/panel-config/page.tsx b/app/examples/editor/panel-config/page.tsx new file mode 100644 index 0000000..3c505e6 --- /dev/null +++ b/app/examples/editor/panel-config/page.tsx @@ -0,0 +1,14 @@ +import { PanelConfigDemo } from "app/platform/panel-config-demo"; + +export const metadata = { + title: "Panel Configuration - UI Builder", + description: "Demonstrates custom panel configuration options" +}; + +export default function Page() { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/app/examples/editor/read-only-mode/page.tsx b/app/examples/editor/read-only-mode/page.tsx new file mode 100644 index 0000000..b97c09c --- /dev/null +++ b/app/examples/editor/read-only-mode/page.tsx @@ -0,0 +1,14 @@ +import { ReadOnlyDemo } from "@/app/platform/read-only-demo"; + +export const metadata = { + title: "UI Builder - Read-Only Mode Demo", + description: "Interactive demonstration of UI Builder's read-only mode capabilities" +}; + +export default function ReadOnlyModePage() { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 7e48beb..da38c44 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,11 @@ /* eslint-disable @next/next/no-sync-scripts */ import "@/styles/globals.css"; +export const metadata = { + title: "UI Builder", + description: "An open source UI builder for building complex UIs", +}; + export default function RootLayout({ children, }: { diff --git a/app/page.tsx b/app/page.tsx index 7b4d4f8..7c791bc 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,13 +1,58 @@ -import { SimpleBuilder } from "./platform/simple-builder"; +import Link from "next/link"; +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { GithubIcon } from "lucide-react"; +import Image from "next/image"; -export const metadata = { - title: "UI Builder", -}; export default function Page() { return ( -
- +
+ + + + UI Builder + UI Builder + + + Get started by exploring the documentation or trying out a basic + example. + + + + + + + +
); } diff --git a/app/platform/app-sidebar.tsx b/app/platform/app-sidebar.tsx index fc4450f..90121e2 100644 --- a/app/platform/app-sidebar.tsx +++ b/app/platform/app-sidebar.tsx @@ -1,26 +1,42 @@ +"use client" + import * as React from "react" import Link from "next/link" +import { usePathname } from "next/navigation" import { Sidebar, SidebarContent, + SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, + SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarRail, } from "@/components/ui/sidebar" import { MENU_DATA } from "@/app/docs/docs-data/data" +import Image from "next/image"; +import { GithubIcon } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { ThemeToggle } from "@/app/platform/theme-toggle" interface AppSidebarProps extends React.ComponentProps { currentPath?: string; } -export function AppSidebar({ currentPath, ...props }: AppSidebarProps) { +function AppSidebarContent({ currentPath, ...props }: AppSidebarProps) { return ( + + + UI Builder + UI Builder + + + {MENU_DATA.map((section) => ( @@ -45,7 +61,19 @@ export function AppSidebar({ currentPath, ...props }: AppSidebarProps) { ))} + + + + + ) } + +export function AppSidebar(props: Omit) { + const pathname = usePathname() + return +} diff --git a/app/platform/builder-drag-drop-test.tsx b/app/platform/builder-drag-drop-test.tsx index 6207143..8544268 100644 --- a/app/platform/builder-drag-drop-test.tsx +++ b/app/platform/builder-drag-drop-test.tsx @@ -806,6 +806,7 @@ export const BuilderDragDropTest = () => { allowPagesCreation={true} allowPagesDeletion={true} allowVariableEditing={true} + persistLayerStore={false} /> ); }; \ No newline at end of file diff --git a/app/platform/builder-with-immutable-bindings.tsx b/app/platform/builder-with-immutable-bindings.tsx index 8dfbf55..c4c9400 100644 --- a/app/platform/builder-with-immutable-bindings.tsx +++ b/app/platform/builder-with-immutable-bindings.tsx @@ -363,6 +363,7 @@ export const BuilderWithImmutableBindings = () => { allowPagesCreation={true} allowPagesDeletion={true} allowVariableEditing={false} + persistLayerStore={false} /> ); }; \ No newline at end of file diff --git a/app/platform/builder-with-pages.tsx b/app/platform/builder-with-pages.tsx index a9a2ea3..4c5ef74 100644 --- a/app/platform/builder-with-pages.tsx +++ b/app/platform/builder-with-pages.tsx @@ -990,6 +990,7 @@ export const BuilderWithPages = ({fixedPages = false}: {fixedPages?: boolean}) = }} allowPagesCreation={!fixedPages} allowPagesDeletion={!fixedPages} + persistLayerStore={false} panelConfig={getDefaultPanelConfigValues(defaultConfigTabsContent())} />; }; \ No newline at end of file diff --git a/app/platform/doc-breadcrumbs.tsx b/app/platform/doc-breadcrumbs.tsx new file mode 100644 index 0000000..e10ac6f --- /dev/null +++ b/app/platform/doc-breadcrumbs.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import { getBreadcrumbsFromUrl } from "@/app/docs/docs-data/data"; +import { usePathname } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; +import { PencilIcon } from "lucide-react"; + +export function DocBreadcrumbs() { + const pathname = usePathname(); + const slug = pathname.replace("/docs/", ""); + + const currentPath = `/docs/${slug}`; + const breadcrumbs = getBreadcrumbsFromUrl(currentPath); + return ( + <> + + + + + {breadcrumbs.category.title} + + + + + {breadcrumbs.page.title} + + + + {pathname != "/docs" && ( + + + + )} + + ); +} diff --git a/app/platform/panel-config-demo.tsx b/app/platform/panel-config-demo.tsx new file mode 100644 index 0000000..2ac40c4 --- /dev/null +++ b/app/platform/panel-config-demo.tsx @@ -0,0 +1,469 @@ +"use client" + +import React, { useState } from "react"; +import UIBuilder, { defaultConfigTabsContent, TabsContentConfig } from "@/components/ui/ui-builder"; +import { complexComponentDefinitions } from "@/lib/ui-builder/registry/complex-component-definitions"; +import { primitiveComponentDefinitions } from "@/lib/ui-builder/registry/primitive-component-definitions"; +import { ComponentLayer, Variable } from '@/components/ui/ui-builder/types'; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Settings, Database, Layout, Home, Code, Eye } from "lucide-react"; +import { useLayerStore } from "@/lib/ui-builder/store/layer-store"; +import { useEditorStore } from "@/lib/ui-builder/store/editor-store"; +import LayerRenderer from "@/components/ui/ui-builder/layer-renderer"; + +// Super Simple Custom Nav Component +const SimpleNav = () => { + const [showCodeDialog, setShowCodeDialog] = useState(false); + const [showPreviewDialog, setShowPreviewDialog] = useState(false); + + const selectedPageId = useLayerStore((state: any) => state.selectedPageId); + const findLayerById = useLayerStore((state: any) => state.findLayerById); + const componentRegistry = useEditorStore((state: any) => state.registry); + + const page = findLayerById(selectedPageId) as ComponentLayer; + + return ( + <> +
+
+ + UI Builder +
+
+ + +
+
+ + {/* Simple Code Dialog */} + {showCodeDialog && ( +
+
+
+

Generated Code

+ +
+
+
{`// Code export functionality would go here`}
+
+
+
+ )} + + {/* Simple Preview Dialog */} + {showPreviewDialog && ( +
+
+
+

Page Preview

+ +
+
+ {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
+
+
+ +
+); + +// 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) => ( + + ))} +
+
+
+
+ + {/* 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) => ( + + ))} +
+
+
+
+ + {/* UI Builder */} +
+ { + console.log(`[${mode}] Pages updated:`, updatedPages); + }} + onVariablesChange={(updatedVariables) => { + console.log(`[${mode}] Variables updated:`, updatedVariables); + }} + /> +
+
+ ); +}; \ No newline at end of file diff --git a/components/ui/ui-builder/components/codeblock.tsx b/components/ui/ui-builder/components/codeblock.tsx index 2bcadd2..cad21ca 100644 --- a/components/ui/ui-builder/components/codeblock.tsx +++ b/components/ui/ui-builder/components/codeblock.tsx @@ -85,9 +85,9 @@ export const CodeBlock = memo(function CodeBlock({ ); return ( -
-
- {language} +
+
+ {language}
\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 \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 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 \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 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}", From 2450a67517fab63909433bacea7026210392c730 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Wed, 23 Jul 2025 16:24:57 -0400 Subject: [PATCH 5/5] chore: remove console logs --- app/(edit)/docs/[slug]/edit/page.tsx | 2 +- app/docs/[slug]/page.tsx | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/(edit)/docs/[slug]/edit/page.tsx b/app/(edit)/docs/[slug]/edit/page.tsx index 5d77862..e8901d7 100644 --- a/app/(edit)/docs/[slug]/edit/page.tsx +++ b/app/(edit)/docs/[slug]/edit/page.tsx @@ -13,6 +13,6 @@ export default async function DocEditPage({ if (!page) { notFound(); } - console.log({slug}); + return } \ No newline at end of file diff --git a/app/docs/[slug]/page.tsx b/app/docs/[slug]/page.tsx index c6025e1..6814166 100644 --- a/app/docs/[slug]/page.tsx +++ b/app/docs/[slug]/page.tsx @@ -13,8 +13,6 @@ export async function generateMetadata( } ): Promise { const slug = (await params).slug - - console.log({slug}); const page = getDocPageForSlug(slug);