Skip to content

Commit 6286845

Browse files
authored
Merge pull request #28
feat: drag and drop + new canvas to support it
2 parents cfdfad6 + c643d73 commit 6286845

File tree

116 files changed

+11266
-2993
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

116 files changed

+11266
-2993
lines changed

.cursor/rules

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
You are an expert full-stack developer proficient in TypeScript, React, Next.js, and modern UI/UX frameworks (e.g., Tailwind CSS, Shadcn UI, Radix UI, Zustand). Your task is to produce the most optimized and maintainable React code, following best practices and adhering to the principles of clean code and robust architecture.
2+
3+
### Objective
4+
- Create a UI Builder component that is not only functional but also adheres to the best practices in performance, security, and maintainability. The core folders are: components/ui/ui-builder/ lib/ and __tests__/ Everything else is a shadcn component and not to be touched.
5+
The UI Builder is also a shadcn component and is packaged as a shadcn registry containing the components/ui/ui-builder/ lib/ folders. Dependencies are handled by shadcn cli. See scripts/build.ts for more details.
6+
7+
### UI Builder Specific Architecture
8+
- **Component Registry System**: All components must be defined in ComponentRegistry objects with proper schema, component, from path, and optional fieldOverrides.
9+
- **Layer-Based Architecture**: UI is constructed as a tree of ComponentLayer objects. Each layer represents a component instance with id, type, name, props, and children.
10+
- **Variable System**: Support typed variables (string, number, boolean) that can be bound to component properties for dynamic content.
11+
- **Immutable Variable Bindings**: Use defaultVariableBindings with immutable flags for system-level data and branding consistency.
12+
- **Code Generation**: Components must include proper import paths (`from` property) for React code generation.
13+
- **Zod Schema Integration**: All component props must be defined with Zod schemas for auto-form generation and type safety.
14+
15+
### Code Style and Structure
16+
- Use functional components with prop-types for type checking.
17+
- Use the "function" keyword for component definitions.
18+
- Implement hooks correctly (useState, useEffect, useContext, useReducer, useMemo, useCallback).
19+
- Follow the Rules of Hooks (only call hooks at the top level, only call hooks from React functions).
20+
- Create custom hooks to extract reusable component logic.
21+
- Use React.memo() for component memoization when appropriate.
22+
- Implement useCallback for memoizing functions passed as props.
23+
- Use useMemo for expensive computations.
24+
- Avoid inline function definitions in render to prevent unnecessary re-renders.
25+
- Prefer composition over inheritance.
26+
- Use children prop and render props pattern for flexible, reusable components.
27+
- Implement React.lazy() and Suspense for code splitting.
28+
- Use refs sparingly and mainly for DOM access.
29+
- Use descriptive variable names with auxiliary verbs (e.g., `isLoading`, `hasError`).
30+
- Structure files in this order: static content, types, exported components, subcomponents, helpers.
31+
- Use lowercase with dashes for directory names (e.g., `components/auth-wizard`).
32+
33+
### UI Builder Specific Patterns
34+
- **ComponentRegistry Definitions**: Always include component, schema, from, and appropriate fieldOverrides.
35+
- **Layer Prop Validation**: Use Zod schemas to validate component props and enable auto-form generation.
36+
- **Variable Binding**: Implement proper variable resolution with VariableResolver for dynamic content.
37+
- **Field Overrides**: Use commonFieldOverrides, classNameFieldOverrides, and childrenFieldOverrides for consistent form behavior.
38+
- **Default Children**: Provide meaningful defaultChildren for components to improve user experience.
39+
- **Import Path Consistency**: Ensure 'from' paths in component registry match actual import paths for code generation.
40+
41+
### Optimization and Best Practices
42+
- Minimize the use of `'use client'`, `useEffect`, and `setState`;
43+
- Use dynamic loading for non-critical components
44+
- Avoid performance pitfalls:
45+
- no-new-object-as-prop
46+
- no-new-array-as-prop
47+
- no-new-function-as-prop
48+
- no-jsx-as-prop
49+
- **Layer Performance**: Use proper memoization for layer rendering to prevent unnecessary re-renders.
50+
- **Variable Resolution**: Cache variable values to avoid repeated computations during rendering.
51+
52+
### Error Handling and Validation
53+
- Prioritize error handling and edge cases:
54+
- Use early returns for error conditions.
55+
- Implement guard clauses to handle preconditions and invalid states early.
56+
- Use custom error types for consistent error handling.
57+
- **Component Registry Validation**: Validate that all referenced component types exist in the registry.
58+
- **Layer Validation**: Ensure layer structure integrity and proper parent-child relationships.
59+
- **Variable Type Safety**: Use Zod schemas to validate variable types and values.
60+
61+
### UI and Styling
62+
- Use Tailwind CSS and Shadcn UI for styling.
63+
- Implement consistent design and responsive patterns across platforms.
64+
- **Editor UI Consistency**: Follow established patterns for editor panels, toolbars, and controls.
65+
- **Canvas Interaction**: Ensure proper visual feedback for drag-and-drop, selection, and hover states.
66+
67+
### State Management
68+
- Use modern state management solutions Zustand to handle global state.
69+
- Implement validation using Zod for schema validation.
70+
- Use Zustand for global state management.
71+
- Lift state up when needed to share state between components.
72+
- Use context for intermediate state sharing when prop drilling becomes cumbersome.
73+
- **Layer Store Management**: Use the established layer store patterns for managing UI Builder state.
74+
- **Variable Store Management**: Properly manage variable state with type safety and validation.
75+
- **Local Storage Persistence**: Respect persistLayerStore configuration for state persistence.
76+
77+
### Testing, Linting Type Checking, and Building Registry
78+
- Write unit tests for components using Jest and @testing-library/react with this format: {componentName}.test.tsx
79+
- After updating a component, run the `npm run test -- --coverage` to ensure the component is working as expected. Update the tests if needed. Aim for 90% coverage.
80+
- After updating a component, run the `npx eslint components/ui/ui-builder/ lib/ --max-warnings 0` to ensure the component is working as expected.
81+
- After updating a component, run the `npx tsc --noEmit` to ensure there are no type errors.
82+
- After updating a component, run the `npm run build-registry` to update the shadcn registry for our ui-builder component.
83+
- **UI Builder Specific Testing**: Test component registry definitions, layer manipulation, variable binding, and code generation functionality.
84+
- **Mock External Dependencies**: Use proper mocks for complex UI components like dropdown-menu and react-markdown.
85+
86+
### Registry and Package Management
87+
- **Shadcn Registry Structure**: Maintain proper registry structure with block-registry.json for distribution.
88+
- **Dependency Management**: All dependencies should be handled through shadcn CLI - avoid manual npm installs for registry components.
89+
- **Build Process**: Use the established build.ts script for registry generation and validation.
90+
- **Import Path Consistency**: Ensure all component imports use consistent paths that work both in the editor and generated code.
91+
92+
### Methodology
93+
1. **System 2 Thinking**: Approach the problem with analytical rigor. Break down the requirements into smaller, manageable parts and thoroughly consider each step before implementation.
94+
2. **Tree of Thoughts**: Evaluate multiple possible solutions and their consequences. Use a structured approach to explore different paths and select the optimal one.
95+
3. **Iterative Refinement**: Before finalizing the code, consider improvements, edge cases, and optimizations. Iterate through potential enhancements to ensure the final solution is robust.
96+
97+
**Process**:
98+
1. **Deep Dive Analysis**: Begin by conducting a thorough analysis of the task at hand, considering the technical requirements and constraints specific to the UI Builder architecture.
99+
2. **Planning**: Develop a clear plan that outlines the architectural structure and flow of the solution, considering layer relationships, variable bindings, and component registry implications.
100+
3. **Implementation**: Implement the solution step-by-step, ensuring that each part adheres to the UI Builder patterns and established best practices.
101+
4. **Review and Optimize**: Perform a review of the code, looking for areas of potential optimization, proper error handling, and UI Builder-specific performance considerations.
102+
5. **Finalization**: Finalize the code by ensuring it meets all requirements, integrates properly with the UI Builder ecosystem, and maintains type safety throughout.
103+
104+
105+
TOOLING NOTES:
106+
- do not run npm run dev yourself as server is always running on port 3000.
107+
- only in extremely difficult cases you are to use the playwright tool.
108+
- if facing outdated library knowledge use context7 tool.
109+

