olliethedev / ui-builder

A React component that provides a no-code, visual way to create UIs, compatible with shadcn/ui and custom components.
https://www.uibuilder.app/
MIT License
211 stars 26 forks source link
reactjs shadcn-ui tailwindcss ui-builder

UI Builder for @shadcn/ui

UI Builder is a React component that allows you to create and edit user interfaces through a visual no-code editor. It comes with a handful of core components and integrates easily with existing shadcn/ui projects and can be extended to use your own custom components.

You can use UI Builder to design and build UIs quickly. It's a great tool for creating landing pages, forms, dashboards, or anything else you can imagine. It can be used internally within your organization as a storybook alternative or a prototyping tool or as part of a product that provides users a no-code way to build their own applications, like Shopify, Builder.io, Framer, etc.

See the demo to get an idea of how it works.

UI Builder Demo

Installation

If you are using the latest shadcn/ui in your project, you can install the component directly from the registry.

npx shadcn@latest add https://raw.githubusercontent.com/olliethedev/ui-builder/main/registry/block-registry.json

Or you can start a new project with the UI Builder:

npx shadcn@latest init https://raw.githubusercontent.com/olliethedev/ui-builder/main/registry/block-registry.json

Note: You need to use style variables to have page theming working correctly.

If you are not using shadcn/ui, you can install the component simply by copying the files in this repo into your project.

Fixing Dependencies after shadcn init or add

Add dev dependencies, since there currently seems to be an issue with shadcn/ui not installing them from the registry:

npm install -D @types/lodash.template @tailwindcss/typography @types/react-syntax-highlighter react-docgen-typescript tailwindcss-animate ts-morph ts-to-zod

And that's it! You have a UI Builder that you can use to build your UI.

Usage

Basic Example

import UIBuilder from "@/components/ui/ui-builder";

export function App() {
  return <UIBuilder />;
}

By default the state of the UI is stored in the browser's local storage, so it will persist across sessions.

Example with initial state and onChange callback

import React from "react";
import UIBuilder from "@/components/ui/ui-builder";
import { PageLayer } from "@/lib/ui-builder/store/layer-store";

// Static initial layers or you can fetch from database
const initialLayers: PageLayer[] = [
  {
    id: "1",
    type: "_page_",
    name: "Page 1",
    props: {
      className: "p-4 flex flex-col gap-2",
    },
    children: [
      {
        id: "qCTIIed",
        type: "Button",
        name: "Button",
        props: {
            variant: "default",
            size: "default",
            className: "w-full items-center gap-2 max-w-sm",
        },
        children: [
            {
                id: "UzZY6Dp",
                type: "span",
                name: "span",
                props: {},
                children: "Hello World",
            },
            {
                id: "hn3PF6A",
                type: "Icon",
                name: "Icon",
                props: {
                    size: "medium",
                    color: "secondary",
                    rotate: "none",
                    iconName: "Github",
                    className: "",
                },
                children: [],
            },
        ],
        },
    ],
  },
];

const App = () => {
  const handleLayersChange = (updatedLayers: PageLayer[]) => {
    // Here you can send the updated layers to the backend
    console.log(updatedLayers);
  };

  return (
    <div>
      <UIBuilder initialLayers={initialLayers} onChange={handleLayersChange} />
    </div>
  );
};

export default App;

You can also render the page layer without editor functionality by using the LayerRenderer component:

import LayerRenderer from "@/components/ui/ui-builder/layer-renderer";
import { PageLayer } from "@/lib/ui-builder/store/layer-store";

const page: PageLayer = {...} // Fetch or define your page

export function MyPage() {
  return <LayerRenderer page={page} />;
}

LayerRenderer is useful when you want to render the finished page without any editor functionality.

Add your custom components to the registry

Navigate to the component-registry.tsx file and add your component definitions to the array. Here is an example of how to define a custom component:


import { ComponentRegistry } from "@/lib/ui-builder/registry/component-registry";
import { z } from 'zod';
import { FancyComponent } from '@/components/ui/fancy-component';
import { classNameFieldOverrides, childrenFieldOverrides } from "@/lib/ui-builder/registry/form-field-overrides";

