diff --git a/.github/workflows/pull-request-tests.yml b/.github/workflows/pull-request-tests.yml index 9c18a79..4c1b944 100644 --- a/.github/workflows/pull-request-tests.yml +++ b/.github/workflows/pull-request-tests.yml @@ -24,5 +24,8 @@ jobs: - name: Lint code run: npx eslint components/ui/ui-builder/ lib/ --max-warnings 0 + - name: Type check + run: npx tsc --noEmit + - name: Run tests run: npm run test:ci \ No newline at end of file diff --git a/README.md b/README.md index c03b130..8ef67f0 100644 --- a/README.md +++ b/README.md @@ -629,7 +629,6 @@ npm run test ## Roadmap - [ ] Add variable binding to layer children and not just props -- [ ] Improve DX. End to end type safety. - [ ] Documentation site for UI Builder with more hands-on examples - [ ] Configurable Tailwind Class subset for things like React-Email components - [ ] Drag and drop component in the editor panel and not just in the layers panel diff --git a/__tests__/index.test.tsx b/__tests__/index.test.tsx index d47a662..d3da878 100644 --- a/__tests__/index.test.tsx +++ b/__tests__/index.test.tsx @@ -106,17 +106,17 @@ it("UIBuilder: accepts both initialLayers and initialVariables", async () => { expect(componentEditor).toBeInTheDocument(); }); -it("UIBuilder: hides Add Variable button when editVariables is false", async () => { +it("UIBuilder: hides Add Variable button when allowVariableEditing is false", async () => { render( ); const componentEditor = await screen.findByTestId("component-editor"); expect(componentEditor).toBeInTheDocument(); - // The editVariables prop should be passed through to the default config + // The allowVariableEditing prop should be passed through to the default config // We can verify this by checking that the prop was processed correctly expect(componentEditor).toBeInTheDocument(); }); @@ -130,7 +130,7 @@ it("UIBuilder: shows Add Variable button by default", async () => { const componentEditor = await screen.findByTestId("component-editor"); expect(componentEditor).toBeInTheDocument(); - // The editVariables prop should default to true + // The allowVariableEditing prop should default to true // We can verify this by checking that the component renders without errors expect(componentEditor).toBeInTheDocument(); }); diff --git a/components/ui/ui-builder/index.tsx b/components/ui/ui-builder/index.tsx index 5395328..81b7c33 100644 --- a/components/ui/ui-builder/index.tsx +++ b/components/ui/ui-builder/index.tsx @@ -24,7 +24,9 @@ import { useEditorStore } from "@/lib/ui-builder/store/editor-store"; import { ComponentRegistry, ComponentLayer, - Variable + Variable, + LayerChangeHandler, + VariableChangeHandler } from "@/components/ui/ui-builder/types"; import { TailwindThemePanel } from "@/components/ui/ui-builder/internal/tailwind-theme-panel"; import { ConfigPanel } from "@/components/ui/ui-builder/internal/config-panel"; @@ -52,14 +54,14 @@ interface PanelConfig { } /** - * UIBuilderProps defines the props for the UIBuilder component. + * UIBuilderProps defines the props for the UIBuilder component with enhanced type safety. */ -interface UIBuilderProps { +interface UIBuilderProps { initialLayers?: ComponentLayer[]; - onChange?: (pages: ComponentLayer[]) => void; + onChange?: LayerChangeHandler; initialVariables?: Variable[]; - onVariablesChange?: (variables: Variable[]) => void; - componentRegistry: ComponentRegistry; + onVariablesChange?: VariableChangeHandler; + componentRegistry: TRegistry; panelConfig?: PanelConfig; persistLayerStore?: boolean; allowVariableEditing?: boolean; @@ -73,7 +75,7 @@ interface UIBuilderProps { * @param {UIBuilderProps} props - The props for the UIBuilder component. * @returns {JSX.Element} The UIBuilder component wrapped in a ThemeProvider. */ -const UIBuilder = ({ +const UIBuilder = ({ initialLayers, onChange, initialVariables, @@ -84,7 +86,7 @@ const UIBuilder = ({ allowVariableEditing = true, allowPagesCreation = true, allowPagesDeletion = true, -}: UIBuilderProps) => { +}: UIBuilderProps) => { const layerStore = useStore(useLayerStore, (state) => state); const editorStore = useStore(useEditorStore, (state) => state); diff --git a/components/ui/ui-builder/internal/layer-menu.tsx b/components/ui/ui-builder/internal/layer-menu.tsx index 5c0aae3..846eda0 100644 --- a/components/ui/ui-builder/internal/layer-menu.tsx +++ b/components/ui/ui-builder/internal/layer-menu.tsx @@ -54,9 +54,14 @@ export const LayerMenu: React.FC = ({ const componentDef = componentRegistry[selectedLayer.type as keyof typeof componentRegistry]; if (!componentDef) return false; + // Safely check if schema has shape property (ZodObject) and children field + const hasChildrenField = 'shape' in componentDef.schema && + componentDef.schema.shape && + componentDef.schema.shape.children !== undefined; + return ( hasLayerChildren(selectedLayer) && - componentDef.schema.shape.children !== undefined + hasChildrenField ); }, [selectedLayer, componentRegistry]); diff --git a/components/ui/ui-builder/internal/props-panel.tsx b/components/ui/ui-builder/internal/props-panel.tsx index ff5e015..7db495f 100644 --- a/components/ui/ui-builder/internal/props-panel.tsx +++ b/components/ui/ui-builder/internal/props-panel.tsx @@ -182,7 +182,7 @@ const ComponentPropsAutoForm: React.FC = ({ Object.keys(dataProps as Record).forEach((key) => { const originalValue = selectedLayer.props[key]; const newValue = (dataProps as Record)[key]; - const fieldDef = schema?.shape?.[key]; + const fieldDef = ('shape' in schema && schema.shape) ? schema.shape[key] : undefined; const baseType = fieldDef ? getBaseType(fieldDef as z.ZodAny) : undefined; @@ -235,7 +235,7 @@ const ComponentPropsAutoForm: React.FC = ({ ); const transformedProps: Record = {}; - const schemaShape = schema?.shape as z.ZodRawShape | undefined; // Get shape from the memoized schema + const schemaShape = ('shape' in schema && schema.shape) ? schema.shape as z.ZodRawShape : undefined; // Get shape from the memoized schema if (schemaShape) { for (const [key, value] of Object.entries(resolvedProps)) { @@ -273,7 +273,16 @@ const ComponentPropsAutoForm: React.FC = ({ }, [selectedLayer, schema, revisionCounter]); // Include revisionCounter to detect undo/redo changes const autoFormSchema = useMemo(() => { - return addDefaultValues(schema, formValues); + // Only pass ZodObject schemas to addDefaultValues, otherwise return the original schema + if ('shape' in schema && typeof schema.shape === 'object') { + try { + return addDefaultValues(schema as any, formValues); + } catch (error) { + console.warn('Failed to add default values to schema:', error); + return schema; + } + } + return schema; }, [schema, formValues]); const autoFormFieldConfig = useMemo(() => { diff --git a/components/ui/ui-builder/internal/render-utils.tsx b/components/ui/ui-builder/internal/render-utils.tsx index 6fe8400..f71c513 100644 --- a/components/ui/ui-builder/internal/render-utils.tsx +++ b/components/ui/ui-builder/internal/render-utils.tsx @@ -9,12 +9,11 @@ import { ErrorFallback } from "@/components/ui/ui-builder/internal/error-fallbac import { isPrimitiveComponent } from "@/lib/ui-builder/store/editor-utils"; import { hasLayerChildren } from "@/lib/ui-builder/store/layer-utils"; import { DevProfiler } from "@/components/ui/ui-builder/internal/dev-profiler"; -import { ComponentRegistry, ComponentLayer, Variable } from '@/components/ui/ui-builder/types'; +import { ComponentRegistry, ComponentLayer, Variable, PropValue } from '@/components/ui/ui-builder/types'; import { useLayerStore } from "@/lib/ui-builder/store/layer-store"; import { resolveVariableReferences } from "@/lib/ui-builder/utils/variable-resolver"; export interface EditorConfig { - zIndex: number; totalLayers: number; selectedLayer: ComponentLayer; @@ -30,7 +29,7 @@ export const RenderLayer: React.FC<{ componentRegistry: ComponentRegistry; editorConfig?: EditorConfig; variables?: Variable[]; - variableValues?: Record; + variableValues?: Record; }> = memo( ({ layer, componentRegistry, editorConfig, variables, variableValues }) => { const storeVariables = useLayerStore((state) => state.variables); @@ -52,7 +51,7 @@ export const RenderLayer: React.FC<{ // Resolve variable references in props const resolvedProps = resolveVariableReferences(layer.props, effectiveVariables, variableValues); - const childProps: Record = useMemo(() => ({ ...resolvedProps }), [resolvedProps]); + const childProps: Record = useMemo(() => ({ ...resolvedProps }), [resolvedProps]); // Memoize child editor config to avoid creating objects in JSX const childEditorConfig = useMemo(() => { diff --git a/components/ui/ui-builder/internal/tailwind-theme-panel.tsx b/components/ui/ui-builder/internal/tailwind-theme-panel.tsx index 3211e59..647b920 100644 --- a/components/ui/ui-builder/internal/tailwind-theme-panel.tsx +++ b/components/ui/ui-builder/internal/tailwind-theme-panel.tsx @@ -74,27 +74,35 @@ function ThemePicker({ }) { const { updateLayer: updateLayerProps } = useLayerStore(); + // Safely extract values with type checking + const colorThemeValue = pageLayer.props?.["data-color-theme"]; + const modeValue = pageLayer.props?.["data-mode"]; + const borderRadiusValue = pageLayer.props?.borderRadius; + const [colorTheme, setColorTheme] = useState( - pageLayer.props?.["data-color-theme"] || "red" + (typeof colorThemeValue === 'string' ? colorThemeValue : "red") as BaseColor["name"] ); const [borderRadius, setBorderRadius] = useState( - pageLayer.props?.["data-border-radius"] || 0.3 + typeof borderRadiusValue === 'number' ? borderRadiusValue : 0.5 ); const [mode, setMode] = useState<"light" | "dark">( - pageLayer.props?.["data-mode"] || "light" + (typeof modeValue === 'string' ? modeValue : "light") as "light" | "dark" ); + + useEffect(() => { if (isDisabled) return; - const colorData = baseColors.find((color) => color.name === colorTheme); - if (colorData) { + const colorThemeData = baseColors.find((color) => color.name === colorTheme); + + if (colorThemeData) { const colorDataWithBorder = { - ...colorData, + ...colorThemeData, cssVars: { - ...colorData.cssVars, + ...colorThemeData.cssVars, [mode]: { - ...colorData.cssVars[mode], + ...colorThemeData.cssVars[mode], radius: `${borderRadius}rem`, }, }, diff --git a/components/ui/ui-builder/layer-renderer.tsx b/components/ui/ui-builder/layer-renderer.tsx index 87e0d70..408b2f8 100644 --- a/components/ui/ui-builder/layer-renderer.tsx +++ b/components/ui/ui-builder/layer-renderer.tsx @@ -3,27 +3,27 @@ import React from "react"; import { EditorConfig, RenderLayer } from "@/components/ui/ui-builder/internal/render-utils"; import { DevProfiler } from "@/components/ui/ui-builder/internal/dev-profiler"; -import { Variable, ComponentLayer, ComponentRegistry } from '@/components/ui/ui-builder/types'; +import { Variable, ComponentLayer, ComponentRegistry, PropValue } from '@/components/ui/ui-builder/types'; -interface LayerRendererProps { +interface LayerRendererProps { className?: string; page: ComponentLayer; editorConfig?: EditorConfig; - componentRegistry: ComponentRegistry; + componentRegistry: TRegistry; /** Optional variable definitions */ variables?: Variable[]; /** Optional variable values to override defaults */ - variableValues?: Record; + variableValues?: Record; } -const LayerRenderer: React.FC = ({ +const LayerRenderer = ({ className, page, editorConfig, componentRegistry, variables, variableValues, -}: LayerRendererProps) => { +}: LayerRendererProps): JSX.Element => { return ( diff --git a/components/ui/ui-builder/types.ts b/components/ui/ui-builder/types.ts index 5f866cb..084dc95 100644 --- a/components/ui/ui-builder/types.ts +++ b/components/ui/ui-builder/types.ts @@ -1,52 +1,132 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { ZodObject } from "zod"; -import { ComponentType as ReactComponentType } from 'react'; +import { ZodObject, ZodSchema, ZodType } from "zod"; +import { ComponentType as ReactComponentType, ReactNode } from 'react'; import { FieldConfigItem, } from "@/components/ui/auto-form/types"; - export type { AutoFormInputComponentProps, FieldConfigItem, } from "@/components/ui/auto-form/types"; -export type ComponentLayer = { +// Enhanced prop value types that can accommodate React props, variables, and common data types +export type PropValue = + | ReactNode + | VariableReference + | Record + | any[] + | string + | number + | boolean + | null + | undefined; + +// Generic component props that allow for flexible but safer typing +export type ComponentProps = Record> = TProps; + +// Enhanced ComponentLayer with generic prop typing +export interface ComponentLayer = Record> { id: string; name?: string; type: string; - props: Record; + props: ComponentProps; children: ComponentLayer[] | string; -}; +} -export interface Variable { +// Variable value types - more specific than before +export type VariableValueType = 'string' | 'number' | 'boolean'; + +// Type-safe variable values based on their type +export type VariableValue = + T extends 'string' ? string : + T extends 'number' ? number : + T extends 'boolean' ? boolean : + never; + +// Enhanced Variable interface with generic typing +export interface Variable { id: string; name: string; - type: 'string' | 'number' | 'boolean'; - defaultValue: any; + type: T; + defaultValue: VariableValue; +} + +// Variable reference marker for props +export interface VariableReference { + __variableRef: string; } +// Default variable binding configuration export interface DefaultVariableBinding { propName: string; variableId: string; immutable?: boolean; } +// Enhanced registry entry with better component typing export interface RegistryEntry> { component?: T; - schema: ZodObject; + schema: ZodObject | ZodSchema; from?: string; isFromDefaultExport?: boolean; - defaultChildren?: (ComponentLayer)[] | string; + defaultChildren?: ComponentLayer[] | string; defaultVariableBindings?: DefaultVariableBinding[]; fieldOverrides?: Record; } -export type FieldConfigFunction = (layer: ComponentLayer, allowVariableBinding?: boolean ) => FieldConfigItem; +// Improved field config function type +export type FieldConfigFunction = (layer: ComponentLayer, allowVariableBinding?: boolean) => FieldConfigItem; + +// Enhanced ComponentRegistry with better typing +export type ComponentRegistry = Record>>; + +// Type-safe layer change handler +export type LayerChangeHandler = + (layers: ComponentLayer[]) => void; + +// Type-safe variable change handler +export type VariableChangeHandler = (variables: Variable[]) => void; + +// Helper types for extracting component props from registry +export type ExtractComponentProps< + TRegistry extends ComponentRegistry, + TComponentName extends keyof TRegistry +> = TRegistry[TComponentName] extends RegistryEntry> + ? TProps + : never; + +// Type-safe layer change handler with registry awareness +export type TypedLayerChangeHandler = + (layers: Array) => void; + +// Utility function types for creating variables +export type CreateVariable = ( + id: string, + name: string, + type: T, + defaultValue: VariableValue +) => Variable; + +// Utility to check if a value is a variable reference +export function isVariableReference(value: any): value is VariableReference { + return typeof value === 'object' && value !== null && '__variableRef' in value; +} -export type ComponentRegistry = Record< - string, RegistryEntry> ->; +// Type-safe variable creation helper +export const createVariable: CreateVariable = ( + id: string, + name: string, + type: T, + defaultValue: VariableValue +): Variable => ({ + id, + name, + type, + defaultValue, +}); diff --git a/lib/ui-builder/registry/form-field-overrides.tsx b/lib/ui-builder/registry/form-field-overrides.tsx index 9335380..728a64e 100644 --- a/lib/ui-builder/registry/form-field-overrides.tsx +++ b/lib/ui-builder/registry/form-field-overrides.tsx @@ -161,7 +161,6 @@ export const childrenAsTipTapFieldOverrides: FieldConfigFunction = ( editorClassName="focus:outline-none px-4 py-2 h-full" // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop onChange={(content) => { - console.log({ content }); //if string call field.onChange if (typeof content === "string") { field.onChange(content); diff --git a/lib/ui-builder/store/layer-store.ts b/lib/ui-builder/store/layer-store.ts index 458bce9..8cf7162 100644 --- a/lib/ui-builder/store/layer-store.ts +++ b/lib/ui-builder/store/layer-store.ts @@ -8,7 +8,7 @@ import isDeepEqual from 'fast-deep-equal'; import { visitLayer, addLayer, hasLayerChildren, findLayerRecursive, createId, countLayers, duplicateWithNewIdsAndName, findAllParentLayersRecursive, migrateV1ToV2, migrateV2ToV3 } from '@/lib/ui-builder/store/layer-utils'; import { getDefaultProps } from '@/lib/ui-builder/store/schema-utils'; import { useEditorStore } from '@/lib/ui-builder/store/editor-store'; -import { ComponentLayer, Variable } from '@/components/ui/ui-builder/types'; +import { ComponentLayer, Variable, PropValue, VariableValueType, isVariableReference } from '@/components/ui/ui-builder/types'; const DEFAULT_PAGE_PROPS = { className: "p-4 flex flex-col gap-2", @@ -25,14 +25,14 @@ export interface LayerStore { addPageLayer: (pageId: string) => void; duplicateLayer: (layerId: string, parentId?: string) => void; removeLayer: (layerId: string) => void; - updateLayer: (layerId: string, newProps: Record, layerRest?: Partial>) => void; + updateLayer: (layerId: string, newProps: Record, layerRest?: Partial>) => void; selectLayer: (layerId: string) => void; selectPage: (pageId: string) => void; findLayerById: (layerId: string | null) => ComponentLayer | undefined; findLayersForPageId: (pageId: string) => ComponentLayer[]; isLayerAPage: (layerId: string) => boolean; - addVariable: (name: string, type: Variable['type'], defaultValue: any) => void; + addVariable: (name: string, type: T, defaultValue: Variable['defaultValue']) => void; updateVariable: (variableId: string, updates: Partial>) => void; removeVariable: (variableId: string) => void; bindPropToVariable: (layerId: string, propName: string, variableId: string) => void; @@ -89,7 +89,10 @@ const store: StateCreator = (set, get) => ( addComponentLayer: (layerType: string, parentId: string, parentPosition?: number) => set(produce((state: LayerStore) => { const { registry } = useEditorStore.getState(); - const defaultProps = getDefaultProps(registry[layerType].schema); + const schema = registry[layerType].schema; + + // Safely check if schema has shape property (ZodObject) + const defaultProps = 'shape' in schema && schema.shape ? getDefaultProps(schema as any) : {}; const defaultChildrenRaw = registry[layerType].defaultChildren; const defaultChildren = typeof defaultChildrenRaw === "string" ? defaultChildrenRaw : (defaultChildrenRaw?.map(child => duplicateWithNewIdsAndName(child, false)) || []); const defaultVariableBindings = registry[layerType].defaultVariableBindings || []; @@ -339,12 +342,12 @@ const store: StateCreator = (set, get) => ( // Check each prop for variable references Object.entries(updatedProps).forEach(([propName, propValue]) => { - if (propValue && typeof propValue === 'object' && propValue.__variableRef === variableId) { + if (isVariableReference(propValue) && propValue.__variableRef === variableId) { // This prop references the variable being removed // Get the default value from the schema const layerSchema = registry[layer.type]?.schema; - if (layerSchema && layerSchema.shape && layerSchema.shape[propName]) { - const defaultProps = getDefaultProps(layerSchema); + if (layerSchema && 'shape' in layerSchema && layerSchema.shape && layerSchema.shape[propName]) { + const defaultProps = getDefaultProps(layerSchema as any); updatedProps[propName] = defaultProps[propName]; hasChanges = true; } else { @@ -390,8 +393,8 @@ const store: StateCreator = (set, get) => ( const layerSchema = registry[layer.type]?.schema; let defaultValue: any = undefined; - if (layerSchema && layerSchema.shape && layerSchema.shape[propName]) { - const defaultProps = getDefaultProps(layerSchema); + if (layerSchema && 'shape' in layerSchema && layerSchema.shape && layerSchema.shape[propName]) { + const defaultProps = getDefaultProps(layerSchema as any); defaultValue = defaultProps[propName]; } diff --git a/lib/ui-builder/utils/variable-resolver.ts b/lib/ui-builder/utils/variable-resolver.ts index 247d744..4bf27e2 100644 --- a/lib/ui-builder/utils/variable-resolver.ts +++ b/lib/ui-builder/utils/variable-resolver.ts @@ -1,11 +1,5 @@ -import { Variable } from '@/components/ui/ui-builder/types'; - -/** - * Checks if a value is a variable reference - */ -export function isVariableReference(value: any): value is { __variableRef: string } { - return typeof value === 'object' && value !== null && '__variableRef' in value; -} +import React from 'react'; +import { Variable, VariableReference, PropValue, isVariableReference } from '@/components/ui/ui-builder/types'; /** * Resolves variable references in props using provided variable values @@ -15,11 +9,11 @@ export function isVariableReference(value: any): value is { __variableRef: strin * @returns Props with variable references resolved */ export function resolveVariableReferences( - props: Record, + props: Record, variables: Variable[], - variableValues?: Record -): Record { - const resolved: Record = {}; + variableValues?: Record +): Record { + const resolved: Record = {}; for (const [key, value] of Object.entries(props)) { if (isVariableReference(value)) { @@ -31,9 +25,9 @@ export function resolveVariableReferences( // Variable not found, use default value or undefined resolved[key] = undefined; } - } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) { - // Recursively resolve nested objects - resolved[key] = resolveVariableReferences(value, variables, variableValues); + } else if (typeof value === 'object' && value !== null && !Array.isArray(value) && !React.isValidElement(value)) { + // Recursively resolve nested objects (but not React elements or arrays) + resolved[key] = resolveVariableReferences(value as Record, variables, variableValues); } else { // Regular value, keep as is resolved[key] = value; @@ -42,3 +36,6 @@ export function resolveVariableReferences( return resolved; } + +// Export the isVariableReference function for backward compatibility +export { isVariableReference }; diff --git a/registry/block-registry.json b/registry/block-registry.json index df65eb8..0716f0d 100644 --- a/registry/block-registry.json +++ b/registry/block-registry.json @@ -70,7 +70,7 @@ "files": [ { "path": "components/ui/ui-builder/types.ts", - "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { ZodObject } from \"zod\";\nimport { ComponentType as ReactComponentType } from 'react';\nimport {\n FieldConfigItem,\n } from \"@/components/ui/auto-form/types\";\n\n\nexport type {\n AutoFormInputComponentProps,\n FieldConfigItem,\n } from \"@/components/ui/auto-form/types\";\n\nexport type ComponentLayer = {\n id: string;\n name?: string;\n type: string;\n props: Record;\n children: ComponentLayer[] | string;\n};\n\nexport interface Variable {\n id: string;\n name: string;\n type: 'string' | 'number' | 'boolean';\n defaultValue: any;\n}\n\nexport interface DefaultVariableBinding {\n propName: string;\n variableId: string;\n immutable?: boolean;\n}\n\nexport interface RegistryEntry> {\n component?: T;\n schema: ZodObject;\n from?: string;\n isFromDefaultExport?: boolean;\n defaultChildren?: (ComponentLayer)[] | string;\n defaultVariableBindings?: DefaultVariableBinding[];\n fieldOverrides?: Record;\n}\n\nexport type FieldConfigFunction = (layer: ComponentLayer, allowVariableBinding?: boolean ) => FieldConfigItem;\n\nexport type ComponentRegistry = Record<\n string, RegistryEntry>\n>;\n\n\n\n\n\n\n", + "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { ZodObject, ZodSchema, ZodType } from \"zod\";\nimport { ComponentType as ReactComponentType, ReactNode } from 'react';\nimport {\n FieldConfigItem,\n } from \"@/components/ui/auto-form/types\";\n\nexport type {\n AutoFormInputComponentProps,\n FieldConfigItem,\n } from \"@/components/ui/auto-form/types\";\n\n// Enhanced prop value types that can accommodate React props, variables, and common data types\nexport type PropValue = \n | ReactNode \n | VariableReference \n | Record \n | any[] \n | string \n | number \n | boolean \n | null \n | undefined;\n\n// Generic component props that allow for flexible but safer typing\nexport type ComponentProps = Record> = TProps;\n\n// Enhanced ComponentLayer with generic prop typing\nexport interface ComponentLayer = Record> {\n id: string;\n name?: string;\n type: string;\n props: ComponentProps;\n children: ComponentLayer[] | string;\n}\n\n// Variable value types - more specific than before\nexport type VariableValueType = 'string' | 'number' | 'boolean';\n\n// Type-safe variable values based on their type\nexport type VariableValue = \n T extends 'string' ? string :\n T extends 'number' ? number :\n T extends 'boolean' ? boolean :\n never;\n\n// Enhanced Variable interface with generic typing\nexport interface Variable {\n id: string;\n name: string;\n type: T;\n defaultValue: VariableValue;\n}\n\n// Variable reference marker for props\nexport interface VariableReference {\n __variableRef: string;\n}\n\n// Default variable binding configuration\nexport interface DefaultVariableBinding {\n propName: string;\n variableId: string;\n immutable?: boolean;\n}\n\n// Enhanced registry entry with better component typing\nexport interface RegistryEntry> {\n component?: T;\n schema: ZodObject | ZodSchema;\n from?: string;\n isFromDefaultExport?: boolean;\n defaultChildren?: ComponentLayer[] | string;\n defaultVariableBindings?: DefaultVariableBinding[];\n fieldOverrides?: Record;\n}\n\n// Improved field config function type\nexport type FieldConfigFunction = (layer: ComponentLayer, allowVariableBinding?: boolean) => FieldConfigItem;\n\n// Enhanced ComponentRegistry with better typing\nexport type ComponentRegistry = Record>>;\n\n// Type-safe layer change handler\nexport type LayerChangeHandler = \n (layers: ComponentLayer[]) => void;\n\n// Type-safe variable change handler \nexport type VariableChangeHandler = (variables: Variable[]) => void;\n\n// Helper types for extracting component props from registry\nexport type ExtractComponentProps<\n TRegistry extends ComponentRegistry,\n TComponentName extends keyof TRegistry\n> = TRegistry[TComponentName] extends RegistryEntry>\n ? TProps\n : never;\n\n// Type-safe layer change handler with registry awareness\nexport type TypedLayerChangeHandler = \n (layers: Array) => void;\n\n// Utility function types for creating variables\nexport type CreateVariable = (\n id: string,\n name: string,\n type: T,\n defaultValue: VariableValue\n) => Variable;\n\n// Utility to check if a value is a variable reference\nexport function isVariableReference(value: any): value is VariableReference {\n return typeof value === 'object' && value !== null && '__variableRef' in value;\n}\n\n// Type-safe variable creation helper\nexport const createVariable: CreateVariable = (\n id: string,\n name: string,\n type: T,\n defaultValue: VariableValue\n): Variable => ({\n id,\n name,\n type,\n defaultValue,\n});\n\n\n\n\n\n\n", "type": "registry:ui", "target": "components/ui/ui-builder/types.ts" }, @@ -88,13 +88,13 @@ }, { "path": "components/ui/ui-builder/layer-renderer.tsx", - "content": "import React from \"react\";\n\nimport { EditorConfig, RenderLayer } from \"@/components/ui/ui-builder/internal/render-utils\";\nimport { DevProfiler } from \"@/components/ui/ui-builder/internal/dev-profiler\";\n\nimport { Variable, ComponentLayer, ComponentRegistry } from '@/components/ui/ui-builder/types';\n\ninterface LayerRendererProps {\n className?: string;\n page: ComponentLayer;\n editorConfig?: EditorConfig;\n componentRegistry: ComponentRegistry;\n /** Optional variable definitions */\n variables?: Variable[];\n /** Optional variable values to override defaults */\n variableValues?: Record;\n}\n\nconst LayerRenderer: React.FC = ({\n className,\n page,\n editorConfig,\n componentRegistry,\n variables,\n variableValues,\n}: LayerRendererProps) => {\n\n return (\n \n \n \n \n \n );\n};\n\nexport default LayerRenderer;\n\n", + "content": "import React from \"react\";\n\nimport { EditorConfig, RenderLayer } from \"@/components/ui/ui-builder/internal/render-utils\";\nimport { DevProfiler } from \"@/components/ui/ui-builder/internal/dev-profiler\";\n\nimport { Variable, ComponentLayer, ComponentRegistry, PropValue } from '@/components/ui/ui-builder/types';\n\ninterface LayerRendererProps {\n className?: string;\n page: ComponentLayer;\n editorConfig?: EditorConfig;\n componentRegistry: TRegistry;\n /** Optional variable definitions */\n variables?: Variable[];\n /** Optional variable values to override defaults */\n variableValues?: Record;\n}\n\nconst LayerRenderer = ({\n className,\n page,\n editorConfig,\n componentRegistry,\n variables,\n variableValues,\n}: LayerRendererProps): JSX.Element => {\n\n return (\n \n \n \n \n \n );\n};\n\nexport default LayerRenderer;\n\n", "type": "registry:ui", "target": "components/ui/ui-builder/layer-renderer.tsx" }, { "path": "components/ui/ui-builder/index.tsx", - "content": "/**\n * Learn more about the UI Builder:\n * https://github.com/olliethedev/ui-builder\n */\n\n\"use client\";\n\nimport React, { useEffect, useState, useMemo, useCallback } from \"react\";\nimport LayersPanel from \"@/components/ui/ui-builder/internal/layers-panel\";\nimport EditorPanel from \"@/components/ui/ui-builder/internal/editor-panel\";\nimport PropsPanel from \"@/components/ui/ui-builder/internal/props-panel\";\nimport { NavBar } from \"@/components/ui/ui-builder/internal/nav\";\nimport { ThemeProvider } from \"next-themes\";\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from \"@/components/ui/tabs\";\nimport {\n ResizablePanel,\n ResizableHandle,\n ResizablePanelGroup,\n} from \"@/components/ui/resizable\";\nimport { Button } from \"@/components/ui/button\";\nimport { useLayerStore } from \"@/lib/ui-builder/store/layer-store\";\nimport { useStore } from \"@/hooks/use-store\";\nimport { useEditorStore } from \"@/lib/ui-builder/store/editor-store\";\nimport {\n ComponentRegistry,\n ComponentLayer,\n Variable\n} from \"@/components/ui/ui-builder/types\";\nimport { TailwindThemePanel } from \"@/components/ui/ui-builder/internal/tailwind-theme-panel\";\nimport { ConfigPanel } from \"@/components/ui/ui-builder/internal/config-panel\";\nimport { VariablesPanel } from \"@/components/ui/ui-builder/internal/variables-panel\";\nimport { TooltipProvider } from \"@/components/ui/tooltip\";\n\n/**\n * TabsContentConfig defines the structure for the content of the page config panel tabs.\n */\nexport interface TabsContentConfig {\n layers: { title: string; content: React.ReactNode };\n appearance?: { title: string; content: React.ReactNode };\n data?: { title: string; content: React.ReactNode };\n}\n\n/**\n * PanelConfig defines the configuration for the main panels in the UI Builder.\n */\ninterface PanelConfig {\n navBar?: React.ReactNode;\n pageConfigPanel?: React.ReactNode;\n pageConfigPanelTabsContent?: TabsContentConfig;\n editorPanel?: React.ReactNode;\n propsPanel?: React.ReactNode;\n}\n\n/**\n * UIBuilderProps defines the props for the UIBuilder component.\n */\ninterface UIBuilderProps {\n initialLayers?: ComponentLayer[];\n onChange?: (pages: ComponentLayer[]) => void;\n initialVariables?: Variable[];\n onVariablesChange?: (variables: Variable[]) => void;\n componentRegistry: ComponentRegistry;\n panelConfig?: PanelConfig;\n persistLayerStore?: boolean;\n allowVariableEditing?: boolean;\n allowPagesCreation?: boolean;\n allowPagesDeletion?: boolean;\n}\n\n/**\n * UIBuilder component manages the initialization of editor and layer stores, and renders the serializable layout.\n *\n * @param {UIBuilderProps} props - The props for the UIBuilder component.\n * @returns {JSX.Element} The UIBuilder component wrapped in a ThemeProvider.\n */\nconst UIBuilder = ({\n initialLayers,\n onChange,\n initialVariables,\n onVariablesChange,\n componentRegistry,\n panelConfig: userPanelConfig,\n persistLayerStore = true,\n allowVariableEditing = true,\n allowPagesCreation = true,\n allowPagesDeletion = true,\n}: UIBuilderProps) => {\n const layerStore = useStore(useLayerStore, (state) => state);\n const editorStore = useStore(useEditorStore, (state) => state);\n\n const [editorStoreInitialized, setEditorStoreInitialized] = useState(false);\n const [layerStoreInitialized, setLayerStoreInitialized] = useState(false);\n\n const memoizedDefaultTabsContent = useMemo(() => defaultConfigTabsContent(), []);\n\n const currentPanelConfig = useMemo(() => {\n const effectiveTabsContent = userPanelConfig?.pageConfigPanelTabsContent || memoizedDefaultTabsContent;\n const defaultPanels = getDefaultPanelConfigValues(true, effectiveTabsContent);\n\n return {\n navBar: userPanelConfig?.navBar ?? defaultPanels.navBar,\n pageConfigPanel: userPanelConfig?.pageConfigPanel ?? defaultPanels.pageConfigPanel,\n editorPanel: userPanelConfig?.editorPanel ?? defaultPanels.editorPanel,\n propsPanel: userPanelConfig?.propsPanel ?? defaultPanels.propsPanel,\n };\n }, [userPanelConfig, memoizedDefaultTabsContent]);\n\n // Effect 1: Initialize Editor Store with registry and page form props\n useEffect(() => {\n if (editorStore && componentRegistry && !editorStoreInitialized) {\n editorStore.initialize(componentRegistry, persistLayerStore, allowPagesCreation, allowPagesDeletion, allowVariableEditing);\n setEditorStoreInitialized(true);\n }\n }, [\n editorStore,\n componentRegistry,\n editorStoreInitialized,\n persistLayerStore,\n allowPagesCreation,\n allowPagesDeletion,\n allowVariableEditing,\n ]);\n\n // Effect 2: Conditionally initialize Layer Store *after* Editor Store is initialized\n useEffect(() => {\n if (layerStore && editorStore) {\n if (initialLayers && !layerStoreInitialized) {\n layerStore.initialize(initialLayers, undefined, undefined, initialVariables);\n setLayerStoreInitialized(true);\n const { clear } = useLayerStore.temporal.getState();\n clear();\n } else {\n setLayerStoreInitialized(true);\n }\n }\n }, [\n layerStore,\n editorStore,\n componentRegistry,\n initialLayers,\n initialVariables,\n layerStoreInitialized,\n ]);\n\n // Effect 3: Handle onChange callback when pages change\n useEffect(() => {\n if (onChange && layerStore?.pages && layerStoreInitialized) {\n onChange(layerStore.pages);\n }\n }, [layerStore?.pages, onChange, layerStoreInitialized]);\n\n // Effect 4: Handle onVariablesChange callback when variables change\n useEffect(() => {\n if (onVariablesChange && layerStore?.variables && layerStoreInitialized) {\n onVariablesChange(layerStore.variables);\n }\n }, [layerStore?.variables, onVariablesChange, layerStoreInitialized]);\n\n const isLoading = !layerStoreInitialized || !editorStoreInitialized;\n const layout = isLoading ? (\n \n ) : (\n \n );\n\n return (\n \n \n {layout}\n \n \n );\n};\n\nfunction MainLayout({ panelConfig }: { panelConfig: PanelConfig }) {\n const mainPanels = useMemo(\n () => [\n {\n title: \"Page Config\",\n content: panelConfig.pageConfigPanel,\n defaultSize: 25,\n },\n {\n title: \"UI Editor\",\n content: panelConfig.editorPanel,\n defaultSize: 50,\n },\n {\n title: \"Props\",\n content: panelConfig.propsPanel,\n defaultSize: 25,\n },\n ],\n [panelConfig]\n );\n\n const [selectedPanel, setSelectedPanel] = useState(mainPanels[1]);\n\n const handlePanelClickById = useCallback((e: React.MouseEvent) => {\n const panelIndex = parseInt(e.currentTarget.dataset.panelIndex || \"0\");\n setSelectedPanel(mainPanels[panelIndex]);\n }, [mainPanels]);\n\n return (\n \n {panelConfig.navBar}\n {/* Desktop Layout */}\n \n \n {mainPanels.map((panel, index) => (\n \n {index > 0 && }\n \n {panel.content}\n \n \n ))}\n \n \n {/* Mobile Layout */}\n \n {selectedPanel.content}\n \n \n {mainPanels.map((panel, index) => (\n \n {panel.title}\n \n ))}\n \n \n \n \n );\n}\n\n/**\n * PageConfigPanel renders a tabbed panel for page configuration, including layers, appearance, and variables tabs.\n *\n * @param {object} props\n * @param {string} props.className - The class name for the panel container.\n * @param {TabsContentConfig} props.tabsContent - The content for the tabs.\n * @returns {JSX.Element} The page config panel with tabs.\n */\nexport function PageConfigPanel({\n className,\n tabsContent,\n}: {\n className: string;\n tabsContent: TabsContentConfig;\n}) {\n const { layers, appearance, data } = tabsContent;\n const tabCount = 1 + (appearance ? 1 : 0) + (data ? 1 : 0);\n \n return (\n \n \n {layers.title}\n {appearance && {appearance.title}}\n {data && {data.title}}\n \n \n {layers.content}\n \n {appearance && (\n \n {appearance.content}\n \n )}\n {data && (\n \n {data.content}\n \n )}\n \n );\n}\n\n/**\n * Returns the default tab content configuration for the page config panel.\n *\n * @param {boolean} editVariables - Whether to allow editing variables.\n * @returns {TabsContentConfig} The default tabs content configuration.\n */\nexport function defaultConfigTabsContent() {\n return {\n layers: { title: \"Layers\", content: },\n appearance: { title: \"Appearance\", content: (\n \n \n \n \n ),\n },\n data: { title: \"Data\", content: }\n }\n}\n\n/**\n * LoadingSkeleton renders a skeleton UI while the builder is initializing.\n *\n * @returns {JSX.Element} The loading skeleton.\n */\nexport function LoadingSkeleton() {\n return (\n \n \n \n \n \n \n \n \n );\n}\n\n/**\n * Returns the default panel configuration values for the UI Builder.\n *\n * @param {boolean} useCanvas - Whether to use the canvas editor.\n * @param {TabsContentConfig} tabsContent - The content for the page config panel tabs.\n * @returns {PanelConfig} The default panel configuration.\n */\nexport const getDefaultPanelConfigValues = (useCanvas: boolean, tabsContent: TabsContentConfig) => {\n return {\n navBar: ,\n pageConfigPanel: (\n \n ),\n editorPanel: (\n \n ),\n propsPanel: (\n \n ),\n };\n};\n\nexport default UIBuilder;\n", + "content": "/**\n * Learn more about the UI Builder:\n * https://github.com/olliethedev/ui-builder\n */\n\n\"use client\";\n\nimport React, { useEffect, useState, useMemo, useCallback } from \"react\";\nimport LayersPanel from \"@/components/ui/ui-builder/internal/layers-panel\";\nimport EditorPanel from \"@/components/ui/ui-builder/internal/editor-panel\";\nimport PropsPanel from \"@/components/ui/ui-builder/internal/props-panel\";\nimport { NavBar } from \"@/components/ui/ui-builder/internal/nav\";\nimport { ThemeProvider } from \"next-themes\";\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from \"@/components/ui/tabs\";\nimport {\n ResizablePanel,\n ResizableHandle,\n ResizablePanelGroup,\n} from \"@/components/ui/resizable\";\nimport { Button } from \"@/components/ui/button\";\nimport { useLayerStore } from \"@/lib/ui-builder/store/layer-store\";\nimport { useStore } from \"@/hooks/use-store\";\nimport { useEditorStore } from \"@/lib/ui-builder/store/editor-store\";\nimport {\n ComponentRegistry,\n ComponentLayer,\n Variable,\n LayerChangeHandler,\n VariableChangeHandler\n} from \"@/components/ui/ui-builder/types\";\nimport { TailwindThemePanel } from \"@/components/ui/ui-builder/internal/tailwind-theme-panel\";\nimport { ConfigPanel } from \"@/components/ui/ui-builder/internal/config-panel\";\nimport { VariablesPanel } from \"@/components/ui/ui-builder/internal/variables-panel\";\nimport { TooltipProvider } from \"@/components/ui/tooltip\";\n\n/**\n * TabsContentConfig defines the structure for the content of the page config panel tabs.\n */\nexport interface TabsContentConfig {\n layers: { title: string; content: React.ReactNode };\n appearance?: { title: string; content: React.ReactNode };\n data?: { title: string; content: React.ReactNode };\n}\n\n/**\n * PanelConfig defines the configuration for the main panels in the UI Builder.\n */\ninterface PanelConfig {\n navBar?: React.ReactNode;\n pageConfigPanel?: React.ReactNode;\n pageConfigPanelTabsContent?: TabsContentConfig;\n editorPanel?: React.ReactNode;\n propsPanel?: React.ReactNode;\n}\n\n/**\n * UIBuilderProps defines the props for the UIBuilder component with enhanced type safety.\n */\ninterface UIBuilderProps {\n initialLayers?: ComponentLayer[];\n onChange?: LayerChangeHandler;\n initialVariables?: Variable[];\n onVariablesChange?: VariableChangeHandler;\n componentRegistry: TRegistry;\n panelConfig?: PanelConfig;\n persistLayerStore?: boolean;\n allowVariableEditing?: boolean;\n allowPagesCreation?: boolean;\n allowPagesDeletion?: boolean;\n}\n\n/**\n * UIBuilder component manages the initialization of editor and layer stores, and renders the serializable layout.\n *\n * @param {UIBuilderProps} props - The props for the UIBuilder component.\n * @returns {JSX.Element} The UIBuilder component wrapped in a ThemeProvider.\n */\nconst UIBuilder = ({\n initialLayers,\n onChange,\n initialVariables,\n onVariablesChange,\n componentRegistry,\n panelConfig: userPanelConfig,\n persistLayerStore = true,\n allowVariableEditing = true,\n allowPagesCreation = true,\n allowPagesDeletion = true,\n}: UIBuilderProps) => {\n const layerStore = useStore(useLayerStore, (state) => state);\n const editorStore = useStore(useEditorStore, (state) => state);\n\n const [editorStoreInitialized, setEditorStoreInitialized] = useState(false);\n const [layerStoreInitialized, setLayerStoreInitialized] = useState(false);\n\n const memoizedDefaultTabsContent = useMemo(() => defaultConfigTabsContent(), []);\n\n const currentPanelConfig = useMemo(() => {\n const effectiveTabsContent = userPanelConfig?.pageConfigPanelTabsContent || memoizedDefaultTabsContent;\n const defaultPanels = getDefaultPanelConfigValues(true, effectiveTabsContent);\n\n return {\n navBar: userPanelConfig?.navBar ?? defaultPanels.navBar,\n pageConfigPanel: userPanelConfig?.pageConfigPanel ?? defaultPanels.pageConfigPanel,\n editorPanel: userPanelConfig?.editorPanel ?? defaultPanels.editorPanel,\n propsPanel: userPanelConfig?.propsPanel ?? defaultPanels.propsPanel,\n };\n }, [userPanelConfig, memoizedDefaultTabsContent]);\n\n // Effect 1: Initialize Editor Store with registry and page form props\n useEffect(() => {\n if (editorStore && componentRegistry && !editorStoreInitialized) {\n editorStore.initialize(componentRegistry, persistLayerStore, allowPagesCreation, allowPagesDeletion, allowVariableEditing);\n setEditorStoreInitialized(true);\n }\n }, [\n editorStore,\n componentRegistry,\n editorStoreInitialized,\n persistLayerStore,\n allowPagesCreation,\n allowPagesDeletion,\n allowVariableEditing,\n ]);\n\n // Effect 2: Conditionally initialize Layer Store *after* Editor Store is initialized\n useEffect(() => {\n if (layerStore && editorStore) {\n if (initialLayers && !layerStoreInitialized) {\n layerStore.initialize(initialLayers, undefined, undefined, initialVariables);\n setLayerStoreInitialized(true);\n const { clear } = useLayerStore.temporal.getState();\n clear();\n } else {\n setLayerStoreInitialized(true);\n }\n }\n }, [\n layerStore,\n editorStore,\n componentRegistry,\n initialLayers,\n initialVariables,\n layerStoreInitialized,\n ]);\n\n // Effect 3: Handle onChange callback when pages change\n useEffect(() => {\n if (onChange && layerStore?.pages && layerStoreInitialized) {\n onChange(layerStore.pages);\n }\n }, [layerStore?.pages, onChange, layerStoreInitialized]);\n\n // Effect 4: Handle onVariablesChange callback when variables change\n useEffect(() => {\n if (onVariablesChange && layerStore?.variables && layerStoreInitialized) {\n onVariablesChange(layerStore.variables);\n }\n }, [layerStore?.variables, onVariablesChange, layerStoreInitialized]);\n\n const isLoading = !layerStoreInitialized || !editorStoreInitialized;\n const layout = isLoading ? (\n \n ) : (\n \n );\n\n return (\n \n \n {layout}\n \n \n );\n};\n\nfunction MainLayout({ panelConfig }: { panelConfig: PanelConfig }) {\n const mainPanels = useMemo(\n () => [\n {\n title: \"Page Config\",\n content: panelConfig.pageConfigPanel,\n defaultSize: 25,\n },\n {\n title: \"UI Editor\",\n content: panelConfig.editorPanel,\n defaultSize: 50,\n },\n {\n title: \"Props\",\n content: panelConfig.propsPanel,\n defaultSize: 25,\n },\n ],\n [panelConfig]\n );\n\n const [selectedPanel, setSelectedPanel] = useState(mainPanels[1]);\n\n const handlePanelClickById = useCallback((e: React.MouseEvent) => {\n const panelIndex = parseInt(e.currentTarget.dataset.panelIndex || \"0\");\n setSelectedPanel(mainPanels[panelIndex]);\n }, [mainPanels]);\n\n return (\n \n {panelConfig.navBar}\n {/* Desktop Layout */}\n \n \n {mainPanels.map((panel, index) => (\n \n {index > 0 && }\n \n {panel.content}\n \n \n ))}\n \n \n {/* Mobile Layout */}\n \n {selectedPanel.content}\n \n \n {mainPanels.map((panel, index) => (\n \n {panel.title}\n \n ))}\n \n \n \n \n );\n}\n\n/**\n * PageConfigPanel renders a tabbed panel for page configuration, including layers, appearance, and variables tabs.\n *\n * @param {object} props\n * @param {string} props.className - The class name for the panel container.\n * @param {TabsContentConfig} props.tabsContent - The content for the tabs.\n * @returns {JSX.Element} The page config panel with tabs.\n */\nexport function PageConfigPanel({\n className,\n tabsContent,\n}: {\n className: string;\n tabsContent: TabsContentConfig;\n}) {\n const { layers, appearance, data } = tabsContent;\n const tabCount = 1 + (appearance ? 1 : 0) + (data ? 1 : 0);\n \n return (\n \n \n {layers.title}\n {appearance && {appearance.title}}\n {data && {data.title}}\n \n \n {layers.content}\n \n {appearance && (\n \n {appearance.content}\n \n )}\n {data && (\n \n {data.content}\n \n )}\n \n );\n}\n\n/**\n * Returns the default tab content configuration for the page config panel.\n *\n * @param {boolean} editVariables - Whether to allow editing variables.\n * @returns {TabsContentConfig} The default tabs content configuration.\n */\nexport function defaultConfigTabsContent() {\n return {\n layers: { title: \"Layers\", content: },\n appearance: { title: \"Appearance\", content: (\n \n \n \n \n ),\n },\n data: { title: \"Data\", content: }\n }\n}\n\n/**\n * LoadingSkeleton renders a skeleton UI while the builder is initializing.\n *\n * @returns {JSX.Element} The loading skeleton.\n */\nexport function LoadingSkeleton() {\n return (\n \n \n \n \n \n \n \n \n );\n}\n\n/**\n * Returns the default panel configuration values for the UI Builder.\n *\n * @param {boolean} useCanvas - Whether to use the canvas editor.\n * @param {TabsContentConfig} tabsContent - The content for the page config panel tabs.\n * @returns {PanelConfig} The default panel configuration.\n */\nexport const getDefaultPanelConfigValues = (useCanvas: boolean, tabsContent: TabsContentConfig) => {\n return {\n navBar: ,\n pageConfigPanel: (\n \n ),\n editorPanel: (\n \n ),\n propsPanel: (\n \n ),\n };\n};\n\nexport default UIBuilder;\n", "type": "registry:ui", "target": "components/ui/ui-builder/index.tsx" }, @@ -148,7 +148,7 @@ }, { "path": "components/ui/ui-builder/internal/tailwind-theme-panel.tsx", - "content": "\"use client\";\nimport React, { useCallback, useEffect, useMemo, useState } from \"react\";\nimport {\n baseColors,\n BaseColor,\n} from \"@/components/ui/ui-builder/internal/base-colors\";\nimport { Button } from \"@/components/ui/button\";\nimport { Label } from \"@/components/ui/label\";\nimport { CheckIcon, InfoIcon, MoonIcon, SunIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n useLayerStore,\n} from \"@/lib/ui-builder/store/layer-store\";\nimport { ComponentLayer } from '@/components/ui/ui-builder/types';\nimport { Toggle } from \"@/components/ui/toggle\";\n\nconst RESET_THEME_PROPS = {\n style: undefined,\n \"data-mode\": undefined,\n \"data-color-theme\": undefined,\n \"data-border-radius\": undefined,\n} as const;\n\nexport function TailwindThemePanel() {\n const {\n selectedPageId,\n updateLayer: updateLayerProps,\n findLayerById,\n } = useLayerStore();\n const selectedPageData = findLayerById(selectedPageId) as ComponentLayer;\n const [isCustomTheme, setIsCustomTheme] = useState(\n selectedPageData?.props[\"data-color-theme\"] !== undefined\n );\n //if not isCustomTheme we delete the themeColors from the pageLayer\n useEffect(() => {\n if (!isCustomTheme) {\n updateLayerProps(selectedPageId, RESET_THEME_PROPS);\n }\n }, [isCustomTheme, selectedPageId, updateLayerProps]);\n\n const handleOnToggle = useCallback(() => {\n setIsCustomTheme(!isCustomTheme);\n }, [isCustomTheme]);\n\n return (\n \n \n {isCustomTheme ? \"Use Default Theme\" : \"Use Custom Theme\"}\n \n {!isCustomTheme && (\n \n Using Your Project's Theme\n \n )}\n {selectedPageData && isCustomTheme && (\n \n )}\n \n );\n}\n\nfunction ThemePicker({\n className,\n isDisabled,\n pageLayer,\n}: {\n className?: string;\n isDisabled: boolean;\n pageLayer: ComponentLayer;\n}) {\n const { updateLayer: updateLayerProps } = useLayerStore();\n\n const [colorTheme, setColorTheme] = useState(\n pageLayer.props?.[\"data-color-theme\"] || \"red\"\n );\n const [borderRadius, setBorderRadius] = useState(\n pageLayer.props?.[\"data-border-radius\"] || 0.3\n );\n const [mode, setMode] = useState<\"light\" | \"dark\">(\n pageLayer.props?.[\"data-mode\"] || \"light\"\n );\n\n useEffect(() => {\n if (isDisabled) return;\n\n const colorData = baseColors.find((color) => color.name === colorTheme);\n if (colorData) {\n const colorDataWithBorder = {\n ...colorData,\n cssVars: {\n ...colorData.cssVars,\n [mode]: {\n ...colorData.cssVars[mode],\n radius: `${borderRadius}rem`,\n },\n },\n } as const;\n\n const themeStyle = themeToStyleVars(colorDataWithBorder.cssVars[mode]);\n\n updateLayerProps(pageLayer.id, {\n style: themeStyle,\n \"data-mode\": mode,\n \"data-color-theme\": colorTheme,\n \"data-border-radius\": borderRadius,\n });\n }\n }, [pageLayer.id, updateLayerProps, colorTheme, borderRadius, mode, isDisabled]);\n\n const colorOptions = useMemo(() => baseColors.map((color: BaseColor) => {\n return (\n \n );\n }), [colorTheme, mode]);\n const borderRadiusOptions = useMemo(() => [0.0, 0.15, 0.3, 0.5, 0.75, 1.0].map((radius) => {\n return (\n \n );\n }), [borderRadius, setBorderRadius]);\n\n const modeOptions = useMemo(() => ([\"light\", \"dark\"] as const).map((modeOption) => {\n return (\n \n );\n }), [mode, setMode]);\n\n return (\n \n Colors\n {colorOptions}\n Border Radius\n {borderRadiusOptions}\n Mode\n {modeOptions}\n \n );\n}\n\nfunction ThemeColorOption({ color, colorTheme, mode, onClick }: { color: BaseColor, colorTheme: string, mode: \"light\" | \"dark\", onClick: (name: BaseColor[\"name\"]) => void }) {\n\n const handleOnClick = useCallback(() => {\n onClick(color.name);\n }, [onClick, color.name]);\n\n const style = useMemo(() => ({\n backgroundColor: `hsl(${\n color.activeColor[mode === \"dark\" ? \"dark\" : \"light\"]\n })`,\n }), [color.activeColor, mode]);\n\n return (\n \n \n {color.name === colorTheme && }\n \n {color.label}\n \n );\n}\n\nfunction ThemeBorderRadiusOption({ radius, borderRadius, onClick }: { radius: number, borderRadius: number, onClick: (radius: number) => void }) {\n\n const handleOnClick = useCallback(() => {\n onClick(radius);\n }, [onClick, radius]);\n\n const style = useMemo(() => ({\n borderRadius: `${radius}rem`,\n }), [radius]);\n\n return (\n \n \n \n \n {radius}\n \n );\n}\n\nfunction ThemeModeOption({modeOption, mode, onClick}: {modeOption: \"light\" | \"dark\", mode: \"light\" | \"dark\", onClick: (mode: \"light\" | \"dark\") => void}){\n\n const handleOnClick = useCallback(() => {\n onClick(modeOption);\n }, [onClick, modeOption]);\n\n return (\n \n {modeOption === \"light\" ? (\n \n ) : (\n \n )}\n {modeOption}\n \n );\n}\n\n\nfunction themeToStyleVars(\n colors:\n | BaseColor[\"cssVars\"][\"dark\"]\n | BaseColor[\"cssVars\"][\"light\"]\n | undefined\n) {\n if (!colors) {\n return undefined;\n }\n const styleVariables = Object.entries(colors).reduce(\n (acc, [key, value]) => {\n acc[`--${key}`] = value;\n return acc;\n },\n {} as { [key: string]: string }\n );\n const globalOverrides = {\n color: `hsl(${colors.foreground})`,\n borderColor: `hsl(${colors.border})`,\n };\n return { ...styleVariables, ...globalOverrides };\n}\n", + "content": "\"use client\";\nimport React, { useCallback, useEffect, useMemo, useState } from \"react\";\nimport {\n baseColors,\n BaseColor,\n} from \"@/components/ui/ui-builder/internal/base-colors\";\nimport { Button } from \"@/components/ui/button\";\nimport { Label } from \"@/components/ui/label\";\nimport { CheckIcon, InfoIcon, MoonIcon, SunIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n useLayerStore,\n} from \"@/lib/ui-builder/store/layer-store\";\nimport { ComponentLayer } from '@/components/ui/ui-builder/types';\nimport { Toggle } from \"@/components/ui/toggle\";\n\nconst RESET_THEME_PROPS = {\n style: undefined,\n \"data-mode\": undefined,\n \"data-color-theme\": undefined,\n \"data-border-radius\": undefined,\n} as const;\n\nexport function TailwindThemePanel() {\n const {\n selectedPageId,\n updateLayer: updateLayerProps,\n findLayerById,\n } = useLayerStore();\n const selectedPageData = findLayerById(selectedPageId) as ComponentLayer;\n const [isCustomTheme, setIsCustomTheme] = useState(\n selectedPageData?.props[\"data-color-theme\"] !== undefined\n );\n //if not isCustomTheme we delete the themeColors from the pageLayer\n useEffect(() => {\n if (!isCustomTheme) {\n updateLayerProps(selectedPageId, RESET_THEME_PROPS);\n }\n }, [isCustomTheme, selectedPageId, updateLayerProps]);\n\n const handleOnToggle = useCallback(() => {\n setIsCustomTheme(!isCustomTheme);\n }, [isCustomTheme]);\n\n return (\n \n \n {isCustomTheme ? \"Use Default Theme\" : \"Use Custom Theme\"}\n \n {!isCustomTheme && (\n \n Using Your Project's Theme\n \n )}\n {selectedPageData && isCustomTheme && (\n \n )}\n \n );\n}\n\nfunction ThemePicker({\n className,\n isDisabled,\n pageLayer,\n}: {\n className?: string;\n isDisabled: boolean;\n pageLayer: ComponentLayer;\n}) {\n const { updateLayer: updateLayerProps } = useLayerStore();\n\n // Safely extract values with type checking\n const colorThemeValue = pageLayer.props?.[\"data-color-theme\"];\n const modeValue = pageLayer.props?.[\"data-mode\"];\n const borderRadiusValue = pageLayer.props?.borderRadius;\n\n const [colorTheme, setColorTheme] = useState(\n (typeof colorThemeValue === 'string' ? colorThemeValue : \"red\") as BaseColor[\"name\"]\n );\n const [borderRadius, setBorderRadius] = useState(\n typeof borderRadiusValue === 'number' ? borderRadiusValue : 0.5\n );\n const [mode, setMode] = useState<\"light\" | \"dark\">(\n (typeof modeValue === 'string' ? modeValue : \"light\") as \"light\" | \"dark\"\n );\n\n \n\n useEffect(() => {\n if (isDisabled) return;\n\n const colorThemeData = baseColors.find((color) => color.name === colorTheme);\n\n if (colorThemeData) {\n const colorDataWithBorder = {\n ...colorThemeData,\n cssVars: {\n ...colorThemeData.cssVars,\n [mode]: {\n ...colorThemeData.cssVars[mode],\n radius: `${borderRadius}rem`,\n },\n },\n } as const;\n\n const themeStyle = themeToStyleVars(colorDataWithBorder.cssVars[mode]);\n\n updateLayerProps(pageLayer.id, {\n style: themeStyle,\n \"data-mode\": mode,\n \"data-color-theme\": colorTheme,\n \"data-border-radius\": borderRadius,\n });\n }\n }, [pageLayer.id, updateLayerProps, colorTheme, borderRadius, mode, isDisabled]);\n\n const colorOptions = useMemo(() => baseColors.map((color: BaseColor) => {\n return (\n \n );\n }), [colorTheme, mode]);\n const borderRadiusOptions = useMemo(() => [0.0, 0.15, 0.3, 0.5, 0.75, 1.0].map((radius) => {\n return (\n \n );\n }), [borderRadius, setBorderRadius]);\n\n const modeOptions = useMemo(() => ([\"light\", \"dark\"] as const).map((modeOption) => {\n return (\n \n );\n }), [mode, setMode]);\n\n return (\n \n Colors\n {colorOptions}\n Border Radius\n {borderRadiusOptions}\n Mode\n {modeOptions}\n \n );\n}\n\nfunction ThemeColorOption({ color, colorTheme, mode, onClick }: { color: BaseColor, colorTheme: string, mode: \"light\" | \"dark\", onClick: (name: BaseColor[\"name\"]) => void }) {\n\n const handleOnClick = useCallback(() => {\n onClick(color.name);\n }, [onClick, color.name]);\n\n const style = useMemo(() => ({\n backgroundColor: `hsl(${\n color.activeColor[mode === \"dark\" ? \"dark\" : \"light\"]\n })`,\n }), [color.activeColor, mode]);\n\n return (\n \n \n {color.name === colorTheme && }\n \n {color.label}\n \n );\n}\n\nfunction ThemeBorderRadiusOption({ radius, borderRadius, onClick }: { radius: number, borderRadius: number, onClick: (radius: number) => void }) {\n\n const handleOnClick = useCallback(() => {\n onClick(radius);\n }, [onClick, radius]);\n\n const style = useMemo(() => ({\n borderRadius: `${radius}rem`,\n }), [radius]);\n\n return (\n \n \n \n \n {radius}\n \n );\n}\n\nfunction ThemeModeOption({modeOption, mode, onClick}: {modeOption: \"light\" | \"dark\", mode: \"light\" | \"dark\", onClick: (mode: \"light\" | \"dark\") => void}){\n\n const handleOnClick = useCallback(() => {\n onClick(modeOption);\n }, [onClick, modeOption]);\n\n return (\n \n {modeOption === \"light\" ? (\n \n ) : (\n \n )}\n {modeOption}\n \n );\n}\n\n\nfunction themeToStyleVars(\n colors:\n | BaseColor[\"cssVars\"][\"dark\"]\n | BaseColor[\"cssVars\"][\"light\"]\n | undefined\n) {\n if (!colors) {\n return undefined;\n }\n const styleVariables = Object.entries(colors).reduce(\n (acc, [key, value]) => {\n acc[`--${key}`] = value;\n return acc;\n },\n {} as { [key: string]: string }\n );\n const globalOverrides = {\n color: `hsl(${colors.foreground})`,\n borderColor: `hsl(${colors.border})`,\n };\n return { ...styleVariables, ...globalOverrides };\n}\n", "type": "registry:ui", "target": "components/ui/ui-builder/internal/tailwind-theme-panel.tsx" }, @@ -160,13 +160,13 @@ }, { "path": "components/ui/ui-builder/internal/render-utils.tsx", - "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport React, { memo, Suspense, useMemo, useRef } from \"react\";\nimport isDeepEqual from \"fast-deep-equal\";\n\nimport { ClickableWrapper } from \"@/components/ui/ui-builder/internal/clickable-wrapper\";\nimport { ErrorBoundary } from \"react-error-boundary\";\n\nimport { ErrorFallback } from \"@/components/ui/ui-builder/internal/error-fallback\";\nimport { isPrimitiveComponent } from \"@/lib/ui-builder/store/editor-utils\";\nimport { hasLayerChildren } from \"@/lib/ui-builder/store/layer-utils\";\nimport { DevProfiler } from \"@/components/ui/ui-builder/internal/dev-profiler\";\nimport { ComponentRegistry, ComponentLayer, Variable } from '@/components/ui/ui-builder/types';\nimport { useLayerStore } from \"@/lib/ui-builder/store/layer-store\";\nimport { resolveVariableReferences } from \"@/lib/ui-builder/utils/variable-resolver\";\n\nexport interface EditorConfig {\n \n zIndex: number;\n totalLayers: number;\n selectedLayer: ComponentLayer;\n parentUpdated?: boolean;\n onSelectElement: (layerId: string) => void;\n handleDuplicateLayer?: () => void;\n handleDeleteLayer?: () => void;\n usingCanvas?: boolean;\n}\n\nexport const RenderLayer: React.FC<{\n layer: ComponentLayer;\n componentRegistry: ComponentRegistry;\n editorConfig?: EditorConfig;\n variables?: Variable[];\n variableValues?: Record;\n}> = memo(\n ({ layer, componentRegistry, editorConfig, variables, variableValues }) => {\n const storeVariables = useLayerStore((state) => state.variables);\n // Use provided variables or fall back to store variables\n const effectiveVariables = variables || storeVariables;\n const componentDefinition =\n componentRegistry[layer.type as keyof typeof componentRegistry];\n\n const prevLayer = useRef(layer);\n\n const infoData = useMemo(() => ({\n layerType: layer.type,\n layerId: layer.id,\n layerName: layer.name,\n availableComponents: Object.keys(componentRegistry),\n layer: layer\n }), [layer, componentRegistry]);\n\n\n // Resolve variable references in props\n const resolvedProps = resolveVariableReferences(layer.props, effectiveVariables, variableValues);\n const childProps: Record = useMemo(() => ({ ...resolvedProps }), [resolvedProps]);\n \n // Memoize child editor config to avoid creating objects in JSX\n const childEditorConfig = useMemo(() => {\n return editorConfig\n ? { ...editorConfig, zIndex: editorConfig.zIndex + 1, parentUpdated: editorConfig.parentUpdated || !isDeepEqual(prevLayer.current, layer) }\n : undefined;\n }, [editorConfig, layer]);\n\n if (!componentDefinition) {\n console.error(\n `[UIBuilder] Component definition not found in registry:`, \n infoData\n );\n return null;\n }\n\n let Component: React.ElementType | undefined =\n componentDefinition.component;\n let isPrimitive = false;\n if (isPrimitiveComponent(componentDefinition)) {\n Component = layer.type as keyof JSX.IntrinsicElements;\n isPrimitive = true;\n }\n\n ;\n\n if (!Component) return null;\n\n \n if (hasLayerChildren(layer) && layer.children.length > 0) {\n childProps.children = layer.children.map((child) => (\n \n ));\n } else if (typeof layer.children === \"string\") {\n childProps.children = layer.children;\n }\n\n const WrappedComponent = isPrimitive ? (\n \n ) : (\n \n \n \n );\n\n if (!editorConfig) {\n return WrappedComponent;\n } else {\n const {\n zIndex,\n totalLayers,\n selectedLayer,\n onSelectElement,\n handleDuplicateLayer,\n handleDeleteLayer,\n usingCanvas,\n } = editorConfig;\n\n return (\n \n \n {WrappedComponent}\n \n \n );\n }\n },\n (prevProps, nextProps) => {\n if(nextProps.editorConfig?.parentUpdated) {\n return false;\n }\n const editorConfigEqual = isDeepEqual(\n prevProps.editorConfig?.selectedLayer?.id,\n nextProps.editorConfig?.selectedLayer?.id\n );\n const layerEqual = isDeepEqual(prevProps.layer, nextProps.layer);\n return editorConfigEqual && layerEqual;\n }\n);\n\nRenderLayer.displayName = \"RenderLayer\";\n\nconst ErrorSuspenseWrapper: React.FC<{\n id: string;\n children: React.ReactNode;\n}> = ({ children }) => {\n const loadingFallback = useMemo(() => , []);\n \n return (\n \n {children}\n \n );\n};\n\n\nconst LoadingComponent: React.FC = () => (\n Loading...\n);\n\n", + "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport React, { memo, Suspense, useMemo, useRef } from \"react\";\nimport isDeepEqual from \"fast-deep-equal\";\n\nimport { ClickableWrapper } from \"@/components/ui/ui-builder/internal/clickable-wrapper\";\nimport { ErrorBoundary } from \"react-error-boundary\";\n\nimport { ErrorFallback } from \"@/components/ui/ui-builder/internal/error-fallback\";\nimport { isPrimitiveComponent } from \"@/lib/ui-builder/store/editor-utils\";\nimport { hasLayerChildren } from \"@/lib/ui-builder/store/layer-utils\";\nimport { DevProfiler } from \"@/components/ui/ui-builder/internal/dev-profiler\";\nimport { ComponentRegistry, ComponentLayer, Variable, PropValue } from '@/components/ui/ui-builder/types';\nimport { useLayerStore } from \"@/lib/ui-builder/store/layer-store\";\nimport { resolveVariableReferences } from \"@/lib/ui-builder/utils/variable-resolver\";\n\nexport interface EditorConfig {\n zIndex: number;\n totalLayers: number;\n selectedLayer: ComponentLayer;\n parentUpdated?: boolean;\n onSelectElement: (layerId: string) => void;\n handleDuplicateLayer?: () => void;\n handleDeleteLayer?: () => void;\n usingCanvas?: boolean;\n}\n\nexport const RenderLayer: React.FC<{\n layer: ComponentLayer;\n componentRegistry: ComponentRegistry;\n editorConfig?: EditorConfig;\n variables?: Variable[];\n variableValues?: Record;\n}> = memo(\n ({ layer, componentRegistry, editorConfig, variables, variableValues }) => {\n const storeVariables = useLayerStore((state) => state.variables);\n // Use provided variables or fall back to store variables\n const effectiveVariables = variables || storeVariables;\n const componentDefinition =\n componentRegistry[layer.type as keyof typeof componentRegistry];\n\n const prevLayer = useRef(layer);\n\n const infoData = useMemo(() => ({\n layerType: layer.type,\n layerId: layer.id,\n layerName: layer.name,\n availableComponents: Object.keys(componentRegistry),\n layer: layer\n }), [layer, componentRegistry]);\n\n\n // Resolve variable references in props\n const resolvedProps = resolveVariableReferences(layer.props, effectiveVariables, variableValues);\n const childProps: Record = useMemo(() => ({ ...resolvedProps }), [resolvedProps]);\n \n // Memoize child editor config to avoid creating objects in JSX\n const childEditorConfig = useMemo(() => {\n return editorConfig\n ? { ...editorConfig, zIndex: editorConfig.zIndex + 1, parentUpdated: editorConfig.parentUpdated || !isDeepEqual(prevLayer.current, layer) }\n : undefined;\n }, [editorConfig, layer]);\n\n if (!componentDefinition) {\n console.error(\n `[UIBuilder] Component definition not found in registry:`, \n infoData\n );\n return null;\n }\n\n let Component: React.ElementType | undefined =\n componentDefinition.component;\n let isPrimitive = false;\n if (isPrimitiveComponent(componentDefinition)) {\n Component = layer.type as keyof JSX.IntrinsicElements;\n isPrimitive = true;\n }\n\n ;\n\n if (!Component) return null;\n\n \n if (hasLayerChildren(layer) && layer.children.length > 0) {\n childProps.children = layer.children.map((child) => (\n \n ));\n } else if (typeof layer.children === \"string\") {\n childProps.children = layer.children;\n }\n\n const WrappedComponent = isPrimitive ? (\n \n ) : (\n \n \n \n );\n\n if (!editorConfig) {\n return WrappedComponent;\n } else {\n const {\n zIndex,\n totalLayers,\n selectedLayer,\n onSelectElement,\n handleDuplicateLayer,\n handleDeleteLayer,\n usingCanvas,\n } = editorConfig;\n\n return (\n \n \n {WrappedComponent}\n \n \n );\n }\n },\n (prevProps, nextProps) => {\n if(nextProps.editorConfig?.parentUpdated) {\n return false;\n }\n const editorConfigEqual = isDeepEqual(\n prevProps.editorConfig?.selectedLayer?.id,\n nextProps.editorConfig?.selectedLayer?.id\n );\n const layerEqual = isDeepEqual(prevProps.layer, nextProps.layer);\n return editorConfigEqual && layerEqual;\n }\n);\n\nRenderLayer.displayName = \"RenderLayer\";\n\nconst ErrorSuspenseWrapper: React.FC<{\n id: string;\n children: React.ReactNode;\n}> = ({ children }) => {\n const loadingFallback = useMemo(() => , []);\n \n return (\n \n {children}\n \n );\n};\n\n\nconst LoadingComponent: React.FC = () => (\n Loading...\n);\n\n", "type": "registry:ui", "target": "components/ui/ui-builder/internal/render-utils.tsx" }, { "path": "components/ui/ui-builder/internal/props-panel.tsx", - "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport React, { useCallback, useMemo } from \"react\";\nimport { z } from \"zod\";\nimport { useLayerStore } from \"@/lib/ui-builder/store/layer-store\";\nimport { useEditorStore } from \"@/lib/ui-builder/store/editor-store\";\nimport {\n ComponentRegistry,\n ComponentLayer,\n} from \"@/components/ui/ui-builder/types\";\nimport { Button } from \"@/components/ui/button\";\nimport AutoForm from \"@/components/ui/auto-form\";\nimport { generateFieldOverrides } from \"@/lib/ui-builder/store/editor-utils\";\nimport { addDefaultValues } from \"@/lib/ui-builder/store/schema-utils\";\nimport { getBaseType } from \"@/components/ui/auto-form/utils\";\nimport { isVariableReference } from \"@/lib/ui-builder/utils/variable-resolver\";\nimport { resolveVariableReferences } from \"@/lib/ui-builder/utils/variable-resolver\";\n\ninterface PropsPanelProps {\n className?: string;\n}\n\nconst PropsPanel: React.FC = ({ className }) => {\n const selectedLayerId = useLayerStore((state) => state.selectedLayerId);\n const findLayerById = useLayerStore((state) => state.findLayerById);\n const removeLayer = useLayerStore((state) => state.removeLayer);\n const duplicateLayer = useLayerStore((state) => state.duplicateLayer);\n const updateLayer = useLayerStore((state) => state.updateLayer);\n const addComponentLayer = useLayerStore((state) => state.addComponentLayer);\n const componentRegistry = useEditorStore((state) => state.registry);\n const selectedLayer = findLayerById(selectedLayerId);\n\n const handleAddComponentLayer = useCallback(\n (layerType: string, parentLayerId: string, addPosition?: number) => {\n addComponentLayer(layerType, parentLayerId, addPosition);\n },\n [addComponentLayer]\n );\n\n const handleDeleteLayer = useCallback(\n (layerId: string) => {\n removeLayer(layerId);\n },\n [removeLayer]\n );\n\n const handleDuplicateLayer = useCallback(() => {\n if (selectedLayer) {\n duplicateLayer(selectedLayer.id);\n }\n }, [selectedLayer, duplicateLayer]);\n\n const handleUpdateLayer = useCallback(\n (\n id: string,\n props: Record,\n rest?: Partial>\n ) => {\n updateLayer(id, props, rest);\n },\n [updateLayer]\n );\n\n //first check if selectedLayer.type is a valid key in componentRegistry\n if (\n selectedLayer &&\n !componentRegistry[selectedLayer.type as keyof typeof componentRegistry]\n ) {\n return null;\n }\n\n return (\n \n {selectedLayer && (\n <>\n \n \n Type: {selectedLayer.type.replaceAll(\"_\", \"\")}\n \n >\n )}\n\n {!selectedLayer && (\n <>\n Component Properties\n No component selected\n >\n )}\n {selectedLayer && (\n \n )}\n \n );\n};\nPropsPanel.displayName = \"PropsPanel\";\nexport default PropsPanel;\n\ninterface ComponentPropsAutoFormProps {\n selectedLayerId: string;\n componentRegistry: ComponentRegistry;\n removeLayer: (id: string) => void;\n duplicateLayer: (id: string) => void;\n updateLayer: (\n id: string,\n props: Record,\n rest?: Partial>\n ) => void;\n addComponentLayer: (\n layerType: string,\n parentLayerId: string,\n addPosition?: number\n ) => void;\n}\n\nconst EMPTY_ZOD_SCHEMA = z.object({});\nconst EMPTY_FORM_VALUES = {};\n\nconst ComponentPropsAutoForm: React.FC = ({\n selectedLayerId,\n componentRegistry,\n removeLayer,\n duplicateLayer,\n updateLayer,\n addComponentLayer,\n}) => {\n const findLayerById = useLayerStore((state) => state.findLayerById);\n const revisionCounter = useEditorStore((state) => state.revisionCounter);\n const selectedLayer = findLayerById(selectedLayerId) as\n | ComponentLayer\n | undefined;\n const isPage = useLayerStore((state) => state.isLayerAPage(selectedLayerId));\n const allowPagesCreation = useEditorStore(\n (state) => state.allowPagesCreation\n );\n const allowPagesDeletion = useEditorStore(\n (state) => state.allowPagesDeletion\n );\n\n // Retrieve the appropriate schema from componentRegistry\n const { schema } = useMemo(() => {\n if (\n selectedLayer &&\n componentRegistry[selectedLayer.type as keyof typeof componentRegistry]\n ) {\n return componentRegistry[\n selectedLayer.type as keyof typeof componentRegistry\n ];\n }\n return { schema: EMPTY_ZOD_SCHEMA }; // Fallback schema\n }, [selectedLayer, componentRegistry]);\n\n const handleDeleteLayer = useCallback(() => {\n removeLayer(selectedLayerId);\n }, [removeLayer, selectedLayerId]);\n\n const handleDuplicateLayer = useCallback(() => {\n duplicateLayer(selectedLayerId);\n }, [duplicateLayer, selectedLayerId]);\n\n const onParsedValuesChange = useCallback(\n (\n parsedValues: z.infer & {\n children?: string | { layerType: string; addPosition: number };\n }\n ) => {\n const { children, ...dataProps } = parsedValues;\n\n // Preserve variable references by merging with original props\n const preservedProps: Record = {};\n if (selectedLayer) {\n // Start with all original props to preserve any that aren't in the form update\n Object.assign(preservedProps, selectedLayer.props);\n\n // Then update only the props that came from the form, preserving variable references\n Object.keys(dataProps as Record).forEach((key) => {\n const originalValue = selectedLayer.props[key];\n const newValue = (dataProps as Record)[key];\n const fieldDef = schema?.shape?.[key];\n const baseType = fieldDef\n ? getBaseType(fieldDef as z.ZodAny)\n : undefined;\n // If the original value was a variable reference, preserve it\n if (isVariableReference(originalValue)) {\n // Keep the variable reference - the form should not override variable bindings\n preservedProps[key] = originalValue;\n } else {\n // Handle date serialization\n if (\n baseType === z.ZodFirstPartyTypeKind.ZodDate &&\n newValue instanceof Date\n ) {\n preservedProps[key] = newValue.toISOString();\n } else {\n preservedProps[key] = newValue;\n }\n }\n });\n }\n\n if (typeof children === \"string\") {\n updateLayer(selectedLayerId, preservedProps, { children: children });\n } else if (children && children.layerType) {\n updateLayer(selectedLayerId, preservedProps, {\n children: selectedLayer?.children,\n });\n addComponentLayer(\n children.layerType,\n selectedLayerId,\n children.addPosition\n );\n } else {\n updateLayer(selectedLayerId, preservedProps);\n }\n },\n [updateLayer, selectedLayerId, selectedLayer, addComponentLayer, schema]\n );\n\n // Prepare values for AutoForm, converting enum values to strings as select elements only accept string values\n const formValues = useMemo(() => {\n if (!selectedLayer) return EMPTY_FORM_VALUES;\n\n const variables = useLayerStore.getState().variables;\n\n // First resolve variable references to get display values\n const resolvedProps = resolveVariableReferences(\n selectedLayer.props,\n variables\n );\n\n const transformedProps: Record = {};\n const schemaShape = schema?.shape as z.ZodRawShape | undefined; // Get shape from the memoized schema\n\n if (schemaShape) {\n for (const [key, value] of Object.entries(resolvedProps)) {\n const fieldDef = schemaShape[key];\n if (fieldDef) {\n const baseType = getBaseType(fieldDef as z.ZodAny);\n if (baseType === z.ZodFirstPartyTypeKind.ZodEnum) {\n // Convert enum value to string if it's not already a string\n transformedProps[key] =\n typeof value === \"string\" ? value : String(value);\n } else if (baseType === z.ZodFirstPartyTypeKind.ZodDate) {\n // Convert string to Date if necessary\n if (value instanceof Date) {\n transformedProps[key] = value;\n } else if (typeof value === \"string\" || typeof value === \"number\") {\n const date = new Date(value);\n transformedProps[key] = isNaN(date.getTime()) ? undefined : date;\n } else {\n transformedProps[key] = undefined;\n }\n } else {\n transformedProps[key] = value;\n }\n } else {\n transformedProps[key] = value;\n }\n }\n } else {\n // Fallback if schema shape isn't available: copy resolved props as is\n Object.assign(transformedProps, resolvedProps);\n }\n\n return { ...transformedProps, children: selectedLayer.children };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [selectedLayer, schema, revisionCounter]); // Include revisionCounter to detect undo/redo changes\n\n const autoFormSchema = useMemo(() => {\n return addDefaultValues(schema, formValues);\n }, [schema, formValues]);\n\n const autoFormFieldConfig = useMemo(() => {\n if (!selectedLayer) return undefined; // Or a default config if appropriate\n return generateFieldOverrides(componentRegistry, selectedLayer);\n }, [componentRegistry, selectedLayer]);\n\n // Create a unique key that changes when we need to force re-render the form\n // This includes selectedLayerId and revisionCounter to handle both layer changes and undo/redo\n const formKey = useMemo(() => {\n return `${selectedLayerId}-${revisionCounter}`;\n }, [selectedLayerId, revisionCounter]);\n\n if (\n !selectedLayer ||\n !componentRegistry[selectedLayer.type as keyof typeof componentRegistry]\n ) {\n return null;\n }\n\n return (\n \n {(!isPage || allowPagesCreation) && (\n \n Duplicate {isPage ? \"Page\" : \"Component\"}\n \n )}\n {(!isPage || allowPagesDeletion) && (\n \n Delete {isPage ? \"Page\" : \"Component\"}\n \n )}\n \n );\n};\n\nComponentPropsAutoForm.displayName = \"ComponentPropsAutoForm\";\n\nconst nameForLayer = (layer: ComponentLayer) => {\n return layer.name || layer.type.replaceAll(\"_\", \"\");\n};\n\nconst Title = () => {\n const { selectedLayerId } = useLayerStore();\n const findLayerById = useLayerStore((state) => state.findLayerById);\n const selectedLayer = findLayerById(selectedLayerId);\n return (\n \n {selectedLayer ? nameForLayer(selectedLayer) : \"\"} Properties\n \n );\n};\n", + "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport React, { useCallback, useMemo } from \"react\";\nimport { z } from \"zod\";\nimport { useLayerStore } from \"@/lib/ui-builder/store/layer-store\";\nimport { useEditorStore } from \"@/lib/ui-builder/store/editor-store\";\nimport {\n ComponentRegistry,\n ComponentLayer,\n} from \"@/components/ui/ui-builder/types\";\nimport { Button } from \"@/components/ui/button\";\nimport AutoForm from \"@/components/ui/auto-form\";\nimport { generateFieldOverrides } from \"@/lib/ui-builder/store/editor-utils\";\nimport { addDefaultValues } from \"@/lib/ui-builder/store/schema-utils\";\nimport { getBaseType } from \"@/components/ui/auto-form/utils\";\nimport { isVariableReference } from \"@/lib/ui-builder/utils/variable-resolver\";\nimport { resolveVariableReferences } from \"@/lib/ui-builder/utils/variable-resolver\";\n\ninterface PropsPanelProps {\n className?: string;\n}\n\nconst PropsPanel: React.FC = ({ className }) => {\n const selectedLayerId = useLayerStore((state) => state.selectedLayerId);\n const findLayerById = useLayerStore((state) => state.findLayerById);\n const removeLayer = useLayerStore((state) => state.removeLayer);\n const duplicateLayer = useLayerStore((state) => state.duplicateLayer);\n const updateLayer = useLayerStore((state) => state.updateLayer);\n const addComponentLayer = useLayerStore((state) => state.addComponentLayer);\n const componentRegistry = useEditorStore((state) => state.registry);\n const selectedLayer = findLayerById(selectedLayerId);\n\n const handleAddComponentLayer = useCallback(\n (layerType: string, parentLayerId: string, addPosition?: number) => {\n addComponentLayer(layerType, parentLayerId, addPosition);\n },\n [addComponentLayer]\n );\n\n const handleDeleteLayer = useCallback(\n (layerId: string) => {\n removeLayer(layerId);\n },\n [removeLayer]\n );\n\n const handleDuplicateLayer = useCallback(() => {\n if (selectedLayer) {\n duplicateLayer(selectedLayer.id);\n }\n }, [selectedLayer, duplicateLayer]);\n\n const handleUpdateLayer = useCallback(\n (\n id: string,\n props: Record,\n rest?: Partial>\n ) => {\n updateLayer(id, props, rest);\n },\n [updateLayer]\n );\n\n //first check if selectedLayer.type is a valid key in componentRegistry\n if (\n selectedLayer &&\n !componentRegistry[selectedLayer.type as keyof typeof componentRegistry]\n ) {\n return null;\n }\n\n return (\n \n {selectedLayer && (\n <>\n \n \n Type: {selectedLayer.type.replaceAll(\"_\", \"\")}\n \n >\n )}\n\n {!selectedLayer && (\n <>\n Component Properties\n No component selected\n >\n )}\n {selectedLayer && (\n \n )}\n \n );\n};\nPropsPanel.displayName = \"PropsPanel\";\nexport default PropsPanel;\n\ninterface ComponentPropsAutoFormProps {\n selectedLayerId: string;\n componentRegistry: ComponentRegistry;\n removeLayer: (id: string) => void;\n duplicateLayer: (id: string) => void;\n updateLayer: (\n id: string,\n props: Record,\n rest?: Partial>\n ) => void;\n addComponentLayer: (\n layerType: string,\n parentLayerId: string,\n addPosition?: number\n ) => void;\n}\n\nconst EMPTY_ZOD_SCHEMA = z.object({});\nconst EMPTY_FORM_VALUES = {};\n\nconst ComponentPropsAutoForm: React.FC = ({\n selectedLayerId,\n componentRegistry,\n removeLayer,\n duplicateLayer,\n updateLayer,\n addComponentLayer,\n}) => {\n const findLayerById = useLayerStore((state) => state.findLayerById);\n const revisionCounter = useEditorStore((state) => state.revisionCounter);\n const selectedLayer = findLayerById(selectedLayerId) as\n | ComponentLayer\n | undefined;\n const isPage = useLayerStore((state) => state.isLayerAPage(selectedLayerId));\n const allowPagesCreation = useEditorStore(\n (state) => state.allowPagesCreation\n );\n const allowPagesDeletion = useEditorStore(\n (state) => state.allowPagesDeletion\n );\n\n // Retrieve the appropriate schema from componentRegistry\n const { schema } = useMemo(() => {\n if (\n selectedLayer &&\n componentRegistry[selectedLayer.type as keyof typeof componentRegistry]\n ) {\n return componentRegistry[\n selectedLayer.type as keyof typeof componentRegistry\n ];\n }\n return { schema: EMPTY_ZOD_SCHEMA }; // Fallback schema\n }, [selectedLayer, componentRegistry]);\n\n const handleDeleteLayer = useCallback(() => {\n removeLayer(selectedLayerId);\n }, [removeLayer, selectedLayerId]);\n\n const handleDuplicateLayer = useCallback(() => {\n duplicateLayer(selectedLayerId);\n }, [duplicateLayer, selectedLayerId]);\n\n const onParsedValuesChange = useCallback(\n (\n parsedValues: z.infer & {\n children?: string | { layerType: string; addPosition: number };\n }\n ) => {\n const { children, ...dataProps } = parsedValues;\n\n // Preserve variable references by merging with original props\n const preservedProps: Record = {};\n if (selectedLayer) {\n // Start with all original props to preserve any that aren't in the form update\n Object.assign(preservedProps, selectedLayer.props);\n\n // Then update only the props that came from the form, preserving variable references\n Object.keys(dataProps as Record).forEach((key) => {\n const originalValue = selectedLayer.props[key];\n const newValue = (dataProps as Record)[key];\n const fieldDef = ('shape' in schema && schema.shape) ? schema.shape[key] : undefined;\n const baseType = fieldDef\n ? getBaseType(fieldDef as z.ZodAny)\n : undefined;\n // If the original value was a variable reference, preserve it\n if (isVariableReference(originalValue)) {\n // Keep the variable reference - the form should not override variable bindings\n preservedProps[key] = originalValue;\n } else {\n // Handle date serialization\n if (\n baseType === z.ZodFirstPartyTypeKind.ZodDate &&\n newValue instanceof Date\n ) {\n preservedProps[key] = newValue.toISOString();\n } else {\n preservedProps[key] = newValue;\n }\n }\n });\n }\n\n if (typeof children === \"string\") {\n updateLayer(selectedLayerId, preservedProps, { children: children });\n } else if (children && children.layerType) {\n updateLayer(selectedLayerId, preservedProps, {\n children: selectedLayer?.children,\n });\n addComponentLayer(\n children.layerType,\n selectedLayerId,\n children.addPosition\n );\n } else {\n updateLayer(selectedLayerId, preservedProps);\n }\n },\n [updateLayer, selectedLayerId, selectedLayer, addComponentLayer, schema]\n );\n\n // Prepare values for AutoForm, converting enum values to strings as select elements only accept string values\n const formValues = useMemo(() => {\n if (!selectedLayer) return EMPTY_FORM_VALUES;\n\n const variables = useLayerStore.getState().variables;\n\n // First resolve variable references to get display values\n const resolvedProps = resolveVariableReferences(\n selectedLayer.props,\n variables\n );\n\n const transformedProps: Record = {};\n const schemaShape = ('shape' in schema && schema.shape) ? schema.shape as z.ZodRawShape : undefined; // Get shape from the memoized schema\n\n if (schemaShape) {\n for (const [key, value] of Object.entries(resolvedProps)) {\n const fieldDef = schemaShape[key];\n if (fieldDef) {\n const baseType = getBaseType(fieldDef as z.ZodAny);\n if (baseType === z.ZodFirstPartyTypeKind.ZodEnum) {\n // Convert enum value to string if it's not already a string\n transformedProps[key] =\n typeof value === \"string\" ? value : String(value);\n } else if (baseType === z.ZodFirstPartyTypeKind.ZodDate) {\n // Convert string to Date if necessary\n if (value instanceof Date) {\n transformedProps[key] = value;\n } else if (typeof value === \"string\" || typeof value === \"number\") {\n const date = new Date(value);\n transformedProps[key] = isNaN(date.getTime()) ? undefined : date;\n } else {\n transformedProps[key] = undefined;\n }\n } else {\n transformedProps[key] = value;\n }\n } else {\n transformedProps[key] = value;\n }\n }\n } else {\n // Fallback if schema shape isn't available: copy resolved props as is\n Object.assign(transformedProps, resolvedProps);\n }\n\n return { ...transformedProps, children: selectedLayer.children };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [selectedLayer, schema, revisionCounter]); // Include revisionCounter to detect undo/redo changes\n\n const autoFormSchema = useMemo(() => {\n // Only pass ZodObject schemas to addDefaultValues, otherwise return the original schema\n if ('shape' in schema && typeof schema.shape === 'object') {\n try {\n return addDefaultValues(schema as any, formValues);\n } catch (error) {\n console.warn('Failed to add default values to schema:', error);\n return schema;\n }\n }\n return schema;\n }, [schema, formValues]);\n\n const autoFormFieldConfig = useMemo(() => {\n if (!selectedLayer) return undefined; // Or a default config if appropriate\n return generateFieldOverrides(componentRegistry, selectedLayer);\n }, [componentRegistry, selectedLayer]);\n\n // Create a unique key that changes when we need to force re-render the form\n // This includes selectedLayerId and revisionCounter to handle both layer changes and undo/redo\n const formKey = useMemo(() => {\n return `${selectedLayerId}-${revisionCounter}`;\n }, [selectedLayerId, revisionCounter]);\n\n if (\n !selectedLayer ||\n !componentRegistry[selectedLayer.type as keyof typeof componentRegistry]\n ) {\n return null;\n }\n\n return (\n \n {(!isPage || allowPagesCreation) && (\n \n Duplicate {isPage ? \"Page\" : \"Component\"}\n \n )}\n {(!isPage || allowPagesDeletion) && (\n \n Delete {isPage ? \"Page\" : \"Component\"}\n \n )}\n \n );\n};\n\nComponentPropsAutoForm.displayName = \"ComponentPropsAutoForm\";\n\nconst nameForLayer = (layer: ComponentLayer) => {\n return layer.name || layer.type.replaceAll(\"_\", \"\");\n};\n\nconst Title = () => {\n const { selectedLayerId } = useLayerStore();\n const findLayerById = useLayerStore((state) => state.findLayerById);\n const selectedLayer = findLayerById(selectedLayerId);\n return (\n \n {selectedLayer ? nameForLayer(selectedLayer) : \"\"} Properties\n \n );\n};\n", "type": "registry:ui", "target": "components/ui/ui-builder/internal/props-panel.tsx" }, @@ -190,7 +190,7 @@ }, { "path": "components/ui/ui-builder/internal/layer-menu.tsx", - "content": "import React, { useMemo, useState } from \"react\";\nimport { ChevronRight, Plus, Trash, Copy } from \"lucide-react\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { useLayerStore } from \"@/lib/ui-builder/store/layer-store\";\nimport { useEditorStore } from \"@/lib/ui-builder/store/editor-store\";\nimport { AddComponentsPopover } from \"@/components/ui/ui-builder/internal/add-component-popover\";\nimport { cn } from \"@/lib/utils\";\nimport { hasLayerChildren } from \"@/lib/ui-builder/store/layer-utils\";\n\ninterface MenuProps {\n layerId: string;\n x: number;\n y: number;\n width: number;\n height: number;\n zIndex: number;\n handleDuplicateComponent?: () => void;\n handleDeleteComponent?: () => void;\n}\n\nexport const LayerMenu: React.FC = ({\n layerId,\n x,\n y,\n zIndex,\n handleDuplicateComponent,\n handleDeleteComponent,\n}) => {\n const [popoverOpen, setPopoverOpen] = useState(false);\n const selectedLayer = useLayerStore((state) => state.findLayerById(layerId));\n const isLayerAPage = useLayerStore((state) => state.isLayerAPage(layerId));\n\n const componentRegistry = useEditorStore((state) => state.registry);\n const allowPagesCreation = useEditorStore((state) => state.allowPagesCreation);\n const allowPagesDeletion = useEditorStore((state) => state.allowPagesDeletion);\n\n // Check permissions for page operations\n const canDuplicate = !isLayerAPage || allowPagesCreation;\n const canDelete = !isLayerAPage || allowPagesDeletion;\n\n const style = useMemo(() => ({\n top: y,\n left: x,\n zIndex: zIndex,\n }), [y, x, zIndex]);\n\n const buttonVariantsValues = useMemo(() => {\n return buttonVariants({ variant: \"ghost\", size: \"sm\" });\n }, []);\n\n const canRenderAddChild = useMemo(() => {\n if (!selectedLayer) return false;\n \n const componentDef = componentRegistry[selectedLayer.type as keyof typeof componentRegistry];\n if (!componentDef) return false;\n \n return (\n hasLayerChildren(selectedLayer) &&\n componentDef.schema.shape.children !== undefined\n );\n }, [selectedLayer, componentRegistry]);\n\n return (\n <>\n \n \n \n\n \n {canRenderAddChild && (\n \n \n Add Component\n \n \n \n )}\n {canDuplicate && (\n \n Duplicate {isLayerAPage ? \"Page\" : \"Component\"}\n \n \n )}\n {canDelete && (\n \n Delete {isLayerAPage ? \"Page\" : \"Component\"}\n \n \n )}\n \n \n \n >\n );\n};\n", + "content": "import React, { useMemo, useState } from \"react\";\nimport { ChevronRight, Plus, Trash, Copy } from \"lucide-react\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { useLayerStore } from \"@/lib/ui-builder/store/layer-store\";\nimport { useEditorStore } from \"@/lib/ui-builder/store/editor-store\";\nimport { AddComponentsPopover } from \"@/components/ui/ui-builder/internal/add-component-popover\";\nimport { cn } from \"@/lib/utils\";\nimport { hasLayerChildren } from \"@/lib/ui-builder/store/layer-utils\";\n\ninterface MenuProps {\n layerId: string;\n x: number;\n y: number;\n width: number;\n height: number;\n zIndex: number;\n handleDuplicateComponent?: () => void;\n handleDeleteComponent?: () => void;\n}\n\nexport const LayerMenu: React.FC = ({\n layerId,\n x,\n y,\n zIndex,\n handleDuplicateComponent,\n handleDeleteComponent,\n}) => {\n const [popoverOpen, setPopoverOpen] = useState(false);\n const selectedLayer = useLayerStore((state) => state.findLayerById(layerId));\n const isLayerAPage = useLayerStore((state) => state.isLayerAPage(layerId));\n\n const componentRegistry = useEditorStore((state) => state.registry);\n const allowPagesCreation = useEditorStore((state) => state.allowPagesCreation);\n const allowPagesDeletion = useEditorStore((state) => state.allowPagesDeletion);\n\n // Check permissions for page operations\n const canDuplicate = !isLayerAPage || allowPagesCreation;\n const canDelete = !isLayerAPage || allowPagesDeletion;\n\n const style = useMemo(() => ({\n top: y,\n left: x,\n zIndex: zIndex,\n }), [y, x, zIndex]);\n\n const buttonVariantsValues = useMemo(() => {\n return buttonVariants({ variant: \"ghost\", size: \"sm\" });\n }, []);\n\n const canRenderAddChild = useMemo(() => {\n if (!selectedLayer) return false;\n \n const componentDef = componentRegistry[selectedLayer.type as keyof typeof componentRegistry];\n if (!componentDef) return false;\n \n // Safely check if schema has shape property (ZodObject) and children field\n const hasChildrenField = 'shape' in componentDef.schema && \n componentDef.schema.shape && \n componentDef.schema.shape.children !== undefined;\n \n return (\n hasLayerChildren(selectedLayer) &&\n hasChildrenField\n );\n }, [selectedLayer, componentRegistry]);\n\n return (\n <>\n \n \n \n\n \n {canRenderAddChild && (\n \n \n Add Component\n \n \n \n )}\n {canDuplicate && (\n \n Duplicate {isLayerAPage ? \"Page\" : \"Component\"}\n \n \n )}\n {canDelete && (\n \n Delete {isLayerAPage ? \"Page\" : \"Component\"}\n \n \n )}\n \n \n \n >\n );\n};\n", "type": "registry:ui", "target": "components/ui/ui-builder/internal/layer-menu.tsx" }, @@ -340,7 +340,7 @@ }, { "path": "lib/ui-builder/utils/variable-resolver.ts", - "content": "import { Variable } from '@/components/ui/ui-builder/types';\n\n/**\n * Checks if a value is a variable reference\n */\nexport function isVariableReference(value: any): value is { __variableRef: string } {\n return typeof value === 'object' && value !== null && '__variableRef' in value;\n}\n\n/**\n * Resolves variable references in props using provided variable values\n * @param props - The props object that may contain variable references\n * @param variables - Array of available variables\n * @param variableValues - Object mapping variable IDs to their resolved values\n * @returns Props with variable references resolved\n */\nexport function resolveVariableReferences(\n props: Record,\n variables: Variable[],\n variableValues?: Record\n): Record {\n const resolved: Record = {};\n\n for (const [key, value] of Object.entries(props)) {\n if (isVariableReference(value)) {\n const variable = variables.find(v => v.id === value.__variableRef);\n if (variable) {\n // Use provided value or fall back to default value\n resolved[key] = variableValues?.[variable.id] ?? variable.defaultValue;\n } else {\n // Variable not found, use default value or undefined\n resolved[key] = undefined;\n }\n } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\n // Recursively resolve nested objects\n resolved[key] = resolveVariableReferences(value, variables, variableValues);\n } else {\n // Regular value, keep as is\n resolved[key] = value;\n }\n }\n\n return resolved;\n}\n", + "content": "import React from 'react';\nimport { Variable, VariableReference, PropValue, isVariableReference } from '@/components/ui/ui-builder/types';\n\n/**\n * Resolves variable references in props using provided variable values\n * @param props - The props object that may contain variable references\n * @param variables - Array of available variables\n * @param variableValues - Object mapping variable IDs to their resolved values\n * @returns Props with variable references resolved\n */\nexport function resolveVariableReferences(\n props: Record,\n variables: Variable[],\n variableValues?: Record\n): Record {\n const resolved: Record = {};\n\n for (const [key, value] of Object.entries(props)) {\n if (isVariableReference(value)) {\n const variable = variables.find(v => v.id === value.__variableRef);\n if (variable) {\n // Use provided value or fall back to default value\n resolved[key] = variableValues?.[variable.id] ?? variable.defaultValue;\n } else {\n // Variable not found, use default value or undefined\n resolved[key] = undefined;\n }\n } else if (typeof value === 'object' && value !== null && !Array.isArray(value) && !React.isValidElement(value)) {\n // Recursively resolve nested objects (but not React elements or arrays)\n resolved[key] = resolveVariableReferences(value as Record, variables, variableValues);\n } else {\n // Regular value, keep as is\n resolved[key] = value;\n }\n }\n\n return resolved;\n}\n\n// Export the isVariableReference function for backward compatibility\nexport { isVariableReference };\n", "type": "registry:lib", "target": "lib/ui-builder/utils/variable-resolver.ts" }, @@ -364,7 +364,7 @@ }, { "path": "lib/ui-builder/store/layer-store.ts", - "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { create, StateCreator } from 'zustand';\nimport { persist, createJSONStorage, StorageValue } from 'zustand/middleware'\nimport { produce } from 'immer';\nimport { temporal } from 'zundo';\nimport isDeepEqual from 'fast-deep-equal';\n\nimport { visitLayer, addLayer, hasLayerChildren, findLayerRecursive, createId, countLayers, duplicateWithNewIdsAndName, findAllParentLayersRecursive, migrateV1ToV2, migrateV2ToV3 } from '@/lib/ui-builder/store/layer-utils';\nimport { getDefaultProps } from '@/lib/ui-builder/store/schema-utils';\nimport { useEditorStore } from '@/lib/ui-builder/store/editor-store';\nimport { ComponentLayer, Variable } from '@/components/ui/ui-builder/types';\n\nconst DEFAULT_PAGE_PROPS = {\n className: \"p-4 flex flex-col gap-2\",\n};\n\nexport interface LayerStore {\n pages: ComponentLayer[];\n selectedLayerId: string | null;\n selectedPageId: string;\n variables: Variable[];\n immutableBindings: Record>; // layerId -> propName -> isImmutable\n initialize: (pages: ComponentLayer[], selectedPageId?: string, selectedLayerId?: string, variables?: Variable[]) => void;\n addComponentLayer: (layerType: string, parentId: string, parentPosition?: number) => void;\n addPageLayer: (pageId: string) => void;\n duplicateLayer: (layerId: string, parentId?: string) => void;\n removeLayer: (layerId: string) => void;\n updateLayer: (layerId: string, newProps: Record, layerRest?: Partial>) => void;\n selectLayer: (layerId: string) => void;\n selectPage: (pageId: string) => void;\n findLayerById: (layerId: string | null) => ComponentLayer | undefined;\n findLayersForPageId: (pageId: string) => ComponentLayer[];\n isLayerAPage: (layerId: string) => boolean;\n\n addVariable: (name: string, type: Variable['type'], defaultValue: any) => void;\n updateVariable: (variableId: string, updates: Partial>) => void;\n removeVariable: (variableId: string) => void;\n bindPropToVariable: (layerId: string, propName: string, variableId: string) => void;\n unbindPropFromVariable: (layerId: string, propName: string) => void;\n isBindingImmutable: (layerId: string, propName: string) => boolean;\n setImmutableBinding: (layerId: string, propName: string, isImmutable: boolean) => void; // Test helper\n}\n\nconst store: StateCreator = (set, get) => (\n {\n // Default to a single empty page\n pages: [\n {\n id: '1',\n type: 'div',\n name: 'Page 1',\n props: DEFAULT_PAGE_PROPS,\n children: [],\n }\n ],\n\n // Variables available for binding\n variables: [],\n // Track immutable bindings: layerId -> propName -> isImmutable\n immutableBindings: {},\n selectedLayerId: null,\n selectedPageId: '1',\n initialize: (pages: ComponentLayer[], selectedPageId?: string, selectedLayerId?: string, variables?: Variable[]) => {\n set({ pages, selectedPageId: selectedPageId || pages[0].id, selectedLayerId: selectedLayerId || null, variables: variables || [] });\n },\n findLayerById: (layerId: string | null) => {\n const { selectedPageId, findLayersForPageId, pages } = get();\n if (!layerId) return undefined;\n if (layerId === selectedPageId) {\n return pages.find(page => page.id === selectedPageId);\n }\n const layers = findLayersForPageId(selectedPageId);\n if (!layers) return undefined;\n return findLayerRecursive(layers, layerId);\n },\n findLayersForPageId: (pageId: string) => {\n const { pages } = get();\n const page = pages.find(page => page.id === pageId);\n if(page && hasLayerChildren(page)) {\n return page?.children || [];\n }\n return [];\n },\n\n isLayerAPage: (layerId: string) => {\n const { pages } = get();\n return pages.some(page => page.id === layerId);\n },\n\n addComponentLayer: (layerType: string, parentId: string, parentPosition?: number) => set(produce((state: LayerStore) => {\n const { registry } = useEditorStore.getState();\n const defaultProps = getDefaultProps(registry[layerType].schema);\n const defaultChildrenRaw = registry[layerType].defaultChildren;\n const defaultChildren = typeof defaultChildrenRaw === \"string\" ? defaultChildrenRaw : (defaultChildrenRaw?.map(child => duplicateWithNewIdsAndName(child, false)) || []);\n const defaultVariableBindings = registry[layerType].defaultVariableBindings || [];\n\n const initialProps = Object.entries(defaultProps).reduce((acc, [key, propDef]) => {\n if (key !== \"children\") {\n acc[key] = propDef;\n }\n return acc;\n }, {} as Record);\n\n const newLayer: ComponentLayer = {\n id: createId(),\n type: layerType,\n name: layerType,\n props: initialProps,\n children: defaultChildren,\n };\n\n // Apply default variable bindings\n for (const binding of defaultVariableBindings) {\n const variable = state.variables.find(v => v.id === binding.variableId);\n if (variable) {\n // Set the variable reference in the props\n newLayer.props[binding.propName] = { __variableRef: binding.variableId };\n \n // Track immutable bindings\n if (binding.immutable) {\n if (!state.immutableBindings[newLayer.id]) {\n state.immutableBindings[newLayer.id] = {};\n }\n state.immutableBindings[newLayer.id][binding.propName] = true;\n }\n }\n }\n\n // Traverse and update the pages to add the new layer\n const updatedPages = addLayer(state.pages, newLayer, parentId, parentPosition);\n // Directly mutate the state instead of returning a new object\n state.pages = updatedPages;\n })),\n\n addPageLayer: (pageName: string) => set(produce((state: LayerStore) => {\n const newPage: ComponentLayer = {\n id: createId(),\n type: 'div',\n name: pageName,\n props: DEFAULT_PAGE_PROPS,\n children: [],\n };\n return {\n pages: [...state.pages, newPage],\n selectedPageId: newPage.id,\n selectedLayerId: newPage.id,\n };\n })),\n\n duplicateLayer: (layerId: string) => set(produce((state: LayerStore) => {\n let layerToDuplicate: ComponentLayer | undefined;\n let parentId: string | undefined;\n let parentPosition: number | undefined;\n\n // Find the layer to duplicate\n state.pages.forEach((page) =>\n visitLayer(page, null, (layer, parent) => {\n if (layer.id === layerId) {\n layerToDuplicate = layer;\n parentId = parent?.id;\n if (parent && hasLayerChildren(parent)) {\n parentPosition = parent.children.indexOf(layer) + 1;\n }\n }\n return layer;\n })\n );\n if (!layerToDuplicate) {\n console.warn(`Layer with ID ${ layerId } not found.`);\n return;\n }\n\n const isNewLayerAPage = state.pages.some(page => page.id === layerId);\n\n const newLayer = duplicateWithNewIdsAndName(layerToDuplicate, true);\n\n if (isNewLayerAPage) {\n return {\n ...state,\n pages: [...state.pages, newLayer],\n selectedPageId: newLayer.id,\n };\n }\n\n //else add it as a child of the parent\n\n const updatedPages = addLayer(state.pages, newLayer, parentId, parentPosition);\n\n // Insert the duplicated layer\n return {\n ...state,\n pages: updatedPages\n };\n })),\n\n removeLayer: (layerId: string) => set(produce((state: LayerStore) => {\n const { selectedLayerId, pages } = get();\n\n let newSelectedLayerId = selectedLayerId;\n\n const isPage = state.pages.some(page => page.id === layerId);\n if (isPage && pages.length > 1) {\n const newPages = state.pages.filter(page => page.id !== layerId);\n return {\n ...state,\n pages: newPages,\n selectedPageId: newPages[0].id,\n };\n }\n\n // Traverse and update the pages to remove the specified layer\n const updatedPages = pages.map((page) =>\n visitLayer(page, null, (layer) => {\n\n if (hasLayerChildren(layer)) {\n\n // Remove the layer by filtering it out from the children\n const updatedChildren = layer.children.filter((child) => child.id !== layerId);\n return { ...layer, children: updatedChildren };\n }\n\n return layer;\n })\n );\n\n if (selectedLayerId === layerId) {\n // If the removed layer was selected, deselect it \n newSelectedLayerId = null;\n }\n return {\n ...state,\n selectedLayerId: newSelectedLayerId,\n pages: updatedPages,\n };\n })),\n\n updateLayer: (layerId: string, newProps: ComponentLayer['props'], layerRest?: Partial>) => set(\n produce((state: LayerStore) => {\n const { selectedPageId, findLayersForPageId, pages } = get();\n\n const pageExists = pages.some(page => page.id === selectedPageId);\n if (!pageExists) {\n console.warn(`No layers found for page ID: ${ selectedPageId }`);\n return state;\n }\n\n if (layerId === selectedPageId) {\n const updatedPages = pages.map(page =>\n page.id === selectedPageId\n ? { ...page, props: { ...page.props, ...newProps }, ...(layerRest || {}) }\n : page\n );\n return { ...state, pages: updatedPages };\n }\n\n const layers = findLayersForPageId(selectedPageId);\n\n\n // Visitor function to update layer properties\n const visitor = (layer: ComponentLayer): ComponentLayer => {\n if (layer.id === layerId) {\n return {\n ...layer,\n ...(layerRest || {}),\n props: { ...layer.props, ...newProps },\n } as ComponentLayer\n }\n return layer;\n };\n\n // Apply the visitor to update layers\n const updatedLayers = layers.map(layer => visitLayer(layer, null, visitor));\n\n const isUnchanged = updatedLayers.every((layer, index) => layer === layers[index]);\n\n if (isUnchanged) {\n console.warn(`Layer with ID ${ layerId } was not found.`);\n return state;\n }\n\n // Update the state with the modified layers\n const updatedPages = state.pages.map(page =>\n page.id === selectedPageId ? { ...page, children: updatedLayers } : page\n );\n\n return { ...state, pages: updatedPages };\n })\n ),\n\n\n selectLayer: (layerId: string) => set(produce((state: LayerStore) => {\n const { selectedPageId, findLayersForPageId } = get();\n const layers = findLayersForPageId(selectedPageId);\n if(selectedPageId === layerId) {\n return {\n selectedLayerId: layerId\n };\n }\n if (!layers) return state;\n const layer = findLayerRecursive(layers, layerId);\n if (layer) {\n return {\n selectedLayerId: layer.id\n };\n }\n return {};\n })),\n\n selectPage: (pageId: string) => set(produce((state: LayerStore) => {\n const page = state.pages.find(page => page.id === pageId);\n if (!page) return state;\n return {\n selectedPageId: pageId\n };\n })),\n\n // Add a new variable\n addVariable: (name, type, defaultValue) => set(produce((state: LayerStore) => {\n state.variables.push({ id: createId(), name, type, defaultValue });\n })),\n\n // Update an existing variable\n updateVariable: (variableId, updates) => set(produce((state: LayerStore) => {\n const v = state.variables.find(v => v.id === variableId);\n if (v) Object.assign(v, updates);\n })),\n\n // Remove a variable\n removeVariable: (variableId) => set(produce((state: LayerStore) => {\n state.variables = state.variables.filter(v => v.id !== variableId);\n \n // Remove any references to the variable in the layers and set default value from schema\n const { registry } = useEditorStore.getState();\n \n // Helper function to clean variable references from props\n const cleanVariableReferences = (layer: ComponentLayer): ComponentLayer => {\n const updatedProps = { ...layer.props };\n let hasChanges = false;\n \n // Check each prop for variable references\n Object.entries(updatedProps).forEach(([propName, propValue]) => {\n if (propValue && typeof propValue === 'object' && propValue.__variableRef === variableId) {\n // This prop references the variable being removed\n // Get the default value from the schema\n const layerSchema = registry[layer.type]?.schema;\n if (layerSchema && layerSchema.shape && layerSchema.shape[propName]) {\n const defaultProps = getDefaultProps(layerSchema);\n updatedProps[propName] = defaultProps[propName];\n hasChanges = true;\n } else {\n // Fallback: remove the prop entirely if no schema default\n delete updatedProps[propName];\n hasChanges = true;\n }\n }\n });\n \n return hasChanges ? { ...layer, props: updatedProps } : layer;\n };\n \n // Update all pages and their layers\n state.pages = state.pages.map(page => \n visitLayer(page, null, cleanVariableReferences)\n );\n })),\n\n // Bind a component prop to a variable reference\n bindPropToVariable: (layerId, propName, variableId) => {\n // Store a special object as prop to indicate binding\n get().updateLayer(layerId, { [propName]: { __variableRef: variableId } });\n },\n\n // Unbind a component prop from a variable reference and set default value from schema\n unbindPropFromVariable: (layerId, propName) => {\n // Check if the binding is immutable\n if (get().isBindingImmutable(layerId, propName)) {\n console.warn(`Cannot unbind immutable variable binding for ${propName} on layer ${layerId}`);\n return;\n }\n\n const { registry } = useEditorStore.getState();\n const layer = get().findLayerById(layerId);\n \n if (!layer) {\n console.warn(`Layer with ID ${layerId} not found.`);\n return;\n }\n\n // Get the default value from the schema\n const layerSchema = registry[layer.type]?.schema;\n let defaultValue: any = undefined;\n \n if (layerSchema && layerSchema.shape && layerSchema.shape[propName]) {\n const defaultProps = getDefaultProps(layerSchema);\n defaultValue = defaultProps[propName];\n }\n \n // If no default value found in schema, use empty string for string-like props\n if (defaultValue === undefined) {\n defaultValue = \"\";\n }\n\n get().updateLayer(layerId, { [propName]: defaultValue });\n },\n\n // Check if a binding is immutable\n isBindingImmutable: (layerId: string, propName: string) => {\n const { immutableBindings } = get();\n return immutableBindings[layerId]?.[propName] === true;\n },\n\n // Test helper\n setImmutableBinding: (layerId: string, propName: string, isImmutable: boolean) => {\n set(produce((state: LayerStore) => {\n if (!state.immutableBindings[layerId]) {\n state.immutableBindings[layerId] = {};\n }\n state.immutableBindings[layerId][propName] = isImmutable;\n }));\n },\n }\n)\n\n// Custom storage adapter (mimics localStorage API for createJSONStorage)\nconst conditionalLocalStorage = {\n getItem: (name: string): Promise => {\n const { persistLayerStoreConfig } = useEditorStore.getState();\n if (!persistLayerStoreConfig) {\n return Promise.resolve(null);\n }\n const value = localStorage.getItem(name);\n return Promise.resolve(value);\n },\n setItem: (name: string, value: string): Promise => {\n const { persistLayerStoreConfig } = useEditorStore.getState();\n if (!persistLayerStoreConfig) {\n return Promise.resolve();\n }\n localStorage.setItem(name, value);\n return Promise.resolve();\n },\n removeItem: (name: string): Promise => {\n const { persistLayerStoreConfig } = useEditorStore.getState();\n if (!persistLayerStoreConfig) {\n return Promise.resolve();\n }\n localStorage.removeItem(name);\n return Promise.resolve();\n },\n};\n\nconst useLayerStore = create(persist(temporal(store,\n {\n equality: (pastState, currentState) =>\n isDeepEqual(pastState, currentState),\n }\n), {\n name: \"layer-store\",\n version: 5,\n storage: createJSONStorage(() => conditionalLocalStorage),\n migrate: (persistedState: unknown, version: number) => {\n /* istanbul ignore if*/\n if (version === 1) {\n return migrateV1ToV2(persistedState as LayerStore);\n } else if (version === 2) {\n return migrateV2ToV3(persistedState as LayerStore);\n } else if (version === 3) {\n // New variable support: ensure variables array exists\n return { ...(persistedState as LayerStore), variables: [] as Variable[], immutableBindings: {} } as LayerStore;\n } else if (version === 4) {\n // New immutable bindings support: ensure immutableBindings object exists\n return { ...(persistedState as LayerStore), immutableBindings: {} } as LayerStore;\n }\n return persistedState;\n }\n}))\n\nexport { useLayerStore, countLayers, findAllParentLayersRecursive };\n", + "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { create, StateCreator } from 'zustand';\nimport { persist, createJSONStorage, StorageValue } from 'zustand/middleware'\nimport { produce } from 'immer';\nimport { temporal } from 'zundo';\nimport isDeepEqual from 'fast-deep-equal';\n\nimport { visitLayer, addLayer, hasLayerChildren, findLayerRecursive, createId, countLayers, duplicateWithNewIdsAndName, findAllParentLayersRecursive, migrateV1ToV2, migrateV2ToV3 } from '@/lib/ui-builder/store/layer-utils';\nimport { getDefaultProps } from '@/lib/ui-builder/store/schema-utils';\nimport { useEditorStore } from '@/lib/ui-builder/store/editor-store';\nimport { ComponentLayer, Variable, PropValue, VariableValueType, isVariableReference } from '@/components/ui/ui-builder/types';\n\nconst DEFAULT_PAGE_PROPS = {\n className: \"p-4 flex flex-col gap-2\",\n};\n\nexport interface LayerStore {\n pages: ComponentLayer[];\n selectedLayerId: string | null;\n selectedPageId: string;\n variables: Variable[];\n immutableBindings: Record>; // layerId -> propName -> isImmutable\n initialize: (pages: ComponentLayer[], selectedPageId?: string, selectedLayerId?: string, variables?: Variable[]) => void;\n addComponentLayer: (layerType: string, parentId: string, parentPosition?: number) => void;\n addPageLayer: (pageId: string) => void;\n duplicateLayer: (layerId: string, parentId?: string) => void;\n removeLayer: (layerId: string) => void;\n updateLayer: (layerId: string, newProps: Record, layerRest?: Partial>) => void;\n selectLayer: (layerId: string) => void;\n selectPage: (pageId: string) => void;\n findLayerById: (layerId: string | null) => ComponentLayer | undefined;\n findLayersForPageId: (pageId: string) => ComponentLayer[];\n isLayerAPage: (layerId: string) => boolean;\n\n addVariable: (name: string, type: T, defaultValue: Variable['defaultValue']) => void;\n updateVariable: (variableId: string, updates: Partial>) => void;\n removeVariable: (variableId: string) => void;\n bindPropToVariable: (layerId: string, propName: string, variableId: string) => void;\n unbindPropFromVariable: (layerId: string, propName: string) => void;\n isBindingImmutable: (layerId: string, propName: string) => boolean;\n setImmutableBinding: (layerId: string, propName: string, isImmutable: boolean) => void; // Test helper\n}\n\nconst store: StateCreator = (set, get) => (\n {\n // Default to a single empty page\n pages: [\n {\n id: '1',\n type: 'div',\n name: 'Page 1',\n props: DEFAULT_PAGE_PROPS,\n children: [],\n }\n ],\n\n // Variables available for binding\n variables: [],\n // Track immutable bindings: layerId -> propName -> isImmutable\n immutableBindings: {},\n selectedLayerId: null,\n selectedPageId: '1',\n initialize: (pages: ComponentLayer[], selectedPageId?: string, selectedLayerId?: string, variables?: Variable[]) => {\n set({ pages, selectedPageId: selectedPageId || pages[0].id, selectedLayerId: selectedLayerId || null, variables: variables || [] });\n },\n findLayerById: (layerId: string | null) => {\n const { selectedPageId, findLayersForPageId, pages } = get();\n if (!layerId) return undefined;\n if (layerId === selectedPageId) {\n return pages.find(page => page.id === selectedPageId);\n }\n const layers = findLayersForPageId(selectedPageId);\n if (!layers) return undefined;\n return findLayerRecursive(layers, layerId);\n },\n findLayersForPageId: (pageId: string) => {\n const { pages } = get();\n const page = pages.find(page => page.id === pageId);\n if(page && hasLayerChildren(page)) {\n return page?.children || [];\n }\n return [];\n },\n\n isLayerAPage: (layerId: string) => {\n const { pages } = get();\n return pages.some(page => page.id === layerId);\n },\n\n addComponentLayer: (layerType: string, parentId: string, parentPosition?: number) => set(produce((state: LayerStore) => {\n const { registry } = useEditorStore.getState();\n const schema = registry[layerType].schema;\n \n // Safely check if schema has shape property (ZodObject)\n const defaultProps = 'shape' in schema && schema.shape ? getDefaultProps(schema as any) : {};\n const defaultChildrenRaw = registry[layerType].defaultChildren;\n const defaultChildren = typeof defaultChildrenRaw === \"string\" ? defaultChildrenRaw : (defaultChildrenRaw?.map(child => duplicateWithNewIdsAndName(child, false)) || []);\n const defaultVariableBindings = registry[layerType].defaultVariableBindings || [];\n\n const initialProps = Object.entries(defaultProps).reduce((acc, [key, propDef]) => {\n if (key !== \"children\") {\n acc[key] = propDef;\n }\n return acc;\n }, {} as Record);\n\n const newLayer: ComponentLayer = {\n id: createId(),\n type: layerType,\n name: layerType,\n props: initialProps,\n children: defaultChildren,\n };\n\n // Apply default variable bindings\n for (const binding of defaultVariableBindings) {\n const variable = state.variables.find(v => v.id === binding.variableId);\n if (variable) {\n // Set the variable reference in the props\n newLayer.props[binding.propName] = { __variableRef: binding.variableId };\n \n // Track immutable bindings\n if (binding.immutable) {\n if (!state.immutableBindings[newLayer.id]) {\n state.immutableBindings[newLayer.id] = {};\n }\n state.immutableBindings[newLayer.id][binding.propName] = true;\n }\n }\n }\n\n // Traverse and update the pages to add the new layer\n const updatedPages = addLayer(state.pages, newLayer, parentId, parentPosition);\n // Directly mutate the state instead of returning a new object\n state.pages = updatedPages;\n })),\n\n addPageLayer: (pageName: string) => set(produce((state: LayerStore) => {\n const newPage: ComponentLayer = {\n id: createId(),\n type: 'div',\n name: pageName,\n props: DEFAULT_PAGE_PROPS,\n children: [],\n };\n return {\n pages: [...state.pages, newPage],\n selectedPageId: newPage.id,\n selectedLayerId: newPage.id,\n };\n })),\n\n duplicateLayer: (layerId: string) => set(produce((state: LayerStore) => {\n let layerToDuplicate: ComponentLayer | undefined;\n let parentId: string | undefined;\n let parentPosition: number | undefined;\n\n // Find the layer to duplicate\n state.pages.forEach((page) =>\n visitLayer(page, null, (layer, parent) => {\n if (layer.id === layerId) {\n layerToDuplicate = layer;\n parentId = parent?.id;\n if (parent && hasLayerChildren(parent)) {\n parentPosition = parent.children.indexOf(layer) + 1;\n }\n }\n return layer;\n })\n );\n if (!layerToDuplicate) {\n console.warn(`Layer with ID ${ layerId } not found.`);\n return;\n }\n\n const isNewLayerAPage = state.pages.some(page => page.id === layerId);\n\n const newLayer = duplicateWithNewIdsAndName(layerToDuplicate, true);\n\n if (isNewLayerAPage) {\n return {\n ...state,\n pages: [...state.pages, newLayer],\n selectedPageId: newLayer.id,\n };\n }\n\n //else add it as a child of the parent\n\n const updatedPages = addLayer(state.pages, newLayer, parentId, parentPosition);\n\n // Insert the duplicated layer\n return {\n ...state,\n pages: updatedPages\n };\n })),\n\n removeLayer: (layerId: string) => set(produce((state: LayerStore) => {\n const { selectedLayerId, pages } = get();\n\n let newSelectedLayerId = selectedLayerId;\n\n const isPage = state.pages.some(page => page.id === layerId);\n if (isPage && pages.length > 1) {\n const newPages = state.pages.filter(page => page.id !== layerId);\n return {\n ...state,\n pages: newPages,\n selectedPageId: newPages[0].id,\n };\n }\n\n // Traverse and update the pages to remove the specified layer\n const updatedPages = pages.map((page) =>\n visitLayer(page, null, (layer) => {\n\n if (hasLayerChildren(layer)) {\n\n // Remove the layer by filtering it out from the children\n const updatedChildren = layer.children.filter((child) => child.id !== layerId);\n return { ...layer, children: updatedChildren };\n }\n\n return layer;\n })\n );\n\n if (selectedLayerId === layerId) {\n // If the removed layer was selected, deselect it \n newSelectedLayerId = null;\n }\n return {\n ...state,\n selectedLayerId: newSelectedLayerId,\n pages: updatedPages,\n };\n })),\n\n updateLayer: (layerId: string, newProps: ComponentLayer['props'], layerRest?: Partial>) => set(\n produce((state: LayerStore) => {\n const { selectedPageId, findLayersForPageId, pages } = get();\n\n const pageExists = pages.some(page => page.id === selectedPageId);\n if (!pageExists) {\n console.warn(`No layers found for page ID: ${ selectedPageId }`);\n return state;\n }\n\n if (layerId === selectedPageId) {\n const updatedPages = pages.map(page =>\n page.id === selectedPageId\n ? { ...page, props: { ...page.props, ...newProps }, ...(layerRest || {}) }\n : page\n );\n return { ...state, pages: updatedPages };\n }\n\n const layers = findLayersForPageId(selectedPageId);\n\n\n // Visitor function to update layer properties\n const visitor = (layer: ComponentLayer): ComponentLayer => {\n if (layer.id === layerId) {\n return {\n ...layer,\n ...(layerRest || {}),\n props: { ...layer.props, ...newProps },\n } as ComponentLayer\n }\n return layer;\n };\n\n // Apply the visitor to update layers\n const updatedLayers = layers.map(layer => visitLayer(layer, null, visitor));\n\n const isUnchanged = updatedLayers.every((layer, index) => layer === layers[index]);\n\n if (isUnchanged) {\n console.warn(`Layer with ID ${ layerId } was not found.`);\n return state;\n }\n\n // Update the state with the modified layers\n const updatedPages = state.pages.map(page =>\n page.id === selectedPageId ? { ...page, children: updatedLayers } : page\n );\n\n return { ...state, pages: updatedPages };\n })\n ),\n\n\n selectLayer: (layerId: string) => set(produce((state: LayerStore) => {\n const { selectedPageId, findLayersForPageId } = get();\n const layers = findLayersForPageId(selectedPageId);\n if(selectedPageId === layerId) {\n return {\n selectedLayerId: layerId\n };\n }\n if (!layers) return state;\n const layer = findLayerRecursive(layers, layerId);\n if (layer) {\n return {\n selectedLayerId: layer.id\n };\n }\n return {};\n })),\n\n selectPage: (pageId: string) => set(produce((state: LayerStore) => {\n const page = state.pages.find(page => page.id === pageId);\n if (!page) return state;\n return {\n selectedPageId: pageId\n };\n })),\n\n // Add a new variable\n addVariable: (name, type, defaultValue) => set(produce((state: LayerStore) => {\n state.variables.push({ id: createId(), name, type, defaultValue });\n })),\n\n // Update an existing variable\n updateVariable: (variableId, updates) => set(produce((state: LayerStore) => {\n const v = state.variables.find(v => v.id === variableId);\n if (v) Object.assign(v, updates);\n })),\n\n // Remove a variable\n removeVariable: (variableId) => set(produce((state: LayerStore) => {\n state.variables = state.variables.filter(v => v.id !== variableId);\n \n // Remove any references to the variable in the layers and set default value from schema\n const { registry } = useEditorStore.getState();\n \n // Helper function to clean variable references from props\n const cleanVariableReferences = (layer: ComponentLayer): ComponentLayer => {\n const updatedProps = { ...layer.props };\n let hasChanges = false;\n \n // Check each prop for variable references\n Object.entries(updatedProps).forEach(([propName, propValue]) => {\n if (isVariableReference(propValue) && propValue.__variableRef === variableId) {\n // This prop references the variable being removed\n // Get the default value from the schema\n const layerSchema = registry[layer.type]?.schema;\n if (layerSchema && 'shape' in layerSchema && layerSchema.shape && layerSchema.shape[propName]) {\n const defaultProps = getDefaultProps(layerSchema as any);\n updatedProps[propName] = defaultProps[propName];\n hasChanges = true;\n } else {\n // Fallback: remove the prop entirely if no schema default\n delete updatedProps[propName];\n hasChanges = true;\n }\n }\n });\n \n return hasChanges ? { ...layer, props: updatedProps } : layer;\n };\n \n // Update all pages and their layers\n state.pages = state.pages.map(page => \n visitLayer(page, null, cleanVariableReferences)\n );\n })),\n\n // Bind a component prop to a variable reference\n bindPropToVariable: (layerId, propName, variableId) => {\n // Store a special object as prop to indicate binding\n get().updateLayer(layerId, { [propName]: { __variableRef: variableId } });\n },\n\n // Unbind a component prop from a variable reference and set default value from schema\n unbindPropFromVariable: (layerId, propName) => {\n // Check if the binding is immutable\n if (get().isBindingImmutable(layerId, propName)) {\n console.warn(`Cannot unbind immutable variable binding for ${propName} on layer ${layerId}`);\n return;\n }\n\n const { registry } = useEditorStore.getState();\n const layer = get().findLayerById(layerId);\n \n if (!layer) {\n console.warn(`Layer with ID ${layerId} not found.`);\n return;\n }\n\n // Get the default value from the schema\n const layerSchema = registry[layer.type]?.schema;\n let defaultValue: any = undefined;\n \n if (layerSchema && 'shape' in layerSchema && layerSchema.shape && layerSchema.shape[propName]) {\n const defaultProps = getDefaultProps(layerSchema as any);\n defaultValue = defaultProps[propName];\n }\n \n // If no default value found in schema, use empty string for string-like props\n if (defaultValue === undefined) {\n defaultValue = \"\";\n }\n\n get().updateLayer(layerId, { [propName]: defaultValue });\n },\n\n // Check if a binding is immutable\n isBindingImmutable: (layerId: string, propName: string) => {\n const { immutableBindings } = get();\n return immutableBindings[layerId]?.[propName] === true;\n },\n\n // Test helper\n setImmutableBinding: (layerId: string, propName: string, isImmutable: boolean) => {\n set(produce((state: LayerStore) => {\n if (!state.immutableBindings[layerId]) {\n state.immutableBindings[layerId] = {};\n }\n state.immutableBindings[layerId][propName] = isImmutable;\n }));\n },\n }\n)\n\n// Custom storage adapter (mimics localStorage API for createJSONStorage)\nconst conditionalLocalStorage = {\n getItem: (name: string): Promise => {\n const { persistLayerStoreConfig } = useEditorStore.getState();\n if (!persistLayerStoreConfig) {\n return Promise.resolve(null);\n }\n const value = localStorage.getItem(name);\n return Promise.resolve(value);\n },\n setItem: (name: string, value: string): Promise => {\n const { persistLayerStoreConfig } = useEditorStore.getState();\n if (!persistLayerStoreConfig) {\n return Promise.resolve();\n }\n localStorage.setItem(name, value);\n return Promise.resolve();\n },\n removeItem: (name: string): Promise => {\n const { persistLayerStoreConfig } = useEditorStore.getState();\n if (!persistLayerStoreConfig) {\n return Promise.resolve();\n }\n localStorage.removeItem(name);\n return Promise.resolve();\n },\n};\n\nconst useLayerStore = create(persist(temporal(store,\n {\n equality: (pastState, currentState) =>\n isDeepEqual(pastState, currentState),\n }\n), {\n name: \"layer-store\",\n version: 5,\n storage: createJSONStorage(() => conditionalLocalStorage),\n migrate: (persistedState: unknown, version: number) => {\n /* istanbul ignore if*/\n if (version === 1) {\n return migrateV1ToV2(persistedState as LayerStore);\n } else if (version === 2) {\n return migrateV2ToV3(persistedState as LayerStore);\n } else if (version === 3) {\n // New variable support: ensure variables array exists\n return { ...(persistedState as LayerStore), variables: [] as Variable[], immutableBindings: {} } as LayerStore;\n } else if (version === 4) {\n // New immutable bindings support: ensure immutableBindings object exists\n return { ...(persistedState as LayerStore), immutableBindings: {} } as LayerStore;\n }\n return persistedState;\n }\n}))\n\nexport { useLayerStore, countLayers, findAllParentLayersRecursive };\n", "type": "registry:lib", "target": "lib/ui-builder/store/layer-store.ts" }, @@ -388,7 +388,7 @@ }, { "path": "lib/ui-builder/registry/form-field-overrides.tsx", - "content": "import {\n FormControl,\n FormDescription,\n FormItem,\n FormLabel,\n} from \"@/components/ui/form\";\nimport { ChildrenSearchableSelect } from \"@/components/ui/ui-builder/internal/children-searchable-select\";\nimport {\n AutoFormInputComponentProps,\n ComponentLayer,\n FieldConfigFunction,\n} from \"@/components/ui/ui-builder/types\";\nimport IconNameField from \"@/components/ui/ui-builder/internal/iconname-field\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { MinimalTiptapEditor } from \"@/components/ui/minimal-tiptap\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { useLayerStore } from \"../store/layer-store\";\nimport { isVariableReference } from \"../utils/variable-resolver\";\nimport { Link, LockKeyhole, Unlink } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { Input } from \"@/components/ui/input\";\nimport { useEditorStore } from \"../store/editor-store\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport BreakpointClassNameControl from \"@/components/ui/ui-builder/internal/classname-control\";\nimport { Label } from \"@/components/ui/label\";\nimport { Badge } from \"@/components/ui/badge\";\n\nexport const classNameFieldOverrides: FieldConfigFunction = (\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n layer,\n allowBinding = false\n) => {\n return {\n fieldType: ({\n label,\n isRequired,\n field,\n fieldProps,\n fieldConfigItem,\n }: AutoFormInputComponentProps) => (\n \n \n \n ),\n };\n};\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport const childrenFieldOverrides: FieldConfigFunction = (\n layer,\n allowBinding = false\n) => {\n return {\n fieldType: ({\n label,\n isRequired,\n fieldConfigItem,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n \n \n ),\n };\n};\n\nexport const iconNameFieldOverrides: FieldConfigFunction = (layer) => {\n return {\n fieldType: ({\n label,\n isRequired,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n ),\n };\n};\n\nexport const childrenAsTextareaFieldOverrides: FieldConfigFunction = (\n layer,\n allowBinding = false\n) => {\n return {\n fieldType: ({\n label,\n isRequired,\n fieldConfigItem,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n \n \n ),\n };\n};\n\nexport const childrenAsTipTapFieldOverrides: FieldConfigFunction = (\n layer,\n allowBinding = false\n) => {\n return {\n fieldType: ({\n label,\n isRequired,\n fieldConfigItem,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n {\n console.log({ content });\n //if string call field.onChange\n if (typeof content === \"string\") {\n field.onChange(content);\n } else {\n console.warn(\"Tiptap content is not a string\");\n }\n }}\n {...fieldProps}\n />\n \n ),\n };\n};\n\nexport const commonFieldOverrides = (allowBinding = false) => {\n return {\n className: (layer: ComponentLayer) => classNameFieldOverrides(layer),\n children: (layer: ComponentLayer) => childrenFieldOverrides(layer),\n };\n};\n\nexport const commonVariableRenderParentOverrides = (propName: string) => {\n return {\n renderParent: ({ children }: { children: React.ReactNode }) => (\n {children}\n ),\n };\n};\n\nexport const textInputFieldOverrides = (\n layer: ComponentLayer,\n allowVariableBinding = false,\n propName: string\n) => {\n return {\n renderParent: allowVariableBinding\n ? ({ children }: { children: React.ReactNode }) => (\n \n {children}\n \n )\n : undefined,\n fieldType: ({\n label,\n isRequired,\n fieldConfigItem,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n field.onChange(e.target.value)}\n {...fieldProps}\n />\n \n ),\n };\n};\n\nexport const renderParentWithVariableBinding = ({\n children,\n}: {\n children: React.ReactNode;\n}) => (\n \n {children}\n YO!\n \n);\n\nexport function VariableBindingWrapper({\n propName,\n children,\n}: {\n propName: string;\n children: React.ReactNode;\n}) {\n const variables = useLayerStore((state) => state.variables);\n const selectedLayerId = useLayerStore((state) => state.selectedLayerId);\n const findLayerById = useLayerStore((state) => state.findLayerById);\n const isBindingImmutable = useLayerStore((state) => state.isBindingImmutable);\n const incrementRevision = useEditorStore((state) => state.incrementRevision);\n const unbindPropFromVariable = useLayerStore(\n (state) => state.unbindPropFromVariable\n );\n const bindPropToVariable = useLayerStore((state) => state.bindPropToVariable);\n\n const selectedLayer = findLayerById(selectedLayerId);\n\n // If variable binding is not allowed or no propName provided, just render the form wrapper\n if (!selectedLayer) {\n return <>{children}>;\n }\n\n const currentValue = selectedLayer.props[propName];\n const isCurrentlyBound = isVariableReference(currentValue);\n const boundVariable = isCurrentlyBound\n ? variables.find((v) => v.id === currentValue.__variableRef)\n : null;\n const isImmutable = isBindingImmutable(selectedLayer.id, propName);\n\n const handleBindToVariable = (variableId: string) => {\n bindPropToVariable(selectedLayer.id, propName, variableId);\n incrementRevision();\n };\n\n // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop\n const handleUnbind = () => {\n // Use the new unbind function which sets default value from schema\n unbindPropFromVariable(selectedLayer.id, propName);\n incrementRevision();\n };\n\n return (\n \n {isCurrentlyBound && boundVariable ? (\n // Bound state - show variable info and unbind button\n \n {propName.charAt(0).toUpperCase() + propName.slice(1)}\n \n \n \n \n \n \n \n {boundVariable.name}\n \n {boundVariable.type}\n \n {isImmutable && (\n \n \n \n )}\n \n \n {String(boundVariable.defaultValue)}\n \n \n \n \n \n {!isImmutable && (\n \n \n \n \n \n \n Unbind Variable\n \n )}\n \n \n ) : (\n // Unbound state - show normal field with bind button\n <>\n {children}\n \n \n \n \n \n \n \n \n \n \n Bind Variable\n \n \n \n Bind to Variable\n \n {variables.length > 0 ? (\n variables.map((variable) => (\n handleBindToVariable(variable.id)}\n className=\"flex flex-col items-start p-3\"\n >\n \n \n \n \n {variable.name}\n \n {variable.type}\n \n \n \n {String(variable.defaultValue)}\n \n \n \n \n ))\n ) : (\n \n No variables defined\n \n )}\n \n \n \n >\n )}\n \n );\n}\n\nexport function FormFieldWrapper({\n label,\n isRequired,\n fieldConfigItem,\n children,\n}: {\n label: string;\n isRequired?: boolean;\n fieldConfigItem?: { description?: React.ReactNode };\n children: React.ReactNode;\n}) {\n return (\n \n \n {label}\n {isRequired && *}\n \n {children}\n {fieldConfigItem?.description && (\n {fieldConfigItem.description}\n )}\n \n );\n}\n", + "content": "import {\n FormControl,\n FormDescription,\n FormItem,\n FormLabel,\n} from \"@/components/ui/form\";\nimport { ChildrenSearchableSelect } from \"@/components/ui/ui-builder/internal/children-searchable-select\";\nimport {\n AutoFormInputComponentProps,\n ComponentLayer,\n FieldConfigFunction,\n} from \"@/components/ui/ui-builder/types\";\nimport IconNameField from \"@/components/ui/ui-builder/internal/iconname-field\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { MinimalTiptapEditor } from \"@/components/ui/minimal-tiptap\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { useLayerStore } from \"../store/layer-store\";\nimport { isVariableReference } from \"../utils/variable-resolver\";\nimport { Link, LockKeyhole, Unlink } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { Input } from \"@/components/ui/input\";\nimport { useEditorStore } from \"../store/editor-store\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport BreakpointClassNameControl from \"@/components/ui/ui-builder/internal/classname-control\";\nimport { Label } from \"@/components/ui/label\";\nimport { Badge } from \"@/components/ui/badge\";\n\nexport const classNameFieldOverrides: FieldConfigFunction = (\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n layer,\n allowBinding = false\n) => {\n return {\n fieldType: ({\n label,\n isRequired,\n field,\n fieldProps,\n fieldConfigItem,\n }: AutoFormInputComponentProps) => (\n \n \n \n ),\n };\n};\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport const childrenFieldOverrides: FieldConfigFunction = (\n layer,\n allowBinding = false\n) => {\n return {\n fieldType: ({\n label,\n isRequired,\n fieldConfigItem,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n \n \n ),\n };\n};\n\nexport const iconNameFieldOverrides: FieldConfigFunction = (layer) => {\n return {\n fieldType: ({\n label,\n isRequired,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n ),\n };\n};\n\nexport const childrenAsTextareaFieldOverrides: FieldConfigFunction = (\n layer,\n allowBinding = false\n) => {\n return {\n fieldType: ({\n label,\n isRequired,\n fieldConfigItem,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n \n \n ),\n };\n};\n\nexport const childrenAsTipTapFieldOverrides: FieldConfigFunction = (\n layer,\n allowBinding = false\n) => {\n return {\n fieldType: ({\n label,\n isRequired,\n fieldConfigItem,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n {\n //if string call field.onChange\n if (typeof content === \"string\") {\n field.onChange(content);\n } else {\n console.warn(\"Tiptap content is not a string\");\n }\n }}\n {...fieldProps}\n />\n \n ),\n };\n};\n\nexport const commonFieldOverrides = (allowBinding = false) => {\n return {\n className: (layer: ComponentLayer) => classNameFieldOverrides(layer),\n children: (layer: ComponentLayer) => childrenFieldOverrides(layer),\n };\n};\n\nexport const commonVariableRenderParentOverrides = (propName: string) => {\n return {\n renderParent: ({ children }: { children: React.ReactNode }) => (\n {children}\n ),\n };\n};\n\nexport const textInputFieldOverrides = (\n layer: ComponentLayer,\n allowVariableBinding = false,\n propName: string\n) => {\n return {\n renderParent: allowVariableBinding\n ? ({ children }: { children: React.ReactNode }) => (\n \n {children}\n \n )\n : undefined,\n fieldType: ({\n label,\n isRequired,\n fieldConfigItem,\n field,\n fieldProps,\n }: AutoFormInputComponentProps) => (\n \n field.onChange(e.target.value)}\n {...fieldProps}\n />\n \n ),\n };\n};\n\nexport const renderParentWithVariableBinding = ({\n children,\n}: {\n children: React.ReactNode;\n}) => (\n \n {children}\n YO!\n \n);\n\nexport function VariableBindingWrapper({\n propName,\n children,\n}: {\n propName: string;\n children: React.ReactNode;\n}) {\n const variables = useLayerStore((state) => state.variables);\n const selectedLayerId = useLayerStore((state) => state.selectedLayerId);\n const findLayerById = useLayerStore((state) => state.findLayerById);\n const isBindingImmutable = useLayerStore((state) => state.isBindingImmutable);\n const incrementRevision = useEditorStore((state) => state.incrementRevision);\n const unbindPropFromVariable = useLayerStore(\n (state) => state.unbindPropFromVariable\n );\n const bindPropToVariable = useLayerStore((state) => state.bindPropToVariable);\n\n const selectedLayer = findLayerById(selectedLayerId);\n\n // If variable binding is not allowed or no propName provided, just render the form wrapper\n if (!selectedLayer) {\n return <>{children}>;\n }\n\n const currentValue = selectedLayer.props[propName];\n const isCurrentlyBound = isVariableReference(currentValue);\n const boundVariable = isCurrentlyBound\n ? variables.find((v) => v.id === currentValue.__variableRef)\n : null;\n const isImmutable = isBindingImmutable(selectedLayer.id, propName);\n\n const handleBindToVariable = (variableId: string) => {\n bindPropToVariable(selectedLayer.id, propName, variableId);\n incrementRevision();\n };\n\n // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop\n const handleUnbind = () => {\n // Use the new unbind function which sets default value from schema\n unbindPropFromVariable(selectedLayer.id, propName);\n incrementRevision();\n };\n\n return (\n \n {isCurrentlyBound && boundVariable ? (\n // Bound state - show variable info and unbind button\n \n {propName.charAt(0).toUpperCase() + propName.slice(1)}\n \n \n \n \n \n \n \n {boundVariable.name}\n \n {boundVariable.type}\n \n {isImmutable && (\n \n \n \n )}\n \n \n {String(boundVariable.defaultValue)}\n \n \n \n \n \n {!isImmutable && (\n \n \n \n \n \n \n Unbind Variable\n \n )}\n \n \n ) : (\n // Unbound state - show normal field with bind button\n <>\n {children}\n \n \n \n \n \n \n \n \n \n \n Bind Variable\n \n \n \n Bind to Variable\n \n {variables.length > 0 ? (\n variables.map((variable) => (\n handleBindToVariable(variable.id)}\n className=\"flex flex-col items-start p-3\"\n >\n \n \n \n \n {variable.name}\n \n {variable.type}\n \n \n \n {String(variable.defaultValue)}\n \n \n \n \n ))\n ) : (\n \n No variables defined\n \n )}\n \n \n \n >\n )}\n \n );\n}\n\nexport function FormFieldWrapper({\n label,\n isRequired,\n fieldConfigItem,\n children,\n}: {\n label: string;\n isRequired?: boolean;\n fieldConfigItem?: { description?: React.ReactNode };\n children: React.ReactNode;\n}) {\n return (\n \n \n {label}\n {isRequired && *}\n \n {children}\n {fieldConfigItem?.description && (\n {fieldConfigItem.description}\n )}\n \n );\n}\n", "type": "registry:lib", "target": "lib/ui-builder/registry/form-field-overrides.tsx" },
No component selected