Skip to content

Commit 80ca912

Browse files
committed
feat: performance and ux improvements
1 parent e62befe commit 80ca912

File tree

13 files changed

+307
-105
lines changed

13 files changed

+307
-105
lines changed

README.md

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,32 @@ Perfect for building landing pages, email templates, forms, admin dashboards, or
1515
🛠 Extend it. 🎨 Style it. 🚀 Own it.
1616

1717

18-
See the [demo](https://uibuilder.app/demo) to get an idea of how it works.
19-
Read the [docs](https://uibuilder.app/) to get started.
18+
See the [demo](https://uibuilder.app/) to get an idea of how it works.
2019

2120
![UI Builder Demo](./public/demo.png)
2221

22+
## How it Works
23+
24+
UI Builder empowers you to visually construct and modify user interfaces by leveraging your own React components. Here's a breakdown of its core principles:
25+
26+
* **Component-Driven Foundation**: At its heart, UI Builder operates on your existing React components. You provide a `componentRegistry` detailing the components you want to make available in the editor. This registry includes the component itself, its Zod schema for props, its import path for code generation, and optional UI customizations for the editor.
27+
28+
* **Layer-Based Canvas**: The user interface is constructed as a tree of "layers." Each layer represents an instance of a component from your registry. Users can visually add, remove, reorder, and nest these layers on an interactive canvas, offering a direct manipulation experience. Any component type, like `div`, `main`, or custom containers, can serve as the root page layer.
29+
30+
* **Dynamic Props Editing**: Each component in the registry is defined with a Zod schema that outlines its props, their types, and default values. UI Builder utilizes this schema to automatically generate a properties panel (using [Auto-Form](https://github.com/vantezzen/autoform/tree/pure-shadcn)). This allows users to configure component instances in real-time, with changes immediately reflected on the canvas. Default values in the Zod schema are crucial for ensuring components render correctly when first added.
31+
32+
* **Centralized `componentRegistry`**: The `componentRegistry` prop passed to the `UIBuilder` component is central to its operation. It's a JavaScript object where you map a unique string identifier (e.g., "Button", "FancyComponent") to each component's detailed definition.
33+
34+
* **Flexible State Management**: By default, the editor's state (the arrangement and configuration of layers) is persisted in the browser's local storage for convenience across sessions. For more robust or backend-integrated solutions, you can provide `initialLayers` to seed the editor (e.g., from a database) and use the `onChange` callback to capture state changes and persist them as needed.
35+
36+
* **React Code Generation**: A key feature is the ability to export the visually designed page structure as clean, readable React code. This process uses the import paths (`from` property) specified in your component registry, ensuring the generated code correctly references your components.
37+
38+
* **Extensibility and Customization**: The system is designed for deep integration with your project:
39+
* Customize the properties form for each component using `fieldOverrides` to enhance the editing experience (e.g., custom labels, input types, or conditional visibility).
40+
* Provide custom React components via the `panelConfig` prop to provide custom components for the editor panels.
41+
* If you only need to display a UI Builder page without the editor, the `LayerRenderer` component can render it using the same `componentRegistry` and page data.
42+
43+
This section aims to give a clearer picture of the UI Builder's architecture and how its different parts interact.
2344

2445
## Installation
2546

@@ -59,20 +80,19 @@ To use the UI Builder, you need to provide a component registry and a form compo
5980
import UIBuilder from "@/components/ui/ui-builder";
6081
import { ComponentRegistry } from "@/components/ui/ui-builder/types";
6182
import { primitiveComponentDefinitions } from "@/lib/ui-builder/registry/primitive-component-definitions"; // Basic html primitives registry
62-
import { complexComponentDefinitions } from "@/lib/ui-builder/registry/complex-component-definitions"; // Basic shadcn/ui components registry
63-
import { ThemePanel } from "@/components/ui/ui-builder/internal/theme-panel"; // Tailwind+shadcn theme settings form for the page root component
83+
import { complexComponentDefinitions } from "@/lib/ui-builder/registry/complex-component-definitions"; // Sample shadcn/ui components registry
6484

6585
// Combine or define your component registry
6686
const myComponentRegistry: ComponentRegistry = {
6787
...primitiveComponentDefinitions,
88+
...complexComponentDefinitions,
6889
// ...add your custom components here
6990
};
7091

7192
export function App() {
7293
return (
7394
<UIBuilder
7495
componentRegistry={myComponentRegistry}
75-
pagePropsForm={<ThemePanel />}
7696
/>
7797
);
7898
}
@@ -90,7 +110,6 @@ import UIBuilder from "@/components/ui/ui-builder";
90110
import { ComponentLayer, ComponentRegistry } from "@/components/ui/ui-builder/types";
91111
import { primitiveComponentDefinitions } from "@/lib/ui-builder/registry/primitive-component-definitions";
92112
import { complexComponentDefinitions } from "@/lib/ui-builder/registry/complex-component-definitions";
93-
import { ThemePanel } from "@/components/ui/ui-builder/internal/theme-panel";
94113

95114
const myComponentRegistry: ComponentRegistry = {
96115
...primitiveComponentDefinitions,
@@ -102,11 +121,10 @@ const myComponentRegistry: ComponentRegistry = {
102121
const initialLayers: ComponentLayer[] = [
103122
{
104123
id: "page1",
105-
type: "div", // Any component type can be a page root
124+
type: "div", // Ensure div is in your componentRegistry
106125
name: "Page 1",
107126
props: {
108127
className: "p-4 flex flex-col gap-2",
109-
// You might include theme/styling props here if your pagePropsForm handles them
110128
},
111129
children: [
112130
{
@@ -157,7 +175,6 @@ const App = () => {
157175
initialLayers={initialLayers}
158176
onChange={handleLayersChange}
159177
componentRegistry={myComponentRegistry}
160-
pagePropsForm={<ThemePanel />}
161178
/>
162179
</div>
163180
);
@@ -167,11 +184,12 @@ export default App;
167184

168185
```
169186

187+
### UIBuilder Props
170188
- `componentRegistry`: **Required**. An object mapping component type names to their definitions (component, schema, import path, etc.).
171-
- `pagePropsForm`: **Required**. A React component used as the form to edit properties of the root page layer.
172189
- `initialLayers`: Optional prop to set up initial pages and layers. Useful for setting the initial state of the builder, from a database for example.
173190
- `onChange`: Optional callback triggered when the editor state changes, providing the updated pages. Can be used to persist the state to a database.
174-
- `useCanvas`: Optional prop to disable the interactive canvas. Defaults to true.
191+
- `panelConfig`: Optional. An object to customize the different panels of the UI Builder (e.g., nav bar, editor panel, props panel). If not provided, a default configuration is used. This allows for fine-grained control over the editor's appearance and layout.
192+
- `persistLayerStore`: Optional boolean (defaults to `true`). Determines whether the editor's state (layers and their configurations) is persisted in the browser's local storage across sessions. Set to `false` to disable local storage persistence, useful if you are managing state entirely through `initialLayers` and `onChange`.
175193

176194

177195
You can also render the page layer without editor functionality by using the LayerRenderer component:
@@ -205,10 +223,10 @@ Here is an example of how to define a custom component within the registry objec
205223

206224
```tsx
207225
import { z } from 'zod';
208-
import { FancyComponent } from '@/components/ui/fancy-component';
226+
import { FancyComponent } from '@/components/ui/fancy-component'; // Example custom component
209227
import { classNameFieldOverrides, childrenFieldOverrides } from "@/lib/ui-builder/registry/form-field-overrides";
210-
import { primitiveComponentDefinitions } from "@/lib/ui-builder/registry/primitive-component-definitions"; // Example primitive components
211-
import { ComponentRegistry } from "@/components/ui/ui-builder/types"; // Correct type import
228+
import { primitiveComponentDefinitions } from "@/lib/ui-builder/registry/primitive-component-definitions";
229+
import { ComponentRegistry } from "@/components/ui/ui-builder/types";
212230

213231
// Define your custom component's schema and configuration
214232
const fancyComponentDefinition = {
@@ -217,7 +235,7 @@ const fancyComponentDefinition = {
217235
// The Zod schema defining the component's props
218236
schema: z.object({
219237
className: z.string().optional(),
220-
children: z.any().optional(), // Use z.any() or a more specific schema for children if needed
238+
children: z.any().optional(), // Use z.any() for children prop and provide custom handling in fieldOverrides
221239
title: z.string().default("Default Title"),
222240
count: z.coerce.number().default(1),
223241
disabled: z.boolean().optional(),
@@ -231,16 +249,11 @@ const fancyComponentDefinition = {
231249
}),
232250
// The import path for code generation
233251
from: "@/components/ui/fancy-component",
234-
// Customizations for the auto-generated properties form
252+
// Customizations for the properties auto-form
235253
fieldOverrides: {
236-
className: (layer) => classNameFieldOverrides(layer), // Standard handling for className
237-
children: (layer) => childrenFieldOverrides(layer), // Standard handling for children
238-
// You can add overrides for other fields here, e.g.:
239-
// count: { label: "Number of items" }, // Change the label
240-
// disabled: { // Conditionally hide the field
241-
// fieldType: "switch", // Specify field type
242-
// isHidden: (layer) => layer.props.mode === "boring",
243-
// },
254+
className: (layer) => classNameFieldOverrides(layer), // Handle className selection with a custom component
255+
children: (layer) => childrenFieldOverrides(layer), // Handle children selection with a custom component
256+
// You can add overrides for other fields here
244257
}
245258
};
246259

@@ -252,17 +265,16 @@ export const myComponentRegistry: ComponentRegistry = {
252265
};
253266

254267
// Then pass `myComponentRegistry` to the UIBuilder prop:
255-
// <UIBuilder componentRegistry={myComponentRegistry} pagePropsForm={...} />
268+
// <UIBuilder componentRegistry={myComponentRegistry} />
256269
```
257270

258271
**Component Definition Fields:**
259272

260273
- `component`: **Required**. The React component function or class.
261274
- `schema`: **Required**. A Zod schema defining the component's props, their types, and validation rules.
262275
- This schema powers the automatic generation of a properties form in the editor using [Auto-Form](https://github.com/vantezzen/autoform/tree/pure-shadcn).
263-
- Props intended to be configurable in the UI Builder **MUST** have a default value specified in the schema (using `.default(...)`). This allows the UI Builder to render the component correctly when it's first added, before the user configures it.
264276
- Currently supported Zod types for auto-form generation include: `boolean`, `date`, `number`, `string`, `enum` (of supported types), `object` (with supported property types), and `array` (of objects with supported property types).
265-
- `from`: **Required**. The source import path for the component. This is used when exporting the page structure as React code.
277+
- `from`: **Required**. The source import path for the component. This is used when generating React code.
266278
- `fieldOverrides`: Optional. An object to customize the auto-generated form fields for the component's properties in the editor's sidebar.
267279
- The keys of this object correspond to the prop names defined in the Zod schema.
268280
- The values are typically functions that receive the current `layer` object and return configuration options for the `AutoForm` field. These options can control the field's label, input type (`fieldType`), visibility (`isHidden`), placeholder text, render logic, and more. See the AutoForm documentation for available options.
@@ -271,8 +283,34 @@ export const myComponentRegistry: ComponentRegistry = {
271283
* Using specific input components (e.g., a color picker, a custom slider).
272284
* Hiding props that shouldn't be user-editable (like internal state).
273285
* Implementing conditional logic (e.g., showing/hiding a field based on another prop's value).
274-
- The example uses `classNameFieldOverrides` and `childrenFieldOverrides` from `@/lib/ui-builder/registry/form-field-overrides` to provide standardized handling for common props like `className` (using a textarea) and `children` (often hidden or handled specially). You can create your own override functions or objects.
286+
- The example uses `classNameFieldOverrides` and `childrenFieldOverrides` from `@/lib/ui-builder/registry/form-field-overrides` to provide standardized handling for common props like `className` (using a auto suggest text input) and `children` (using a custom component). You can create your own override functions or objects.
287+
- `defaultChildren`: Optional. Default children to use when a new instance of this component is added to the canvas. For example setting initial text on a span component.
288+
- `isFromDefaultExport`: Optional. If true, indicates the component should be imported as a default export when generating React code.
289+
290+
291+
### Customizing the Page Config Panel Tabs
292+
293+
You can customize the tabs in the left "Page Config" panel (by default, "Layers" and "Appearance") by providing a `pageConfigPanelTabsContent` property inside the `panelConfig` prop. This allows you to change the tab titles or provide custom content for each tab.
294+
295+
```tsx
296+
import UIBuilder, { defaultConfigTabsContent } from "@/components/ui/ui-builder";
297+
298+
// Example: Customizing the tabs
299+
const myTabsContent = {
300+
layers: { title: "My Layers", content: <MyCustomLayersPanel /> },
301+
appearance: { title: "Theme", content: <MyCustomAppearancePanel /> },
302+
};
303+
304+
<UIBuilder
305+
componentRegistry={myComponentRegistry}
306+
panelConfig={{
307+
pageConfigPanelTabsContent: myTabsContent,
308+
// ...other panel overrides
309+
}}
310+
/>
311+
```
275312

313+
If you do not provide this, the default tabs ("Layers" and "Appearance") will be used.
276314

277315
---
278316
For more detailed documentation read the [docs](https://uibuilder.app/)
@@ -285,7 +323,7 @@ For more detailed documentation read the [docs](https://uibuilder.app/)
285323
- Removed _page_ layer type in favor of using any component type (like `div`, `main`, or custom containers) as the root page layer. This enhances flexibility, enabling use cases like building react-email templates directly. You should migrate any layers stored in the database to use a standard component type as the root. The [migrateV2ToV3](lib/ui-builder/store/layer-utils.ts) function in `layer-utils.ts` can assist with this migration.
286324
- The `componentRegistry` is now passed as a prop to the `UIBuilder` component instead of being defined in a standalone file.
287325
- Removed the script used to generate component schema definitions. This approach proved problematic to maintain and didn't function correctly for complex components or varying project setups. Component schema definitions should now be manually created or generated using project-specific tooling if desired.
288-
- `pagePropsForm` prop added to `UIBuilder` to allow customization of the form used for editing page-level (root layer) properties.
326+
- `panelConfig` prop added to `UIBuilder` to allow customization of the page config panel tabs.
289327
- Made `layer-store` local storage persistence optional and configurable via props.
290328

291329

__tests__/index.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,19 @@ jest.mock("../components/ui/ui-builder/internal/nav", () => ({
2626
}));
2727

2828
it("UIBuilder: renders", async () => {
29-
render(<UIBuilder componentRegistry={componentRegistry} pagePropsForm={<div>Page Props Form</div>} />);
29+
render(<UIBuilder componentRegistry={componentRegistry} />);
3030
const themeProvider = await screen.findByTestId("theme-provider");
3131
expect(themeProvider).toBeInTheDocument();
3232
});
3333

3434
it("UIBuilder: renders component-editor", async () => {
35-
render(<UIBuilder componentRegistry={componentRegistry} pagePropsForm={<div>Page Props Form</div>} />);
35+
render(<UIBuilder componentRegistry={componentRegistry} />);
3636
const componentEditor = await screen.findByTestId("component-editor");
3737
expect(componentEditor).toBeInTheDocument();
3838
});
3939

4040
it("UIBuilder: renders page-config-panel", async () => {
41-
render(<UIBuilder componentRegistry={componentRegistry} pagePropsForm={<div>Page Props Form</div>} />);
41+
render(<UIBuilder componentRegistry={componentRegistry} />);
4242
const pageConfigPanel = await screen.findByTestId("page-config-panel");
4343
expect(pageConfigPanel).toBeInTheDocument();
4444
});

__tests__/layers-panel.test.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ describe("LayersPanel", () => {
9393

9494
const mockEditorState = {
9595
registry: mockEditorRegistry,
96-
pagePropsForm: null,
9796
initializeRegistry: mockInitializeRegistry,
9897
getComponentDefinition: mockGetComponentDefinition,
9998
previewMode: 'responsive',

0 commit comments

Comments
 (0)