Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions app/(edit)/docs/[slug]/edit/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function EditLayout({ children }: { children: React.ReactNode }) {
return <div>{children}</div>;
}
18 changes: 18 additions & 0 deletions app/(edit)/docs/[slug]/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <DocEditor page={page} />
}
55 changes: 55 additions & 0 deletions app/docs/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Metadata> {
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 (
<div className="flex-1 overflow-auto">
<Suspense fallback={<DocSkeleton />}>
<DocRenderer className="max-w-6xl mx-auto my-8" page={page} />
</Suspense>
</div>
);
}

function DocSkeleton() {
return (
<div className="flex-1 overflow-auto">
<div className="max-w-6xl mx-auto my-8 flex flex-col gap-4">
<Skeleton className="h-10 w-full" />
<Skeleton className="h-96 w-full" />
<Skeleton className="h-96 w-full" />
</div>
</div>
);
}
172 changes: 172 additions & 0 deletions app/docs/docs-data/data.ts
Original file line number Diff line number Diff line change
@@ -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);
}
90 changes: 90 additions & 0 deletions app/docs/docs-data/docs-page-layers/component-registry.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading