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/(edit)/docs/[slug]/edit/page.tsx b/app/(edit)/docs/[slug]/edit/page.tsx new file mode 100644 index 0000000..e8901d7 --- /dev/null +++ b/app/(edit)/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/docs-data/data"; + +export default async function DocEditPage({ + params, + }: { + params: Promise<{ slug: string }>; + }){ + const { slug } = await params; + const page = getDocPageForSlug(slug); + if (!page) { + notFound(); + } + + 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..6814166 --- /dev/null +++ b/app/docs/[slug]/page.tsx @@ -0,0 +1,55 @@ +import { Suspense } from "react"; +import { DocRenderer } from "@/app/platform/doc-renderer"; +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 + + 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(); + } + + return ( +
+ }> + + +
+ ); +} + +function DocSkeleton() { + return ( +
+
+ + + +
+
+ ); +} diff --git a/app/docs/docs-data/data.ts b/app/docs/docs-data/data.ts new file mode 100644 index 0000000..4aa6a3a --- /dev/null +++ b/app/docs/docs-data/data.ts @@ -0,0 +1,172 @@ +import { INTRODUCTION_LAYER } from "@/app/docs/docs-data/docs-page-layers/introduction"; +import { QUICK_START_LAYER } from "@/app/docs/docs-data/docs-page-layers/quick-start"; +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 { 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 { 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"; + +export const DOCS_PAGES = [ + // Core + INTRODUCTION_LAYER, + QUICK_START_LAYER, + + // Component System + COMPONENT_REGISTRY_LAYER, + CUSTOM_COMPONENTS_LAYER, + FIELD_OVERRIDES_LAYER, + PANEL_CONFIGURATION_LAYER, + + // Data & Variables + VARIABLES_LAYER, + VARIABLE_BINDING_LAYER, + READ_ONLY_MODE_LAYER, + + // Layout & Persistence + LAYER_STRUCTURE_LAYER, + PERSISTENCE_LAYER, + + // Rendering + RENDERING_PAGES_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 ; + url: `/docs/${ExistingDocPageIds}`; + }[]; +} + +export const MENU_DATA: DocPageNavItem[] = [ + { + title: "Core", + items: [ + { + title: "Introduction", + url: "/docs/introduction", + }, + { + title: "Quick Start", + url: "/docs/quick-start", + }, + ], + }, + { + title: "Component System", + items: [ + { + title: "Components Intro", + url: "/docs/component-registry", + }, + { + title: "Custom Components", + url: "/docs/custom-components", + }, + { + title: "Advanced Configuration", + url: "/docs/field-overrides", + }, + { + title: "Panel Configuration", + url: "/docs/panel-configuration", + } + ], + }, + { + title: "Data & Variables", + items: [ + { + title: "Variables", + url: "/docs/variables", + }, + { + title: "Variable Binding", + url: "/docs/variable-binding", + }, + { + title: "Editing Restrictions", + url: "/docs/read-only-mode", + }, + ], + }, + { + title: "Layout & Persistence", + items: [ + { + title: "Layer Structure", + url: "/docs/layer-structure", + }, + { + title: "State Management & Persistence", + url: "/docs/persistence", + }, + ], + }, + { + title: "Rendering", + items: [ + { + title: "Rendering Pages", + url: "/docs/rendering-pages", + }, + ], + } +] 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: "Home", + 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/component-registry.ts b/app/docs/docs-data/docs-page-layers/component-registry.ts new file mode 100644 index 0000000..8ab5966 --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/component-registry.ts @@ -0,0 +1,90 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const COMPONENT_REGISTRY_LAYER = { + "id": "component-registry", + "type": "div", + "name": "Components Intro", + "props": { + "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", + "data-group": "component-system" + }, + "children": [ + { + "type": "span", + "children": "Components Introduction", + "id": "component-registry-title", + "name": "Text", + "props": { + "className": "text-4xl" + } + }, + { + "id": "component-registry-intro", + "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. 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 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", + "type": "div", + "name": "div", + "props": {}, + "children": [ + { + "id": "component-registry-badge", + "type": "Badge", + "name": "Badge", + "props": { + "variant": "default", + "className": "rounded rounded-b-none" + }, + "children": [ + { + "id": "component-registry-badge-text", + "type": "span", + "name": "span", + "props": {}, + "children": "Live Component Registry" + } + ] + }, + { + "id": "component-registry-demo", + "type": "div", + "name": "div", + "props": { + "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" + }, + "children": [ + { + "id": "component-registry-iframe", + "type": "iframe", + "name": "iframe", + "props": { + "src": "/examples/editor", + "title": "UI Builder Component Registry Demo", + "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 new file mode 100644 index 0000000..22e84a9 --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/custom-components.ts @@ -0,0 +1,36 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const CUSTOM_COMPONENTS_LAYER = { + "id": "custom-components", + "type": "div", + "name": "Custom Components", + "props": { + "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", + "data-group": "component-system" + }, + "children": [ + { + "type": "span", + "children": "Creating Custom Components", + "id": "custom-components-title", + "name": "Text", + "props": { + "className": "text-4xl" + } + }, + { + "id": "custom-components-intro", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "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": "## 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/field-overrides.ts b/app/docs/docs-data/docs-page-layers/field-overrides.ts new file mode 100644 index 0000000..4662d24 --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/field-overrides.ts @@ -0,0 +1,90 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const FIELD_OVERRIDES_LAYER = { + "id": "field-overrides", + "type": "div", + "name": "Advanced 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": "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. 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/introduction.ts b/app/docs/docs-data/docs-page-layers/introduction.ts new file mode 100644 index 0000000..2a868d8 --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/introduction.ts @@ -0,0 +1,111 @@ +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": "**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", + "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": "Live Demo" + } + ] + }, + { + "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": "/examples/basic", + "title": "UI Builder Basic Example", + "className": "aspect-square md:aspect-video" + }, + "children": [] + } + ] + } + ] + }, + { + "id": "cUFUpBr", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "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 new file mode 100644 index 0000000..09738c0 --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/layer-structure.ts @@ -0,0 +1,78 @@ +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 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-interface", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "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/panel-configuration.ts b/app/docs/docs-data/docs-page-layers/panel-configuration.ts new file mode 100644 index 0000000..41a69f2 --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/panel-configuration.ts @@ -0,0 +1,92 @@ +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": "advanced-configuration" + }, + "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": "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": "Panel Configuration Demo", + "props": { + "className": "w-full border rounded-lg overflow-hidden bg-white shadow-sm" + }, + "children": [ + { + "id": "demo-iframe", + "type": "iframe", + "name": "Demo Iframe", + "props": { + "src": "/examples/editor/panel-config", + "className": "w-full h-[600px] border-none", + "title": "Panel Configuration Demo" + }, + "children": [] + } + ] + }, + { + "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": "## Common Use Cases\n\n### White-Label Applications\n\nCustomize the interface to match your brand:\n\n```tsx\nconst brandedPanelConfig = {\n pageConfigPanelTabsContent: {\n layers: { \n title: \"Components\", \n content: defaultConfigTabsContent().layers.content \n },\n appearance: { \n title: \"Brand Kit\", \n content: \n },\n data: { \n title: \"Content\", \n content: \n }\n }\n};\n```\n\n### Specialized Workflows\n\nTailor the interface for specific use cases:\n\n```tsx\n// Email template builder\nconst emailBuilderConfig = {\n pageConfigPanelTabsContent: {\n layers: { title: \"Blocks\", content: defaultConfigTabsContent().layers.content },\n appearance: { title: \"Styling\", content: },\n data: { title: \"Merge Tags\", content: },\n preview: { title: \"Preview\", content: }\n }\n};\n\n// Landing page builder\nconst landingPageConfig = {\n pageConfigPanelTabsContent: {\n layers: { title: \"Sections\", content: defaultConfigTabsContent().layers.content },\n appearance: { title: \"Theme\", content: },\n data: { title: \"A/B Tests\", content: },\n analytics: { title: \"Analytics\", content: }\n }\n};\n```\n\n### Simplified Interfaces\n\nHide complexity for non-technical users:\n\n```tsx\n// Content editor - no technical options\nconst contentEditorConfig = {\n pageConfigPanelTabsContent: {\n layers: { title: \"Content\", content: defaultConfigTabsContent().layers.content }\n // Hide appearance and data tabs\n }\n};\n\n// Designer interface - focus on visual\nconst designerConfig = {\n pageConfigPanelTabsContent: {\n layers: { title: \"Layers\", content: defaultConfigTabsContent().layers.content },\n appearance: { title: \"Design\", content: }\n // Hide data/variables tab\n }\n};\n```\n\n### Integration with External Tools\n\nConnect with your existing systems:\n\n```tsx\nconst integratedConfig = {\n pageConfigPanelTabsContent: {\n layers: { title: \"Structure\", content: defaultConfigTabsContent().layers.content },\n appearance: { title: \"Styling\", content: defaultConfigTabsContent().appearance?.content },\n data: { title: \"Data\", content: defaultConfigTabsContent().data?.content },\n cms: { title: \"CMS\", content: },\n assets: { title: \"Assets\", content: }\n }\n};\n```" + } + ] + } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/persistence.ts b/app/docs/docs-data/docs-page-layers/persistence.ts new file mode 100644 index 0000000..7fee5a7 --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/persistence.ts @@ -0,0 +1,99 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const PERSISTENCE_LAYER = { + "id": "persistence", + "type": "div", + "name": "State Management & Persistence", + "props": { + "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", + "data-group": "layout-persistence" + }, + "children": [ + { + "type": "span", + "children": "State Management & Persistence", + "id": "persistence-title", + "name": "Text", + "props": { + "className": "text-4xl" + } + }, + { + "id": "persistence-intro", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "UI Builder provides flexible state management and persistence options for your layouts and variables. Choose between automatic local storage, custom database integration, or complete manual control based on your application's needs." + }, + { + "id": "persistence-state-overview", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Understanding UI Builder State\n\nUI Builder manages two main types of state:\n- **Pages & Layers**: The component hierarchy, structure, and configuration\n- **Variables**: Dynamic data definitions that can be bound to component properties\n\nBoth are managed independently and can be persisted using different strategies." + }, + { + "id": "persistence-local-storage", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Local Storage Persistence\n\nBy default, UI Builder automatically saves state to browser local storage:\n\n```tsx\n// Default behavior - auto-saves to localStorage\n\n\n// Disable local storage persistence\n\n```\n\n**When to use:** Development, prototyping, or single-user applications where browser storage is sufficient.\n\n**Limitations:** Data is tied to the browser/device and can be cleared by the user." + }, + { + "id": "persistence-database-integration", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Database Integration\n\nFor production applications, integrate with your database using the initialization and callback props:\n\n```tsx\nimport UIBuilder from '@/components/ui/ui-builder';\nimport type { ComponentLayer, Variable, LayerChangeHandler, VariableChangeHandler } from '@/components/ui/ui-builder/types';\n\nfunction DatabaseIntegratedBuilder({ userId }: { userId: string }) {\n const [initialLayers, setInitialLayers] = useState();\n const [initialVariables, setInitialVariables] = useState();\n const [isLoading, setIsLoading] = useState(true);\n\n // Load initial state from database\n useEffect(() => {\n async function loadUserLayout() {\n try {\n const [layoutRes, variablesRes] = await Promise.all([\n fetch(`/api/layouts/${userId}`),\n fetch(`/api/variables/${userId}`)\n ]);\n \n const layoutData = await layoutRes.json();\n const variablesData = await variablesRes.json();\n \n setInitialLayers(layoutData.layers || []);\n setInitialVariables(variablesData.variables || []);\n } catch (error) {\n console.error('Failed to load layout:', error);\n setInitialLayers([]);\n setInitialVariables([]);\n } finally {\n setIsLoading(false);\n }\n }\n\n loadUserLayout();\n }, [userId]);\n\n // Save layers to database\n const handleLayersChange: LayerChangeHandler = async (updatedLayers) => {\n try {\n await fetch(`/api/layouts/${userId}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers: updatedLayers })\n });\n } catch (error) {\n console.error('Failed to save layers:', error);\n }\n };\n\n // Save variables to database\n const handleVariablesChange: VariableChangeHandler = async (updatedVariables) => {\n try {\n await fetch(`/api/variables/${userId}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ variables: updatedVariables })\n });\n } catch (error) {\n console.error('Failed to save variables:', error);\n }\n };\n\n if (isLoading) {\n return
Loading your layout...
;\n }\n\n return (\n \n );\n}\n```" + }, + { + "id": "persistence-props-reference", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Persistence-Related Props\n\n### Core Persistence Props\n\n- **`persistLayerStore`** (`boolean`, default: `true`): Controls localStorage persistence\n- **`initialLayers`** (`ComponentLayer[]`): Initial pages/layers to load\n- **`onChange`** (`LayerChangeHandler`): Callback when layers change\n- **`initialVariables`** (`Variable[]`): Initial variables to load\n- **`onVariablesChange`** (`VariableChangeHandler`): Callback when variables change\n\n### Permission Control Props\n\nControl what users can modify to prevent unwanted changes:\n\n- **`allowVariableEditing`** (`boolean`, default: `true`): Allow variable creation/editing\n- **`allowPagesCreation`** (`boolean`, default: `true`): Allow new page creation\n- **`allowPagesDeletion`** (`boolean`, default: `true`): Allow page deletion\n\n```tsx\n// Read-only editor for content review\n\n```" + }, + { + "id": "persistence-working-examples", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Working Examples\n\nExplore these working examples in the project:\n\n- **[Basic Example](/examples/basic)**: Simple setup with localStorage persistence\n- **[Editor Example](/examples/editor)**: Full editor with custom configuration\n- **[Immutable Pages](/examples/editor/immutable-pages)**: Read-only pages with `allowPagesCreation={false}` and `allowPagesDeletion={false}`\n\nThe examples demonstrate different persistence patterns you can adapt for your use case." + }, + { + "id": "persistence-debounced-save", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Debounced Auto-Save\n\nAvoid excessive API calls with debounced saving:\n\n```tsx\nimport { useCallback } from 'react';\nimport { debounce } from 'lodash';\n\nfunction AutoSaveBuilder() {\n // Debounce saves to reduce API calls\n const debouncedSaveLayers = useCallback(\n debounce(async (layers: ComponentLayer[]) => {\n try {\n await fetch('/api/layouts/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers })\n });\n } catch (error) {\n console.error('Auto-save failed:', error);\n }\n }, 2000), // 2 second delay\n []\n );\n\n const debouncedSaveVariables = useCallback(\n debounce(async (variables: Variable[]) => {\n try {\n await fetch('/api/variables/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ variables })\n });\n } catch (error) {\n console.error('Variables auto-save failed:', error);\n }\n }, 2000),\n []\n );\n\n return (\n \n );\n}\n```" + }, + { + "id": "persistence-data-format", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Data Format\n\nUI Builder saves data as plain JSON with a predictable structure:\n\n```json\n{\n \"layers\": [\n {\n \"id\": \"page-1\",\n \"type\": \"div\",\n \"name\": \"Page 1\",\n \"props\": {\n \"className\": \"p-4 bg-white\"\n },\n \"children\": [\n {\n \"id\": \"button-1\",\n \"type\": \"Button\",\n \"name\": \"Submit Button\",\n \"props\": {\n \"variant\": \"default\",\n \"children\": {\n \"__variableRef\": \"buttonText\"\n }\n },\n \"children\": []\n }\n ]\n }\n ],\n \"variables\": [\n {\n \"id\": \"buttonText\",\n \"name\": \"Button Text\",\n \"type\": \"string\",\n \"defaultValue\": \"Click Me!\"\n }\n ]\n}\n```\n\n**Variable References**: Component properties bound to variables use `{ \"__variableRef\": \"variableId\" }` format." + }, + { + "id": "persistence-api-routes", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Next.js API Route Examples\n\n### Layout API Route\n\n```tsx\n// app/api/layouts/[userId]/route.ts\nimport { NextRequest, NextResponse } from 'next/server';\n\nexport async function GET(\n request: NextRequest,\n { params }: { params: { userId: string } }\n) {\n try {\n const layout = await getUserLayout(params.userId);\n return NextResponse.json({ layers: layout });\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to load layout' },\n { status: 500 }\n );\n }\n}\n\nexport async function PUT(\n request: NextRequest,\n { params }: { params: { userId: string } }\n) {\n try {\n const { layers } = await request.json();\n await saveUserLayout(params.userId, layers);\n return NextResponse.json({ success: true });\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to save layout' },\n { status: 500 }\n );\n }\n}\n```\n\n### Variables API Route \n\n```tsx\n// app/api/variables/[userId]/route.ts\nexport async function PUT(\n request: NextRequest,\n { params }: { params: { userId: string } }\n) {\n try {\n const { variables } = await request.json();\n await saveUserVariables(params.userId, variables);\n return NextResponse.json({ success: true });\n } catch (error) {\n return NextResponse.json(\n { error: 'Failed to save variables' },\n { status: 500 }\n );\n }\n}\n```" + }, + { + "id": "persistence-error-handling", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Error Handling & Recovery\n\nImplement robust error handling for production applications:\n\n```tsx\nfunction RobustBuilder() {\n const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'error' | 'success'>('idle');\n const [lastError, setLastError] = useState(null);\n\n const handleSaveWithRetry = async (layers: ComponentLayer[], retries = 3) => {\n setSaveStatus('saving');\n \n for (let i = 0; i < retries; i++) {\n try {\n await fetch('/api/layouts/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ layers })\n });\n \n setSaveStatus('success');\n setLastError(null);\n return;\n } catch (error) {\n if (i === retries - 1) {\n setSaveStatus('error');\n setLastError('Failed to save after multiple attempts');\n } else {\n // Wait before retry with exponential backoff\n await new Promise(resolve => \n setTimeout(resolve, Math.pow(2, i) * 1000)\n );\n }\n }\n }\n };\n\n return (\n
\n {/* Status Bar */}\n
\n
\n {saveStatus === 'saving' && (\n Saving...\n )}\n {saveStatus === 'success' && (\n Saved\n )}\n {saveStatus === 'error' && (\n Save Failed\n )}\n
\n \n {lastError && (\n \n )}\n
\n \n {/* Builder */}\n
\n \n
\n
\n );\n}\n```" + }, + { + "id": "persistence-best-practices", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Best Practices\n\n### 1. **Choose the Right Persistence Strategy**\n- **localStorage**: Development, prototyping, single-user apps\n- **Database**: Production, multi-user, collaborative editing\n- **Hybrid**: localStorage for drafts, database for published versions\n\n### 2. **Implement Proper Error Handling**\n- Always handle save failures gracefully\n- Provide user feedback for save status\n- Implement retry logic with exponential backoff\n- Offer recovery options when saves fail\n\n### 3. **Optimize API Performance**\n- Use debouncing to reduce API calls (2-5 second delays)\n- Consider batching layer and variable saves\n- Implement optimistic updates for better UX\n- Use proper HTTP status codes and error responses\n\n### 4. **Control User Permissions**\n- Use `allowVariableEditing={false}` for content-only editing\n- Set `allowPagesCreation={false}` for fixed page structures \n- Implement role-based access control in your API routes\n\n### 5. **Plan for Scale**\n- Consider data size limits (layers can grow large)\n- Implement pagination for large datasets\n- Use database indexing on user IDs and timestamps\n- Consider caching frequently accessed layouts" + } + ] + } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/quick-start.ts b/app/docs/docs-data/docs-page-layers/quick-start.ts new file mode 100644 index 0000000..4737978 --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/quick-start.ts @@ -0,0 +1,172 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const QUICK_START_LAYER = { + "id": "quick-start", + "type": "div", + "name": "Quick Start", + "props": { + "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", + "data-group": "core" + }, + "children": [ + { + "type": "span", + "children": "Quick Start", + "id": "quick-start-title", + "name": "Text", + "props": { + "className": "text-4xl" + } + }, + { + "id": "quick-start-intro", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "Get up and running with UI Builder in minutes. This guide covers installation, basic setup, and your first working editor." + }, + { + "id": "quick-start-compatibility", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Compatibility Notes\n\n⚠️ **Tailwind 4 + React 19**: Migration coming soon. Currently blocked by 3rd party component compatibility. If using latest shadcn/ui CLI fails, try: `npx shadcn@2.1.8 add ...`\n\n⚠️ **Server Components**: Not supported. RSC can't be re-rendered client-side for live preview. A separate RSC renderer for final page rendering is possible." + }, + { + "id": "quick-start-install", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Installation\n\nIf you are using shadcn/ui in your project, install the component directly from the registry:\n\n```bash\nnpx shadcn@latest add https://raw.githubusercontent.com/olliethedev/ui-builder/main/registry/block-registry.json\n```\n\nOr start a new project with UI Builder:\n\n```bash\nnpx shadcn@latest init https://raw.githubusercontent.com/olliethedev/ui-builder/main/registry/block-registry.json\n```\n\n**Note:** You need to use [style variables](https://ui.shadcn.com/docs/theming) to have page theming working correctly.\n\n### Fix Dependencies\n\nAdd dev dependencies (current shadcn/ui registry limitation):\n\n```bash\nnpm install -D @types/lodash.template @tailwindcss/typography @types/react-syntax-highlighter tailwindcss-animate @types/object-hash\n```" + }, + { + "id": "quick-start-basic-setup", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Basic Setup\n\nThe minimal setup requires just a component registry:\n\n```tsx\nimport UIBuilder from \"@/components/ui/ui-builder\";\nimport { primitiveComponentDefinitions } from \"@/lib/ui-builder/registry/primitive-component-definitions\";\nimport { complexComponentDefinitions } from \"@/lib/ui-builder/registry/complex-component-definitions\";\n\nconst componentRegistry = {\n ...primitiveComponentDefinitions, // div, span, img, etc.\n ...complexComponentDefinitions, // Button, Badge, Card, etc.\n};\n\nexport function App() {\n return (\n \n );\n}\n```\n\nThis gives you a full visual editor with pre-built shadcn/ui components." + }, + { + "id": "quick-start-example", + "type": "div", + "name": "div", + "props": {}, + "children": [ + { + "id": "quick-start-badge", + "type": "Badge", + "name": "Badge", + "props": { + "variant": "default", + "className": "rounded rounded-b-none" + }, + "children": [ + { + "id": "quick-start-badge-text", + "type": "span", + "name": "span", + "props": {}, + "children": "Try it now" + } + ] + }, + { + "id": "quick-start-demo", + "type": "div", + "name": "div", + "props": { + "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" + }, + "children": [ + { + "id": "quick-start-iframe", + "type": "iframe", + "name": "iframe", + "props": { + "src": "/examples/basic", + "title": "Quick Start Example", + "className": "aspect-square md:aspect-video" + }, + "children": [] + } + ] + } + ] + }, + { + "id": "quick-start-with-state", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Adding State Management\n\nFor real applications, you'll want to control the initial state and persist changes:\n\n```tsx\nimport UIBuilder from \"@/components/ui/ui-builder\";\nimport { ComponentLayer, Variable } from \"@/components/ui/ui-builder/types\";\n\n// Initial page structure\nconst initialLayers: ComponentLayer[] = [\n {\n id: \"welcome-page\",\n type: \"div\",\n name: \"Welcome Page\",\n props: {\n className: \"p-8 min-h-screen flex flex-col gap-6\",\n },\n children: [\n {\n id: \"title\",\n type: \"h1\",\n name: \"Page Title\",\n props: { \n className: \"text-4xl font-bold text-center\",\n },\n children: \"Welcome to UI Builder!\",\n },\n {\n id: \"cta-button\",\n type: \"Button\",\n name: \"CTA Button\",\n props: {\n variant: \"default\",\n className: \"mx-auto w-fit\",\n },\n children: [{\n id: \"button-text\",\n type: \"span\",\n name: \"Button Text\",\n props: {},\n children: \"Get Started\",\n }],\n },\n ],\n },\n];\n\n// Variables for dynamic content\nconst initialVariables: Variable[] = [\n {\n id: \"welcome-msg\",\n name: \"welcomeMessage\",\n type: \"string\",\n defaultValue: \"Welcome to UI Builder!\"\n }\n];\n\nexport function AppWithState() {\n const handleLayersChange = (updatedLayers: ComponentLayer[]) => {\n // Save to database, localStorage, etc.\n console.log(\"Layers updated:\", updatedLayers);\n };\n\n const handleVariablesChange = (updatedVariables: Variable[]) => {\n // Save to database, localStorage, etc.\n console.log(\"Variables updated:\", updatedVariables);\n };\n\n return (\n \n );\n}\n```" + }, + { + "id": "quick-start-key-props", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## UIBuilder Props Reference\n\n### Required Props\n- **`componentRegistry`** - Maps component names to their definitions (see **Components Intro**)\n\n### Optional Props\n- **`initialLayers`** - Set initial page structure (e.g., from database)\n- **`onChange`** - Callback when pages change (for persistence)\n- **`initialVariables`** - Set initial variables for dynamic content \n- **`onVariablesChange`** - Callback when variables change\n- **`panelConfig`** - Customize editor panels (see **Panel Configuration**)\n- **`persistLayerStore`** - Enable localStorage persistence (default: `true`)\n- **`allowVariableEditing`** - Allow users to edit variables (default: `true`) \n- **`allowPagesCreation`** - Allow users to create pages (default: `true`)\n- **`allowPagesDeletion`** - Allow users to delete pages (default: `true`)\n\n**Note**: Only `componentRegistry` is required. All other props are optional and have sensible defaults." + }, + { + "id": "quick-start-rendering", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Rendering Without the Editor\n\nTo display pages in production without the editor interface, use `LayerRenderer`:\n\n```tsx\nimport LayerRenderer from \"@/components/ui/ui-builder/layer-renderer\";\n\n// Basic rendering\nexport function MyPage({ page }) {\n return (\n \n );\n}\n\n// With variables for dynamic content\nexport function DynamicPage({ page, userData }) {\n const variableValues = {\n \"welcome-msg\": `Welcome back, ${userData.name}!`\n };\n \n return (\n \n );\n}\n```\n\n🎯 **Try it**: Check out the **[Renderer Demo](/examples/renderer)** and **[Variables Demo](/examples/renderer/variables)** to see LayerRenderer in action." + }, + { + "id": "quick-start-advanced-demo", + "type": "div", + "name": "div", + "props": {}, + "children": [ + { + "id": "advanced-demo-badge", + "type": "Badge", + "name": "Badge", + "props": { + "variant": "outline", + "className": "rounded rounded-b-none" + }, + "children": [ + { + "id": "advanced-demo-badge-text", + "type": "span", + "name": "span", + "props": {}, + "children": "Full Featured Editor" + } + ] + }, + { + "id": "advanced-demo-frame", + "type": "div", + "name": "div", + "props": { + "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" + }, + "children": [ + { + "id": "advanced-demo-iframe", + "type": "iframe", + "name": "iframe", + "props": { + "src": "/examples/editor", + "title": "Full Featured Editor Demo", + "className": "aspect-square md:aspect-video" + }, + "children": [] + } + ] + } + ] + }, + { + "id": "quick-start-next-steps", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Next Steps\n\nNow that you have UI Builder running, explore these key areas:\n\n### Essential Concepts\n- **Components Intro** - Understand the component registry system\n- **Variables** - Add dynamic content with typed variables\n- **Rendering Pages** - Use LayerRenderer in your production app\n\n### Customization\n- **Custom Components** - Add your own React components to the registry\n- **Panel Configuration** - Customize the editor interface for your users\n\n### Advanced Use Cases\n- **Variable Binding** - Auto-bind components to system data\n- **Immutable Pages** - Create locked templates for consistency" + } + ] + } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/read-only-mode.ts b/app/docs/docs-data/docs-page-layers/read-only-mode.ts new file mode 100644 index 0000000..9bedcaf --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/read-only-mode.ts @@ -0,0 +1,57 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const READ_ONLY_MODE_LAYER = { + "id": "read-only-mode", + "type": "div", + "name": "Editing Restrictions", + "props": { + "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", + "data-group": "data-variables" + }, + "children": [ + { + "type": "span", + "children": "Editing Restrictions", + "id": "read-only-mode-title", + "name": "Text", + "props": { + "className": "text-4xl" + } + }, + { + "id": "read-only-mode-intro", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "Control editing capabilities in UI Builder by restricting specific operations like variable editing, page creation, and page deletion. Perfect for production environments, content-only editing, and role-based access control." + }, + { + "id": "read-only-mode-content", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Permission Control Props\n\nUI Builder provides three boolean props to control editing permissions:\n\n```tsx\n\n```\n\n| Prop | Default | Description |\n|------|---------|-------------|\n| `allowVariableEditing` | `true` | Controls variable add/edit/delete in Variables panel |\n| `allowPagesCreation` | `true` | Controls ability to create new pages |\n| `allowPagesDeletion` | `true` | Controls ability to delete existing pages |\n\n## Interactive Demo\n\nExperience all read-only modes in one interactive demo:\n\n- **Live Demo:** [/examples/editor/read-only-mode](/examples/editor/read-only-mode)\n- **Features:** Switch between different permission levels in real-time\n- **Modes:** Full editing, content-only, no variables, and full read-only\n- **What to try:** Toggle between modes to see UI changes and restrictions" + }, + { + "id": "demo-iframe", + "type": "iframe", + "name": "Read-Only Demo Iframe", + "props": { + "src": "/examples/editor/read-only-mode", + "width": "100%", + "height": "600", + "frameBorder": "0", + "className": "w-full border border-gray-200 rounded-lg shadow-sm mb-6", + "title": "UI Builder Read-Only Mode Interactive Demo" + }, + "children": [] + }, + { + "id": "read-only-mode-content-continued", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Common Use Cases\n\n### Content-Only Editing\n\nAllow content editing while preventing structural changes:\n\n```tsx\n\n```\n\n**Use case:** Content teams updating copy, images, and variable content without changing page layouts.\n\n### Production Preview Mode\n\nLock down all structural changes:\n\n```tsx\n\n```\n\n**Use case:** Previewing templates in production with system-controlled variables.\n\n### Role-Based Access Control\n\nDifferent permissions based on user roles:\n\n```tsx\nfunction RoleBasedEditor({ user, template }) {\n const canEditVariables = user.role === 'admin' || user.role === 'developer';\n const canManagePages = user.role === 'admin';\n\n return (\n \n );\n}\n```\n\n## Variable Binding in Templates\n\nWhen creating templates with variable references, use the correct format:\n\n```tsx\n// ✅ Correct: Variable binding in component props\nconst templateLayer: ComponentLayer = {\n id: \"title\",\n type: \"span\",\n name: \"Title\",\n props: {\n className: \"text-2xl font-bold\",\n children: { __variableRef: \"pageTitle\" } // Correct format\n },\n children: []\n};\n\n// ❌ Incorrect: Variable binding directly in children\nconst badTemplate: ComponentLayer = {\n id: \"title\",\n type: \"span\",\n name: \"Title\",\n props: {\n className: \"text-2xl font-bold\"\n },\n children: { __variableRef: \"pageTitle\" } // Wrong - causes TypeScript errors\n};\n```\n\n**Key points:**\n- Variable references go in the `props` object\n- Use `__variableRef` (without quotes) as the property name\n- The value is the variable ID as a string\n- Set `children: []` when using variable binding\n\n## Additional Examples\n\n### Fixed Pages Example\n\nSee the immutable pages example that demonstrates locked page structure:\n\n- **Live Demo:** [/examples/editor/immutable-pages](/examples/editor/immutable-pages)\n- **Implementation:** Uses `allowPagesCreation={false}` and `allowPagesDeletion={false}`\n- **What's locked:** Page creation and deletion\n- **What works:** Content editing, component manipulation, theme changes\n\n### Variable Read-Only Example\n\nSee the immutable bindings example that demonstrates locked variables:\n\n- **Live Demo:** [/examples/editor/immutable-bindings](/examples/editor/immutable-bindings)\n- **Implementation:** Uses `allowVariableEditing={false}`\n- **What's locked:** Variable creation, editing, and deletion\n- **What works:** Variable binding in props panel, visual component editing\n\n## What's Still Available in Read-Only Mode\n\nEven with restrictions enabled, users can still:\n\n✅ **Visual Component Editing:** Add, remove, and modify components on the canvas \n✅ **Props Panel:** Configure component properties and bind to existing variables \n✅ **Appearance Panel:** Modify themes and styling \n✅ **Layer Navigation:** Select and organize components in the layers panel \n✅ **Undo/Redo:** Full history navigation \n✅ **Code Generation:** Export React code \n\n## When to Use LayerRenderer Instead\n\nFor pure display without any editing interface, use `LayerRenderer`:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\n\nfunction DisplayOnlyPage({ pageData, variables, userValues }) {\n return (\n \n );\n}\n```\n\n**Choose LayerRenderer when:**\n- No editing interface needed\n- Smaller bundle size required \n- Better performance for display-only scenarios\n- Rendering with dynamic data at runtime\n\n**Choose restricted UIBuilder when:**\n- Some editing capabilities needed\n- Code generation features required\n- Visual interface helps with content understanding\n- Fine-grained permission control needed\n\n## Implementation Pattern\n\n```tsx\nfunction ConfigurableEditor({ \n template, \n user, \n environment \n}) {\n const permissions = {\n allowVariableEditing: environment !== 'production' && user.canEditVariables,\n allowPagesCreation: user.role === 'admin',\n allowPagesDeletion: user.role === 'admin'\n };\n\n return (\n \n );\n}\n```\n\n## Best Practices\n\n### Security Considerations\n\n- **Validate data server-side:** Client-side restrictions are for UX, not security\n- **Sanitize inputs:** Always validate and sanitize layer data and variables\n- **Use immutable bindings:** For system variables that must never change\n- **Implement proper authentication:** Control access at the application level\n\n### User Experience\n\n- **Provide clear feedback:** Show users what's restricted and why\n- **Progressive permissions:** Unlock features as users gain trust/experience\n- **Contextual help:** Explain restrictions in context\n- **Consistent behavior:** Apply restrictions predictably across the interface\n\n### Performance\n\n- **Use LayerRenderer for display-only:** Smaller bundle, better performance\n- **Cache configurations:** Avoid re-computing permissions on every render\n- **Optimize initial data:** Only load necessary variables and pages\n- **Consider lazy loading:** Load restricted features only when needed" + } + ] + } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/rendering-pages.ts b/app/docs/docs-data/docs-page-layers/rendering-pages.ts new file mode 100644 index 0000000..d9b8ccb --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/rendering-pages.ts @@ -0,0 +1,130 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const RENDERING_PAGES_LAYER = { + "id": "rendering-pages", + "type": "div", + "name": "Rendering Pages", + "props": { + "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", + "data-group": "rendering" + }, + "children": [ + { + "type": "span", + "children": "Rendering Pages", + "id": "rendering-pages-title", + "name": "Text", + "props": { + "className": "text-4xl" + } + }, + { + "id": "rendering-pages-intro", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "Render UI Builder pages in production using the `LayerRenderer` component. Display your designed pages without the editor interface, with full support for dynamic content through variables." + }, + { + "id": "rendering-pages-demo", + "type": "div", + "name": "div", + "props": {}, + "children": [ + { + "id": "rendering-pages-badge", + "type": "Badge", + "name": "Badge", + "props": { + "variant": "default", + "className": "rounded rounded-b-none" + }, + "children": [ + { + "id": "rendering-pages-badge-text", + "type": "span", + "name": "span", + "props": {}, + "children": "Basic Rendering Demo" + } + ] + }, + { + "id": "rendering-pages-demo-frame", + "type": "div", + "name": "div", + "props": { + "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" + }, + "children": [ + { + "id": "rendering-pages-iframe", + "type": "iframe", + "name": "iframe", + "props": { + "src": "/examples/renderer", + "title": "Page Rendering Demo", + "className": "aspect-square md:aspect-video w-full" + }, + "children": [] + } + ] + } + ] + }, + { + "id": "rendering-pages-variables-demo", + "type": "div", + "name": "div", + "props": {}, + "children": [ + { + "id": "rendering-pages-variables-badge", + "type": "Badge", + "name": "Badge", + "props": { + "variant": "outline", + "className": "rounded rounded-b-none" + }, + "children": [ + { + "id": "rendering-pages-variables-badge-text", + "type": "span", + "name": "span", + "props": {}, + "children": "Variables Demo" + } + ] + }, + { + "id": "rendering-pages-variables-demo-frame", + "type": "div", + "name": "div", + "props": { + "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" + }, + "children": [ + { + "id": "rendering-pages-variables-iframe", + "type": "iframe", + "name": "iframe", + "props": { + "src": "/examples/renderer/variables", + "title": "Variables Rendering Demo", + "className": "aspect-square md:aspect-video w-full" + }, + "children": [] + } + ] + } + ] + }, + { + "id": "rendering-pages-content", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Basic Usage\n\nUse the `LayerRenderer` component to display UI Builder pages without the editor interface:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\nimport { ComponentLayer, ComponentRegistry } from '@/components/ui/ui-builder/types';\n\n// Your component registry (same as used in UIBuilder)\nconst myComponentRegistry: ComponentRegistry = {\n // Your component definitions...\n};\n\n// Page data from UIBuilder or database\nconst page: ComponentLayer = {\n id: \"welcome-page\",\n type: \"div\",\n name: \"Welcome Page\",\n props: {\n className: \"p-6 max-w-4xl mx-auto\"\n },\n children: [\n {\n id: \"title\",\n type: \"h1\",\n name: \"Title\",\n props: {\n className: \"text-3xl font-bold mb-4\"\n },\n children: \"Welcome to My App\"\n },\n {\n id: \"description\",\n type: \"p\",\n name: \"Description\",\n props: {\n className: \"text-gray-600\"\n },\n children: \"This page was built with UI Builder.\"\n }\n ]\n};\n\nfunction MyRenderedPage() {\n return (\n \n );\n}\n```\n\n## Rendering with Variables\n\nMake your pages dynamic by binding component properties to variables:\n\n```tsx\nimport LayerRenderer from '@/components/ui/ui-builder/layer-renderer';\nimport { Variable } from '@/components/ui/ui-builder/types';\n\n// Define your variables\nconst variables: Variable[] = [\n {\n id: \"userName\",\n name: \"User Name\",\n type: \"string\",\n defaultValue: \"Guest\"\n },\n {\n id: \"userAge\",\n name: \"User Age\",\n type: \"number\",\n defaultValue: 25\n },\n {\n id: \"showWelcomeMessage\",\n name: \"Show Welcome Message\",\n type: \"boolean\",\n defaultValue: true\n }\n];\n\n// Page with variable bindings (created in UIBuilder)\nconst pageWithVariables: ComponentLayer = {\n id: \"user-profile\",\n type: \"div\",\n props: {\n className: \"p-6 bg-white rounded-lg shadow\"\n },\n children: [\n {\n id: \"welcome-message\",\n type: \"h2\",\n props: {\n className: \"text-2xl font-bold mb-2\",\n children: { __variableRef: \"userName\" } // Bound to userName variable\n },\n children: []\n },\n {\n id: \"age-display\",\n type: \"p\",\n props: {\n className: \"text-gray-600\",\n children: { __variableRef: \"userAge\" } // Bound to userAge variable\n },\n children: []\n }\n ]\n};\n\n// Provide runtime values for variables\nconst variableValues = {\n userName: \"Jane Smith\",\n userAge: 28,\n showWelcomeMessage: true\n};\n\nfunction DynamicUserProfile() {\n return (\n \n );\n}\n```\n\n## Production Integration\n\nIntegrate with your data sources to create personalized experiences:\n\n```tsx\nfunction CustomerPage({ customerId }: { customerId: string }) {\n const [pageData, setPageData] = useState(null);\n const [customerData, setCustomerData] = useState({});\n \n useEffect(() => {\n async function loadData() {\n // Load page structure from your CMS/database\n const pageResponse = await fetch('/api/pages/customer-dashboard');\n const page = await pageResponse.json();\n \n // Load customer-specific data\n const customerResponse = await fetch(`/api/customers/${customerId}`);\n const customer = await customerResponse.json();\n \n setPageData(page);\n setCustomerData(customer);\n }\n \n loadData();\n }, [customerId]);\n \n if (!pageData) return
Loading...
;\n \n return (\n \n );\n}\n```\n\n## Performance Optimization\n\nOptimize rendering performance for production:\n\n```tsx\n// Memoize the renderer to prevent unnecessary re-renders\nconst MemoizedRenderer = React.memo(LayerRenderer, (prevProps, nextProps) => {\n return (\n prevProps.page === nextProps.page &&\n JSON.stringify(prevProps.variableValues) === JSON.stringify(nextProps.variableValues)\n );\n});\n\n// Use in your component\nfunction OptimizedPage() {\n return (\n \n );\n}\n```\n\n## Error Handling\n\nHandle rendering errors gracefully in production:\n\n```tsx\nimport { ErrorBoundary } from 'react-error-boundary';\n\nfunction ErrorFallback({ error }: { error: Error }) {\n return (\n
\n