export const customComponentDefinitions: ComponentRegistry = {
    FancyComponent: {
        component: FancyComponent,
        schema: z.object({
            className: z.string().optional(),
            children: z.any().optional(),
            title: z.string().default("Default Title"),
            count: z.coerce.number().default(1),
            disabled: z.boolean().optional(),
            timestamp: z.coerce.date().optional(),
            mode: z
                .enum([
                    "fancy",
                    "boring"
                ])
                .default("fancy"),
        }),
        from: "@/components/ui/button",
        fieldOverrides: {
            className:(layer)=> classNameFieldOverrides(layer),
            children: (layer)=> childrenFieldOverrides(layer)
        }
    },
}

See core concepts below for more information on the component definitions.

Optional: Generate the component registry for your components

To generate the component definition for your project components you can run the following command:

npx tsx lib/ui-builder/scripts/zod-gen.ts

This will generate the component definitions at the root of every folder in your /components directory. Note: You should wrap the generated schema patchSchema from schema-uitls.ts this will fix some issues with the generated schema and make sure they play well with auto-form. But in many complex component cases the generated files will need to be refactored manually. But the script will save you a lot of time compared to writing the definitions manually. The script uses ts-morph , react-docgen-typescript, and ts-to-zod to generate the component definitions.

UI Builder Technical Overview


Note: This project is an work in progress and the API will change.


Core Concepts

Layers

Pages

Components

Core Types

Layer Types (layer-store.ts)

The layer-store.ts module defines the essential types used to manage UI layers.


export type Layer =
  | ComponentLayer
  | PageLayer;

export type ComponentLayer = {
  id: string;
  name?: string;
  type: keyof typeof componentRegistry;
  props: Record<string, any>;
  children: Layer[] | string;
};

export type PageLayer = {
  id: string;
  name?: string;
  type: '_page_';
  props: Record<string, any>;
  children: Layer[];
}

interface LayerStore {
  pages: PageLayer[];
  selectedLayerId: string | null;
  selectedPageId: string;
  initialize: (pages: PageLayer[]) => void;
  addComponentLayer: (layerType: keyof typeof componentRegistry, parentId: string, parentPosition?: number) => void;
  addPageLayer: (pageId: string) => void;
  duplicateLayer: (layerId: string, parentId?: string) => void;
  removeLayer: (layerId: string) => void;
  updateLayer: (layerId: string, newProps: Record<string, any>, layerRest?: Partial<Omit<Layer, 'props'>>) => void;
  selectLayer: (layerId: string) => void;
  selectPage: (pageId: string) => void;
  findLayerById: (layerId: string | null) => Layer | undefined;
  findLayersForPageId: (pageId: string) => Layer[];
}

ComponentRegistry Types (component-registry.tsx)

The component-registry.tsx module manages the registration and configuration of UI components.

export interface RegistryEntry<T extends ReactComponentType<any>> {
  component?: T;
  schema: ZodObject<any>;
  from?: string;
  defaultChildren?: (ComponentLayer)[];
  fieldOverrides?: Record<string, FieldConfigFunction>;
}

export type ComponentRegistry = Record<
  string,
  RegistryEntry<ReactComponentType<any>>
>;

export type FieldConfigFunction = (layer: ComponentLayer) => FieldConfigItem;

export const componentRegistry: ComponentRegistry = {
  // ...YourOtherProjectComponentDefinitions
  ...complexComponentDefinitions,
  ...primitiveComponentDefinitions,
} as const;

export const generateFieldOverrides = (layer: ComponentLayer): Record<string, FieldConfigItem> => {...}

Registration Structure

Each component is registered with the following details:

Example Registration

export const componentRegistry: ComponentRegistry = {
  Button: {
    component: Button,
    schema: z.object({
        className: z.string().optional(),
        children: z.any().optional(),
        asChild: z.boolean().optional(),
        variant: z
            .enum([
                "default",
                "destructive",
                "outline",
                "secondary",
                "ghost",
                "link",
            ])
            .default("default"),
        size: z.enum(["default", "sm", "lg", "icon"]).default("default"),
    }),
    from: "@/components/ui/button",
    defaultChildren: [
        {
            id: "button-text",
            type: "span",
            name: "span",
            props: {},
            children: "Button",
        } satisfies ComponentLayer,
    ],
    fieldOverrides: {
        className:(layer)=> classNameFieldOverrides(layer),
        children: (layer)=> childrenFieldOverrides(layer)
    }
  }
  // ... Other component definitions
};

Button Component:

Changelog

v0.0.2

Development

Build component registry after updating lib or ui:

npm run build-registry

Host the registry locally:

npm run host-registry

Use the local registry in a local project:

npx shadcn@latest add http://127.0.0.1:8080/block-registry.json -o

Running Tests

npm run test

Roadmap

License

MIT