.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"ignorePatterns": ["__tests__/**", "auto-form/**"],
55
"rules": {
66
"@typescript-eslint/no-explicit-any": "off",
7-
"@typescript-eslint/no-unused-vars":"off",
7+
"@typescript-eslint/no-unused-vars":"warn",
88
"@typescript-eslint/no-empty-object-type": "off",
99

1010
// React Hooks performance rules (already included in next/core-web-vitals)

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ If you are not using shadcn/ui, you can install the component simply by copying
5757
Add dev dependencies, since there currently seems to be an issue with shadcn/ui not installing them from the registry:
5858

5959
```bash
60-
npm install -D @types/lodash.template @tailwindcss/typography @types/react-syntax-highlighter react-docgen-typescript tailwindcss-animate ts-morph ts-to-zod
60+
npm install -D @types/lodash.template @tailwindcss/typography @types/react-syntax-highlighter tailwindcss-animate @types/object-hash
6161
```
6262

6363
And that's it! You have a UI Builder that you can use to build your UI.
@@ -628,9 +628,8 @@ npm run test
628628

629629
## Roadmap
630630

631-
- [ ] Add variable binding to layer children and not just props
632-
- [ ] Drag and drop component in the editor panel and not just in the layers panel
633631
- [ ] Documentation site for UI Builder with more hands-on examples
632+
- [ ] Add variable binding to layer children and not just props
634633
- [ ] Update to React 19
635634
- [ ] Update to latest Shadcn/ui + Tailwind CSS v4
636635
- [ ] Add Blocks. Reusable component blocks that can be used in multiple pages

__tests__/add-component-popover.test.tsx

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3-
import { AddComponentsPopover } from '@/components/ui/ui-builder/internal/add-component-popover';
3+
import { AddComponentsPopover } from '@/components/ui/ui-builder/internal/components/add-component-popover';
44

55
// Mock scrollIntoView for jsdom environment
66
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
@@ -164,43 +164,6 @@ describe('AddComponentsPopover', () => {
164164
});
165165
});
166166