Page failed to load

\n

{error.message}

\n \n
\n );\n}\n\nfunction SafeRenderedPage() {\n return (\n \n \n \n );\n}\n```\n\n## LayerRenderer Props\n\n- **`page`** (required): The `ComponentLayer` to render\n- **`componentRegistry`** (required): Registry mapping component types to their definitions\n- **`className`**: Optional CSS class for the root container\n- **`variables`**: Array of `Variable` definitions available for binding\n- **`variableValues`**: Object mapping variable IDs to runtime values (overrides defaults)\n- **`editorConfig`**: Internal editor configuration (rarely needed in production)\n\n## Best Practices\n\n1. **Use the same `componentRegistry`** in both `UIBuilder` and `LayerRenderer`\n2. **Validate variable values** before passing to LayerRenderer to prevent runtime errors\n3. **Handle loading states** while fetching page data and variables\n4. **Implement error boundaries** to gracefully handle rendering failures\n5. **Cache page data** when possible for better performance\n6. **Memoize expensive variable calculations** to avoid unnecessary re-computations\n7. **Test variable bindings** thoroughly to ensure robustness across different data scenarios" + } + ] + } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/docs-data/docs-page-layers/variable-binding.ts b/app/docs/docs-data/docs-page-layers/variable-binding.ts new file mode 100644 index 0000000..bbfe66c --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/variable-binding.ts @@ -0,0 +1,83 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const VARIABLE_BINDING_LAYER = { + "id": "variable-binding", + "type": "div", + "name": "Variable Binding", + "props": { + "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", + "data-group": "data-variables" + }, + "children": [ + { + "type": "span", + "children": "Variable Binding", + "id": "variable-binding-title", + "name": "Text", + "props": { + "className": "text-4xl" + } + }, + { + "id": "variable-binding-intro", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "Learn how to connect variables to component properties through the UI and programmatically. This page focuses on the binding mechanics—see **Variables** for fundamentals and **Data Binding** for external data integration." + }, + { + "id": "variable-binding-demo", + "type": "div", + "name": "div", + "props": {}, + "children": [ + { + "id": "variable-binding-badge", + "type": "Badge", + "name": "Badge", + "props": { + "variant": "default", + "className": "rounded rounded-b-none" + }, + "children": [ + { + "id": "variable-binding-badge-text", + "type": "span", + "name": "span", + "props": {}, + "children": "Live Variable Binding Example" + } + ] + }, + { + "id": "variable-binding-demo-frame", + "type": "div", + "name": "div", + "props": { + "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" + }, + "children": [ + { + "id": "variable-binding-iframe", + "type": "iframe", + "name": "iframe", + "props": { + "src": "/examples/renderer/variables", + "title": "Variable Binding Demo", + "className": "aspect-square md:aspect-video w-full" + }, + "children": [] + } + ] + } + ] + }, + { + "id": "variable-binding-content", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## How Variable Binding Works\n\nVariable binding replaces static property values with dynamic references. When bound, a component property stores a variable reference object:\n\n```tsx\n// Before binding - static value\nconst button = {\n props: {\n children: 'Click me',\n disabled: false\n }\n};\n\n// After binding - variable references \nconst button = {\n props: {\n children: { __variableRef: 'button-text-var' },\n disabled: { __variableRef: 'is-loading-var' }\n }\n};\n```\n\n💡 **See it in action**: The demo above shows variable bindings with real-time value resolution from the [working example](/examples/renderer/variables).\n\n## Binding Variables Through the UI\n\n### Step-by-Step Binding Process\n\n1. **Select a component** in the editor canvas\n2. **Open the Properties panel** (right sidebar) \n3. **Find the property** you want to bind\n4. **Click the link icon** (🔗) next to the property field\n5. **Choose a variable** from the dropdown menu\n6. **The property is now bound** and shows the variable info\n\n### Visual Indicators in the Props Panel\n\nBound properties are visually distinct:\n\n- **Link icon** (🔗) indicates the property supports binding\n- **Variable card** displays when a property is bound\n- **Variable name and type** are shown (e.g., `userName` • `string`)\n- **Current value** shows the variable's resolved value\n- **Unlink button** (🔗⛌) allows unbinding (if not immutable)\n- **Lock icon** (🔒) indicates immutable bindings that cannot be changed\n\n### Unbinding Variables\n\nTo remove a variable binding:\n\n1. **Select the component** with bound properties\n2. **Find the bound property** in the props panel\n3. **Click the unlink icon** next to the variable card\n4. **Property reverts** to its schema default value\n\n**Note**: Immutable bindings (🔒) cannot be unbound through the UI.\n\n## Working Example: Variable Bindings in Action\n\nHere's the actual structure from our live demo showing real variable bindings:\n\n```tsx\n// Page structure with variable bindings\nconst page: ComponentLayer = {\n id: \"variables-demo-page\",\n type: \"div\",\n props: {\n className: \"max-w-4xl mx-auto p-8 space-y-8\"\n },\n children: [\n {\n id: \"page-title\",\n type: \"h1\", \n props: {\n className: \"text-4xl font-bold text-gray-900\",\n children: { __variableRef: \"pageTitle\" } // ← Variable binding\n }\n },\n {\n id: \"user-name\",\n type: \"span\",\n props: {\n className: \"text-gray-900\",\n children: { __variableRef: \"userName\" } // ← Another binding\n }\n },\n {\n id: \"primary-button\",\n type: \"Button\",\n props: {\n variant: \"default\",\n children: { __variableRef: \"buttonText\" }, // ← Button text binding\n disabled: { __variableRef: \"isLoading\" } // ← Boolean binding\n }\n }\n ]\n};\n\n// Variables that match the bindings\nconst variables: Variable[] = [\n {\n id: \"pageTitle\",\n name: \"Page Title\",\n type: \"string\",\n defaultValue: \"UI Builder Variables Demo\"\n },\n {\n id: \"userName\",\n name: \"User Name\", \n type: \"string\",\n defaultValue: \"John Doe\"\n },\n {\n id: \"buttonText\",\n name: \"Primary Button Text\",\n type: \"string\", \n defaultValue: \"Click Me!\"\n },\n {\n id: \"isLoading\",\n name: \"Loading State\",\n type: \"boolean\",\n defaultValue: false\n }\n];\n```\n\n## Automatic Variable Binding\n\n### Default Variable Bindings\n\nComponents can automatically bind to variables when added to the canvas:\n\n```tsx\nconst componentRegistry = {\n UserProfile: {\n component: UserProfile,\n schema: z.object({\n userId: z.string().default(\"user_123\"),\n displayName: z.string().default(\"John Doe\"),\n email: z.string().email().default(\"john@example.com\")\n }),\n from: \"@/components/ui/user-profile\",\n defaultVariableBindings: [\n {\n propName: \"userId\",\n variableId: \"current_user_id\",\n immutable: true // Cannot be unbound\n },\n {\n propName: \"displayName\", \n variableId: \"current_user_name\",\n immutable: false // Can be changed\n }\n ]\n }\n};\n```\n\n### Immutable Bindings\n\nImmutable bindings prevent accidental unbinding of critical data:\n\n- **System data**: User IDs, tenant IDs, session info\n- **Security**: Permissions, access levels, authentication state\n- **Branding**: Company logos, colors, brand consistency\n- **Template integrity**: Essential bindings in white-label scenarios\n\n```tsx\n// Example: Brand-consistent component with locked bindings\nconst BrandedButton = {\n component: Button,\n schema: z.object({\n text: z.string().default(\"Click Me\"),\n brandColor: z.string().default(\"#3b82f6\"),\n companyName: z.string().default(\"Acme Corp\")\n }),\n defaultVariableBindings: [\n {\n propName: \"brandColor\",\n variableId: \"company_brand_color\",\n immutable: true // 🔒 Locked to maintain brand consistency\n },\n {\n propName: \"companyName\",\n variableId: \"company_name\", \n immutable: true // 🔒 Company identity protected\n }\n // text prop left unbound for content flexibility\n ]\n};\n```\n\n## Variable Resolution\n\nAt runtime, variable references are resolved to actual values:\n\n```tsx\n// Variable reference in component props\nconst buttonProps = {\n children: { __variableRef: 'welcome-message' },\n disabled: { __variableRef: 'is-loading' }\n};\n\n// Variables definition\nconst variables = [\n {\n id: 'welcome-message',\n name: 'welcomeMessage',\n type: 'string',\n defaultValue: 'Welcome!'\n },\n {\n id: 'is-loading',\n name: 'isLoading', \n type: 'boolean',\n defaultValue: false\n }\n];\n\n// Runtime values override defaults\nconst variableValues = {\n 'welcome-message': 'Hello, Jane!',\n 'is-loading': true\n};\n\n// Resolution process:\n// 1. Find variable by ID → 'welcome-message'\n// 2. Use runtime value if provided → 'Hello, Jane!'\n// 3. Fall back to default if no runtime value → 'Welcome!'\n// 4. Final resolved props: { children: 'Hello, Jane!', disabled: true }\n```\n\n> 📚 **See More**: Learn about [Data Binding](/docs/data-binding) for external data integration and [Rendering Pages](/docs/rendering-pages) for LayerRenderer usage.\n\n## Managing Bindings Programmatically\n\n### Using Layer Store Methods\n\n```tsx\nimport { useLayerStore } from '@/lib/ui-builder/store/layer-store';\n\nfunction CustomBindingControl() {\n const bindPropToVariable = useLayerStore((state) => state.bindPropToVariable);\n const unbindPropFromVariable = useLayerStore((state) => state.unbindPropFromVariable);\n const isBindingImmutable = useLayerStore((state) => state.isBindingImmutable);\n\n const handleBind = () => {\n // Bind a component's 'title' prop to a variable\n bindPropToVariable('button-123', 'title', 'page-title-var');\n };\n\n const handleUnbind = () => {\n // Check if binding is immutable first\n if (!isBindingImmutable('button-123', 'title')) {\n unbindPropFromVariable('button-123', 'title');\n }\n };\n\n return (\n
\n \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.ts b/app/docs/docs-data/docs-page-layers/variables.ts new file mode 100644 index 0000000..841b01e --- /dev/null +++ b/app/docs/docs-data/docs-page-layers/variables.ts @@ -0,0 +1,83 @@ +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + +export const VARIABLES_LAYER = { + "id": "variables", + "type": "div", + "name": "Variables", + "props": { + "className": "h-full bg-background px-4 flex flex-col gap-6 min-h-screen", + "data-group": "data-variables" + }, + "children": [ + { + "type": "span", + "children": "Variables", + "id": "variables-title", + "name": "Text", + "props": { + "className": "text-4xl" + } + }, + { + "id": "variables-intro", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "**Variables are the key to creating dynamic, data-driven interfaces with UI Builder.** Instead of hardcoding static values into your components, variables allow you to bind component properties to dynamic data that can change at runtime.\n\nThis transforms static designs into powerful applications with:\n- **Personalized content** that adapts to user data\n- **Reusable templates** that work across different contexts \n- **Multi-tenant applications** with customized branding per client\n- **A/B testing** and feature flags through boolean variables" + }, + { + "id": "variables-demo", + "type": "div", + "name": "div", + "props": {}, + "children": [ + { + "id": "variables-badge", + "type": "Badge", + "name": "Badge", + "props": { + "variant": "default", + "className": "rounded rounded-b-none" + }, + "children": [ + { + "id": "variables-badge-text", + "type": "span", + "name": "span", + "props": {}, + "children": "Live Variables Example" + } + ] + }, + { + "id": "variables-demo-frame", + "type": "div", + "name": "div", + "props": { + "className": "border border-primary shadow-lg rounded-b-sm rounded-tr-sm overflow-hidden" + }, + "children": [ + { + "id": "variables-iframe", + "type": "iframe", + "name": "iframe", + "props": { + "src": "/examples/renderer/variables", + "title": "Variables Demo", + "className": "aspect-square md:aspect-video w-full" + }, + "children": [] + } + ] + } + ] + }, + { + "id": "variables-content", + "type": "Markdown", + "name": "Markdown", + "props": {}, + "children": "## Variable Types\n\nUI Builder supports three typed variables:\n\n```tsx\ninterface Variable {\n id: string; // Unique identifier\n name: string; // Display name (becomes property name in generated code)\n type: 'string' | 'number' | 'boolean';\n defaultValue: string | number | boolean; // Must match the type\n}\n\n// Examples:\nconst stringVar: Variable = {\n id: 'page-title',\n name: 'pageTitle',\n type: 'string',\n defaultValue: 'Welcome to UI Builder'\n};\n\nconst numberVar: Variable = {\n id: 'user-age',\n name: 'userAge', \n type: 'number',\n defaultValue: 25\n};\n\nconst booleanVar: Variable = {\n id: 'is-loading',\n name: 'isLoading',\n type: 'boolean',\n defaultValue: false\n};\n```\n\n💡 **See it in action**: The demo above shows all three types with real-time variable binding and runtime value overrides.\n\n## Creating Variables\n\n### Via Initial Variables Prop\n\nSet up variables when initializing the UIBuilder:\n\n```tsx\nimport UIBuilder from '@/components/ui/ui-builder';\nimport { Variable } from '@/components/ui/ui-builder/types';\n\nconst initialVariables: Variable[] = [\n {\n id: 'welcome-msg',\n name: 'welcomeMessage',\n type: 'string',\n defaultValue: 'Welcome to our site!'\n },\n {\n id: 'user-count',\n name: 'userCount',\n type: 'number', \n defaultValue: 0\n },\n {\n id: 'show-banner',\n name: 'showBanner',\n type: 'boolean',\n defaultValue: true\n }\n];\n\nfunction App() {\n return (\n {\n // Persist variable definitions to your backend\n console.log('Variables updated:', variables);\n }}\n />\n );\n}\n```\n\n### Via the Data Panel\n\nUsers can create variables directly in the editor:\n\n1. **Navigate to the \"Data\" tab** in the left panel\n2. **Click \"Add Variable\"** to create a new variable\n3. **Choose variable type** (string, number, boolean)\n4. **Set name and default value**\n5. **Variable is immediately available** for binding in the props panel\n\n## Using Variables\n\nVariables can be bound to component properties in two ways:\n\n### Manual Binding\nUsers can bind variables to component properties in the props panel by clicking the link icon next to any field.\n\n### Automatic Binding \nComponents can be configured to automatically bind to specific variables when added:\n\n```tsx\nconst componentRegistry = {\n UserProfile: {\n component: UserProfile,\n schema: z.object({\n userId: z.string(),\n displayName: z.string(),\n }),\n from: '@/components/ui/user-profile',\n // Automatically bind user data when component is added\n defaultVariableBindings: [\n { \n propName: 'userId', \n variableId: 'current-user-id', \n immutable: true // Cannot be unbound in UI\n },\n { \n propName: 'displayName', \n variableId: 'current-user-name', \n immutable: false // Can be changed by users\n }\n ]\n }\n};\n```\n\n**Immutable bindings** prevent users from unbinding critical variables for system data, branding consistency, and template integrity.\n\n> 💡 **Learn more**: See [Variable Binding](/docs/variable-binding) for detailed binding mechanics and [Data Binding](/docs/data-binding) for connecting to external data sources.\n\n## Variable Management\n\n### Controlling Variable Editing\n\nControl whether users can edit variables in the UI:\n\n```tsx\n\n```\n\nWhen `allowVariableEditing` is `false`:\n- Variables panel becomes read-only\n- \"Add Variable\" button is hidden\n- Edit/delete buttons on individual variables are hidden\n- Variable values can still be overridden at runtime during rendering\n\n### Variable Change Handling\n\nRespond to variable definition changes in the editor:\n\n```tsx\nfunction App() {\n const handleVariablesChange = (variables: Variable[]) => {\n // Persist variable definitions to backend\n fetch('/api/variables', {\n method: 'POST',\n body: JSON.stringify(variables)\n });\n };\n\n return (\n \n );\n}\n```\n\n## Common Use Cases\n\n### Personalization\n\n```tsx\n// Variables for user-specific content\nconst userVariables: Variable[] = [\n { id: 'user-name', name: 'userName', type: 'string', defaultValue: 'User' },\n { id: 'user-avatar', name: 'userAvatar', type: 'string', defaultValue: '/default-avatar.png' },\n { id: 'is-premium', name: 'isPremiumUser', type: 'boolean', defaultValue: false }\n];\n```\n\n### Feature Flags\n\n```tsx\n// Variables for conditional features\nconst featureFlags: Variable[] = [\n { id: 'show-beta-feature', name: 'showBetaFeature', type: 'boolean', defaultValue: false },\n { id: 'enable-dark-mode', name: 'enableDarkMode', type: 'boolean', defaultValue: true }\n];\n```\n\n### Multi-tenant Branding\n\n```tsx\n// Variables for client-specific branding\nconst brandingVariables: Variable[] = [\n { id: 'company-name', name: 'companyName', type: 'string', defaultValue: 'Acme Corp' },\n { id: 'primary-color', name: 'primaryColor', type: 'string', defaultValue: '#3b82f6' },\n { id: 'logo-url', name: 'logoUrl', type: 'string', defaultValue: '/default-logo.png' }\n];\n```\n\n## Best Practices\n\n- **Use descriptive names** for variables (e.g., `userName` not `u`)\n- **Choose appropriate types** for your data (string for text, number for counts, boolean for flags)\n- **Set meaningful default values** for better preview experience in the editor\n- **Use immutable bindings** for system-critical data that shouldn't be unbound\n- **Group related variables** with consistent naming patterns\n- **Keep variable names simple** - they become property names in generated code\n- **Separate variable definitions from values** - define structure in the editor, inject data at runtime\n\n## Variable Workflow Summary\n\n1. **Define Variables**: Create variable definitions in the editor or via `initialVariables`\n2. **Bind to Components**: Link component properties to variables in the props panel\n3. **Save Structure**: Store the page structure and variable definitions (via `onChange` and `onVariablesChange`)\n4. **Render with Data**: Use `LayerRenderer` with `variableValues` to inject real data at runtime\n\nThis workflow enables the separation of content structure from actual data, making your UI Builder pages truly dynamic and reusable.\n\n> 📚 **Next Steps**: Learn about [Variable Binding](/docs/variable-binding) for detailed binding mechanics, [Data Binding](/docs/data-binding) for external data integration, and [Rendering Pages](/docs/rendering-pages) for runtime usage." + } + ] + } as const satisfies ComponentLayer; \ No newline at end of file diff --git a/app/docs/layout.tsx b/app/docs/layout.tsx new file mode 100644 index 0000000..354ee66 --- /dev/null +++ b/app/docs/layout.tsx @@ -0,0 +1,43 @@ +import { Suspense } from "react"; +import { ThemeProvider } from "next-themes"; +import { AppSidebar } from "@/app/platform/app-sidebar"; +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar"; +import { DocBreadcrumbs } from "../platform/doc-breadcrumbs"; + +export const metadata = { + title: "Documentation - UI Builder", + description: "Everything you need to know about building UIs with our drag-and-drop builder.", +}; + +export default function DocsLayout({ + children +}: { + children: React.ReactNode, +}) { + return ( + + + + + + {children} + + + + ); +} + +function DocsHeader() { + return ( +
+ + }> + + +
+ ); +} \ No newline at end of file diff --git a/app/docs/page.tsx b/app/docs/page.tsx new file mode 100644 index 0000000..26deecb --- /dev/null +++ b/app/docs/page.tsx @@ -0,0 +1,59 @@ +import Link from "next/link"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { MENU_DATA } from "@/app/docs/docs-data/data"; +import { ArrowRightIcon } from "lucide-react"; + +export default function DocsPage() { + return ( +
+
+
+

Documentation

+

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

+
+ +
+ {MENU_DATA.map((section) => ( + + + {section.title} + + {getCardDescription(section.title)} + + + +
+ {section.items?.map((item) => ( + + {item.title} + + + ))} +
+
+
+ ))} +
+
+
+ ); +} + +function getCardDescription(title: string): string { + const descriptions: Record = { + "Core": "Get started with the fundamentals of the UI builder", + "Component System": "Learn about components, customization, and configuration", + "Editor Features": "Explore the powerful editor panels and features", + "Data & Variables": "Master data binding and variable management", + "Layout & Persistence": "Understand structure and state management", + "Rendering": "Learn how to render and theme your pages" + }; + + return descriptions[title] || "Explore this section"; +} \ No newline at end of file diff --git a/app/examples/basic/page.tsx b/app/examples/basic/page.tsx new file mode 100644 index 0000000..24321ad --- /dev/null +++ b/app/examples/basic/page.tsx @@ -0,0 +1,13 @@ +import { SimpleBuilder } from "@/app/platform/simple-builder"; + +export const metadata = { + title: "UI Builder", +}; + +export default function Page() { + return ( +
+ +
+ ); +} diff --git a/app/examples/editor/panel-config/page.tsx b/app/examples/editor/panel-config/page.tsx new file mode 100644 index 0000000..3c505e6 --- /dev/null +++ b/app/examples/editor/panel-config/page.tsx @@ -0,0 +1,14 @@ +import { PanelConfigDemo } from "app/platform/panel-config-demo"; + +export const metadata = { + title: "Panel Configuration - UI Builder", + description: "Demonstrates custom panel configuration options" +}; + +export default function Page() { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/app/examples/editor/read-only-mode/page.tsx b/app/examples/editor/read-only-mode/page.tsx new file mode 100644 index 0000000..b97c09c --- /dev/null +++ b/app/examples/editor/read-only-mode/page.tsx @@ -0,0 +1,14 @@ +import { ReadOnlyDemo } from "@/app/platform/read-only-demo"; + +export const metadata = { + title: "UI Builder - Read-Only Mode Demo", + description: "Interactive demonstration of UI Builder's read-only mode capabilities" +}; + +export default function ReadOnlyModePage() { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 7e48beb..da38c44 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,11 @@ /* eslint-disable @next/next/no-sync-scripts */ import "@/styles/globals.css"; +export const metadata = { + title: "UI Builder", + description: "An open source UI builder for building complex UIs", +}; + export default function RootLayout({ children, }: { diff --git a/app/page.tsx b/app/page.tsx index 7b4d4f8..7c791bc 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,13 +1,58 @@ -import { SimpleBuilder } from "./platform/simple-builder"; +import Link from "next/link"; +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { GithubIcon } from "lucide-react"; +import Image from "next/image"; -export const metadata = { - title: "UI Builder", -}; export default function Page() { return ( -
- +
+ + + + UI Builder + 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 new file mode 100644 index 0000000..90121e2 --- /dev/null +++ b/app/platform/app-sidebar.tsx @@ -0,0 +1,79 @@ +"use client" + +import * as React from "react" +import Link from "next/link" +import { usePathname } from "next/navigation" + +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, +} from "@/components/ui/sidebar" +import { MENU_DATA } from "@/app/docs/docs-data/data" +import Image from "next/image"; +import { GithubIcon } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { ThemeToggle } from "@/app/platform/theme-toggle" + +interface AppSidebarProps extends React.ComponentProps { + currentPath?: string; +} + +function AppSidebarContent({ currentPath, ...props }: AppSidebarProps) { + return ( + + + + UI Builder + UI Builder + + + + + {MENU_DATA.map((section) => ( + + {section.title} + + + {section.items?.map((item) => { + const isActive = currentPath === item.url; + return ( + + + {item.title} + + + ); + })} + + + + ))} + + + + + + + + + ) +} + +export function AppSidebar(props: Omit) { + const pathname = usePathname() + return +} diff --git a/app/platform/builder-drag-drop-test.tsx b/app/platform/builder-drag-drop-test.tsx index 6207143..8544268 100644 --- a/app/platform/builder-drag-drop-test.tsx +++ b/app/platform/builder-drag-drop-test.tsx @@ -806,6 +806,7 @@ export const BuilderDragDropTest = () => { allowPagesCreation={true} allowPagesDeletion={true} allowVariableEditing={true} + persistLayerStore={false} /> ); }; \ No newline at end of file diff --git a/app/platform/builder-with-immutable-bindings.tsx b/app/platform/builder-with-immutable-bindings.tsx index 8dfbf55..c4c9400 100644 --- a/app/platform/builder-with-immutable-bindings.tsx +++ b/app/platform/builder-with-immutable-bindings.tsx @@ -363,6 +363,7 @@ export const BuilderWithImmutableBindings = () => { allowPagesCreation={true} allowPagesDeletion={true} allowVariableEditing={false} + persistLayerStore={false} /> ); }; \ No newline at end of file diff --git a/app/platform/builder-with-pages.tsx b/app/platform/builder-with-pages.tsx index a9a2ea3..4c5ef74 100644 --- a/app/platform/builder-with-pages.tsx +++ b/app/platform/builder-with-pages.tsx @@ -990,6 +990,7 @@ export const BuilderWithPages = ({fixedPages = false}: {fixedPages?: boolean}) = }} allowPagesCreation={!fixedPages} allowPagesDeletion={!fixedPages} + persistLayerStore={false} panelConfig={getDefaultPanelConfigValues(defaultConfigTabsContent())} />; }; \ No newline at end of file diff --git a/app/platform/doc-breadcrumbs.tsx b/app/platform/doc-breadcrumbs.tsx new file mode 100644 index 0000000..e10ac6f --- /dev/null +++ b/app/platform/doc-breadcrumbs.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import { getBreadcrumbsFromUrl } from "@/app/docs/docs-data/data"; +import { usePathname } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; +import { PencilIcon } from "lucide-react"; + +export function DocBreadcrumbs() { + const pathname = usePathname(); + const slug = pathname.replace("/docs/", ""); + + const currentPath = `/docs/${slug}`; + const breadcrumbs = getBreadcrumbsFromUrl(currentPath); + return ( + <> + + + + + {breadcrumbs.category.title} + + + + + {breadcrumbs.page.title} + + + + {pathname != "/docs" && ( + + + + )} + + ); +} diff --git a/app/platform/doc-editor.tsx b/app/platform/doc-editor.tsx new file mode 100644 index 0000000..263b950 --- /dev/null +++ b/app/platform/doc-editor.tsx @@ -0,0 +1,22 @@ +"use client"; + +import React from "react"; +import UIBuilder from "@/components/ui/ui-builder"; +import { complexComponentDefinitions } from "@/lib/ui-builder/registry/complex-component-definitions"; +import { primitiveComponentDefinitions } from "@/lib/ui-builder/registry/primitive-component-definitions"; +import { ComponentLayer } from "@/components/ui/ui-builder/types"; + + + +export const DocEditor = ({page}: {page: ComponentLayer}) => { + return ( + + ); +} diff --git a/app/platform/doc-renderer.tsx b/app/platform/doc-renderer.tsx new file mode 100644 index 0000000..54f7c0c --- /dev/null +++ b/app/platform/doc-renderer.tsx @@ -0,0 +1,14 @@ +"use client"; + +import LayerRenderer from "@/components/ui/ui-builder/layer-renderer"; +import { complexComponentDefinitions } from "@/lib/ui-builder/registry/complex-component-definitions"; +import { primitiveComponentDefinitions } from "@/lib/ui-builder/registry/primitive-component-definitions"; + +import { ComponentLayer } from "@/components/ui/ui-builder/types"; +const COMPONENT_REGISTRY = { + ...complexComponentDefinitions, + ...primitiveComponentDefinitions, +} +export const DocRenderer = ({page, className}: {page: ComponentLayer, className?: string}) => { + return ; +}; \ No newline at end of file diff --git a/app/platform/panel-config-demo.tsx b/app/platform/panel-config-demo.tsx new file mode 100644 index 0000000..2ac40c4 --- /dev/null +++ b/app/platform/panel-config-demo.tsx @@ -0,0 +1,469 @@ +"use client" + +import React, { useState } from "react"; +import UIBuilder, { defaultConfigTabsContent, TabsContentConfig } from "@/components/ui/ui-builder"; +import { complexComponentDefinitions } from "@/lib/ui-builder/registry/complex-component-definitions"; +import { primitiveComponentDefinitions } from "@/lib/ui-builder/registry/primitive-component-definitions"; +import { ComponentLayer, Variable } from '@/components/ui/ui-builder/types'; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Settings, Database, Layout, Home, Code, Eye } from "lucide-react"; +import { useLayerStore } from "@/lib/ui-builder/store/layer-store"; +import { useEditorStore } from "@/lib/ui-builder/store/editor-store"; +import LayerRenderer from "@/components/ui/ui-builder/layer-renderer"; + +// Super Simple Custom Nav Component +const SimpleNav = () => { + const [showCodeDialog, setShowCodeDialog] = useState(false); + const [showPreviewDialog, setShowPreviewDialog] = useState(false); + + const selectedPageId = useLayerStore((state: any) => state.selectedPageId); + const findLayerById = useLayerStore((state: any) => state.findLayerById); + const componentRegistry = useEditorStore((state: any) => state.registry); + + const page = findLayerById(selectedPageId) as ComponentLayer; + + return ( + <> +
+
+ + UI Builder +
+
+ + +
+
+ + {/* 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/app/platform/renderer-with-vars.tsx b/app/platform/renderer-with-vars.tsx index 1dc2053..8286593 100644 --- a/app/platform/renderer-with-vars.tsx +++ b/app/platform/renderer-with-vars.tsx @@ -4,79 +4,360 @@ import LayerRenderer from "@/components/ui/ui-builder/layer-renderer"; import { ComponentLayer } from "@/components/ui/ui-builder/types"; import { ComponentRegistry } from "@/components/ui/ui-builder/types"; import { Variable } from '@/components/ui/ui-builder/types'; -import SimpleComponent from "app/platform/simple-component"; import { primitiveComponentDefinitions } from "@/lib/ui-builder/registry/primitive-component-definitions"; -import z from "zod"; - +import { complexComponentDefinitions } from "@/lib/ui-builder/registry/complex-component-definitions"; const myComponentRegistry: ComponentRegistry = { - SimpleComponent: { - component: SimpleComponent, - schema: z.object({ - someString: z.string(), - someNumber: z.number(), - someBoolean: z.boolean() - }), - from: "app/platform/simple-component" - }, - ...primitiveComponentDefinitions + ...primitiveComponentDefinitions, + ...complexComponentDefinitions }; +// Page structure with actual variable bindings const page: ComponentLayer = { - id: "example-page", + id: "variables-demo-page", type: "div", props: { - className: "flex flex-col items-center justify-center gap-4 p-4 bg-gray-100" + className: "max-w-4xl mx-auto p-8 space-y-8 bg-gradient-to-br from-blue-50 to-purple-50 min-h-screen" }, children: [ + // Header with variable binding + { + id: "header", + type: "div", + props: { + className: "text-center space-y-4" + }, + children: [ + { + id: "page-title", + type: "h1", + props: { + className: "text-4xl font-bold text-gray-900", + children: { __variableRef: "pageTitle" } // Bound to variable + }, + children: [] + }, + { + id: "page-subtitle", + type: "p", + props: { + className: "text-xl text-gray-600", + children: { __variableRef: "pageSubtitle" } // Bound to variable + }, + children: [] + } + ] + }, + // User info card with variable bindings + { + id: "user-card", + type: "div", + props: { + className: "bg-white rounded-lg shadow-lg p-6 border border-gray-200" + }, + children: [ + { + id: "user-card-title", + type: "h2", + props: { + className: "text-2xl font-semibold text-gray-800 mb-4", + children: "User Information" + }, + children: [] + }, + { + id: "user-info", + type: "div", + props: { + className: "space-y-3" + }, + children: [ + { + id: "user-name-row", + type: "div", + props: { + className: "flex items-center gap-2" + }, + children: [ + { + id: "name-label", + type: "span", + props: { + className: "font-medium text-gray-700 w-20", + children: "Name:" + }, + children: [] + }, + { + id: "user-name", + type: "span", + props: { + className: "text-gray-900", + children: { __variableRef: "userName" } // Bound to variable + }, + children: [] + } + ] + }, + { + id: "user-age-row", + type: "div", + props: { + className: "flex items-center gap-2" + }, + children: [ + { + id: "age-label", + type: "span", + props: { + className: "font-medium text-gray-700 w-20", + children: "Age:" + }, + children: [] + }, + { + id: "user-age", + type: "span", + props: { + className: "text-gray-900", + children: { __variableRef: "userAge" } // Bound to variable + }, + children: [] + } + ] + } + ] + } + ] + }, + // Interactive buttons with variable bindings + { + id: "buttons-section", + type: "div", + props: { + className: "bg-white rounded-lg shadow-lg p-6 border border-gray-200" + }, + children: [ + { + id: "buttons-title", + type: "h2", + props: { + className: "text-2xl font-semibold text-gray-800 mb-4", + children: "Dynamic Buttons" + }, + children: [] + }, + { + id: "buttons-container", + type: "div", + props: { + className: "flex flex-wrap gap-4" + }, + children: [ + { + id: "primary-button", + type: "Button", + props: { + variant: "default", + className: "flex-1 min-w-fit", + children: { __variableRef: "buttonText" }, // Bound to variable + disabled: { __variableRef: "isLoading" } // Bound to variable + }, + children: [] + }, + { + id: "secondary-button", + type: "Button", + props: { + variant: "outline", + className: "flex-1 min-w-fit", + children: { __variableRef: "secondaryButtonText" } // Bound to variable + }, + children: [] + } + ] + } + ] + }, + // Feature flags section { - id: "simple-component", - type: "SimpleComponent", + id: "features-section", + type: "div", props: { - someString: "Hello", - someNumber: 123, - someBoolean: true + className: "bg-white rounded-lg shadow-lg p-6 border border-gray-200" }, - children: [] + children: [ + { + id: "features-title", + type: "h2", + props: { + className: "text-2xl font-semibold text-gray-800 mb-4", + children: "Feature Toggles" + }, + children: [] + }, + { + id: "features-list", + type: "div", + props: { + className: "space-y-3" + }, + children: [ + { + id: "dark-mode-feature", + type: "div", + props: { + className: "flex items-center justify-between p-3 bg-gray-50 rounded" + }, + children: [ + { + id: "dark-mode-label", + type: "span", + props: { + className: "font-medium text-gray-700", + children: "Dark Mode" + }, + children: [] + }, + { + id: "dark-mode-badge", + type: "Badge", + props: { + variant: { __variableRef: "darkModeEnabled" }, // Bound to variable (will resolve to "default" or "secondary") + children: { __variableRef: "darkModeStatus" } // Bound to variable + }, + children: [] + } + ] + }, + { + id: "notifications-feature", + type: "div", + props: { + className: "flex items-center justify-between p-3 bg-gray-50 rounded" + }, + children: [ + { + id: "notifications-label", + type: "span", + props: { + className: "font-medium text-gray-700", + children: "Notifications" + }, + children: [] + }, + { + id: "notifications-badge", + type: "Badge", + props: { + variant: { __variableRef: "notificationsEnabled" }, // Bound to variable + children: { __variableRef: "notificationsStatus" } // Bound to variable + }, + children: [] + } + ] + } + ] + } + ] } ] -} +}; -// Define variables for the page +// Define variables that match the bindings const variables: Variable[] = [ { - id: "someString", - name: "String Variable", + id: "pageTitle", + name: "Page Title", + type: "string", + defaultValue: "UI Builder Variables Demo" + }, + { + id: "pageSubtitle", + name: "Page Subtitle", + type: "string", + defaultValue: "See how variables make your content dynamic" + }, + { + id: "userName", + name: "User Name", type: "string", - defaultValue: "Hello" + defaultValue: "John Doe" }, { - id: "someNumber", - name: "Number Variable", + id: "userAge", + name: "User Age", type: "number", - defaultValue: 123 + defaultValue: 25 }, { - id: "someBoolean", - name: "Boolean Variable", + id: "buttonText", + name: "Primary Button Text", + type: "string", + defaultValue: "Click Me!" + }, + { + id: "secondaryButtonText", + name: "Secondary Button Text", + type: "string", + defaultValue: "Learn More" + }, + { + id: "isLoading", + name: "Loading State", type: "boolean", - defaultValue: true + defaultValue: false + }, + { + id: "darkModeEnabled", + name: "Dark Mode Enabled", + type: "string", + defaultValue: "secondary" + }, + { + id: "darkModeStatus", + name: "Dark Mode Status", + type: "string", + defaultValue: "Disabled" + }, + { + id: "notificationsEnabled", + name: "Notifications Enabled", + type: "string", + defaultValue: "default" + }, + { + id: "notificationsStatus", + name: "Notifications Status", + type: "string", + defaultValue: "Enabled" } ]; +// Override some variable values to show dynamic behavior const variableValues = { - someString: "Hello", - someNumber: 123, - someBoolean: true + pageTitle: "🚀 Dynamic Variables in Action", + pageSubtitle: "Values injected at runtime - try changing them!", + userName: "Jane Smith", + userAge: 30, + buttonText: "Get Started Now", + secondaryButtonText: "View Documentation", + isLoading: false, + darkModeEnabled: "default", + darkModeStatus: "Enabled", + notificationsEnabled: "secondary", + notificationsStatus: "Disabled" }; export function RendererWithVars() { return ( - +
+ +
); } \ No newline at end of file diff --git a/app/platform/simple-builder.tsx b/app/platform/simple-builder.tsx index 6185b36..08a53b9 100644 --- a/app/platform/simple-builder.tsx +++ b/app/platform/simple-builder.tsx @@ -11,7 +11,7 @@ const initialLayers = [ type: "div", name: "Page", props: { - className: "bg-gray-200 flex flex-col justify-center items-center gap-4 p-2 w-full h-96", + className: "bg-gray-200 flex flex-col justify-center items-center gap-4 p-2 w-full h-screen", }, children: [ { @@ -19,7 +19,7 @@ const initialLayers = [ type: "div", name: "Box A", props: { - className: "bg-red-300 p-2 w-1/2 h-1/3 text-center", + className: "flex flex-row justify-center items-center bg-red-300 p-2 w-full md:w-1/2 h-1/3 text-center", }, children: [ { @@ -27,7 +27,7 @@ const initialLayers = [ type: "span", name: "Text", props: { - className: "text-4xl font-bold text-white", + className: "text-4xl font-bold text-secondary", }, children: "A", } @@ -38,7 +38,7 @@ const initialLayers = [ type: "div", name: "Box B", props: { - className: "bg-green-300 p-2 w-1/2 h-1/3 text-center", + className: "flex flex-row justify-center items-center bg-green-300 p-2 w-full md:w-1/2 h-1/3 text-center", }, children: [ { @@ -46,7 +46,7 @@ const initialLayers = [ type: "span", name: "Text", props: { - className: "text-4xl font-bold text-white", + className: "text-4xl font-bold text-secondary", }, children: "B", } @@ -57,7 +57,7 @@ const initialLayers = [ type: "div", name: "Box C", props: { - className: "flex flex-row justify-center items-center bg-blue-300 p-2 w-1/2 h-1/3 p-2 w-1/2 h-1/3 text-center", + className: "flex flex-row justify-center items-center bg-blue-300 p-2 w-full md:w-1/2 h-1/3 p-2 w-1/2 h-1/3 text-center", }, children: [ { @@ -65,19 +65,21 @@ const initialLayers = [ type: "div", name: "Inner Box D", props: { - className: "bg-yellow-300 p-2 w-1/2 h-1/3 p-2 w-1/2 h-1/3 text-center", + className: "bg-yellow-300 p-2 w-1/2 p-2 w-1/2 h-auto text-center", }, - children: [], + children: [ + { + id: "7", + type: "span", + name: "Text", + props: { + className: "text-4xl font-bold text-secondary-foreground", + }, + children: "C", + } + ], }, - { - id: "7", - type: "span", - name: "Text", - props: { - className: "text-4xl font-bold text-white", - }, - children: "C", - } + ], }, ], diff --git a/app/platform/theme-toggle.tsx b/app/platform/theme-toggle.tsx new file mode 100644 index 0000000..b305dc9 --- /dev/null +++ b/app/platform/theme-toggle.tsx @@ -0,0 +1,46 @@ +"use client"; +import { useTheme } from "next-themes" +import { useCallback } from "react" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" +import { Button } from "@/components/ui/button" +import { SunIcon, MoonIcon } from "lucide-react" + +export function ThemeToggle() { + const { setTheme } = useTheme(); + + + const handleSetLightTheme = useCallback(() => { + setTheme("light"); + }, [setTheme]); + const handleSetDarkTheme = useCallback(() => { + setTheme("dark"); + }, [setTheme]); + const handleSetSystemTheme = useCallback(() => { + setTheme("system"); + }, [setTheme]); + + return ( + + + + + + + + Toggle theme + + + 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) =>