167-
it('should limit tabs to maximum of 3', async () => {
168-
// Mock registry with more than 3 groups
169-
const registryWithManyGroups = {
170-
...mockRegistry,
171-
ExtraComponent1: {
172-
component: () => null,
173-
name: 'ExtraComponent1',
174-
from: '@/group1/component'
175-
},
176-
ExtraComponent2: {
177-
component: () => null,
178-
name: 'ExtraComponent2',
179-
from: '@/group2/component'
180-
}
181-
};
182-
183-
mockUseEditorStore.mockImplementation((selector) => {
184-
const state = {
185-
registry: registryWithManyGroups,
186-
};
187-
return selector(state as any);
188-
});
189-
190-
render(
191-
<AddComponentsPopover parentLayerId="parent-1">
192-
<button>Add Component</button>
193-
</AddComponentsPopover>
194-
);
195-
196-
const trigger = screen.getByText('Add Component');
197-
fireEvent.click(trigger);
198-
199-
await waitFor(() => {
200-
const tabs = screen.getAllByRole('tab');
201-
expect(tabs.length).toBeLessThanOrEqual(3);
202-
});
203-
});
204167
});
205168

206169
describe('Component Selection', () => {

__tests__/breakpoint-classname-control.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import React from "react";
33
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
44
import userEvent from "@testing-library/user-event";
5-
import { BreakpointClassNameControl } from "@/components/ui/ui-builder/internal/classname-control/breakpoint-classname-control";
5+
import { BreakpointClassNameControl } from "@/components/ui/ui-builder/internal/form-fields/classname-control/breakpoint-classname-control";
66

77
// Mock the dependencies
88
jest.mock("@/components/ui/tooltip", () => ({
@@ -73,7 +73,7 @@ jest.mock("@/components/ui/accordion", () => ({
7373
),
7474
}));
7575

76-
jest.mock("@/components/ui/ui-builder/internal/classname-multiselect", () => {
76+
jest.mock("@/components/ui/ui-builder/internal/form-fields/classname-control/classname-multiselect", () => {
7777
return {
7878
__esModule: true,
7979
default: ({ value, onChange }: any) => (
@@ -90,7 +90,7 @@ jest.mock("@/components/ui/ui-builder/internal/classname-multiselect", () => {
9090
};
9191
});
9292

93-
jest.mock("@/components/ui/ui-builder/internal/classname-control/classname-item-control", () => ({
93+
jest.mock("@/components/ui/ui-builder/internal/form-fields/classname-control/classname-item-control", () => ({
9494
ClassNameItemControl: ({ value, onChange }: any) => (
9595
<div data-testid="classname-item-control">
9696
<input

__tests__/children-searchable-select.test.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from "react";
22
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
33
import userEvent from "@testing-library/user-event";
4-
import { ChildrenSearchableSelect } from "@/components/ui/ui-builder/internal/children-searchable-select";
4+
import { ChildrenSearchableSelect } from "@/components/ui/ui-builder/internal/form-fields/children-searchable-select";
55
import { ComponentLayer } from "@/components/ui/ui-builder/types";
66

77
// Mock the layer store
@@ -19,7 +19,7 @@ jest.mock("@/lib/ui-builder/store/layer-store", () => ({
1919
}));
2020

2121
// Mock the AddComponentsPopover
22-
jest.mock("@/components/ui/ui-builder/internal/add-component-popover", () => ({
22+
jest.mock("@/components/ui/ui-builder/internal/components/add-component-popover", () => ({
2323
AddComponentsPopover: ({ children, onChange, parentLayerId }: any) => (
2424
<div data-testid="add-components-popover" data-parent-layer-id={parentLayerId}>
2525
<button
@@ -93,7 +93,9 @@ describe("ChildrenSearchableSelect", () => {
9393
beforeEach(() => {
9494
jest.clearAllMocks();
9595
mockFindLayerById.mockReturnValue(mockSelectedLayer);
96-
mockHasLayerChildren.mockReturnValue(true);
96+
mockHasLayerChildren.mockImplementation((layer: ComponentLayer) => {
97+
return Array.isArray(layer.children) && typeof layer.children !== 'string';
98+
});
9799
});
98100

99101
it("renders add component button", () => {
@@ -127,7 +129,7 @@ describe("ChildrenSearchableSelect", () => {
127129
layerType: "Button",
128130
parentLayerId: "parent-layer",
129131
});
130-
});
132+
}, 10000);
131133

132134
it("renders child layer badges when layer has children", () => {
133135
render(
@@ -139,10 +141,11 @@ describe("ChildrenSearchableSelect", () => {
139141
});
140142

141143
it("does not render child badges when layer has no children", () => {
142-
mockHasLayerChildren.mockReturnValue(false);
144+
const layerWithStringChildren = { ...mockLayer, children: "text content" };
145+
mockFindLayerById.mockReturnValue({ ...mockSelectedLayer, children: "text content" });
143146

144147
render(
145-
<ChildrenSearchableSelect layer={mockLayer} onChange={mockOnChange} />
148+
<ChildrenSearchableSelect layer={layerWithStringChildren} onChange={mockOnChange} />
146149
);
147150

148151
expect(screen.queryByText("Selected Input")).not.toBeInTheDocument();
@@ -161,7 +164,6 @@ describe("ChildrenSearchableSelect", () => {
161164
it("does not render child badges when selected layer has no children", () => {
162165
const layerWithoutChildren = { ...mockSelectedLayer, children: [] };
163166
mockFindLayerById.mockReturnValue(layerWithoutChildren);
164-
mockHasLayerChildren.mockImplementation((layer) => layer.children.length > 0);
165167

166168
render(
167169
<ChildrenSearchableSelect layer={mockLayer} onChange={mockOnChange} />
@@ -278,7 +280,6 @@ describe("ChildrenSearchableSelect", () => {
278280
it("handles empty children array", () => {
279281
const emptySelectedLayer = { ...mockSelectedLayer, children: [] };
280282
mockFindLayerById.mockReturnValue(emptySelectedLayer);
281-
mockHasLayerChildren.mockReturnValue(false);
282283

283284
render(
284285
<ChildrenSearchableSelect layer={mockLayer} onChange={mockOnChange} />

0 commit comments

Comments
 (0)