diff --git a/packages/components/package.json b/packages/components/package.json
index 018adb25e..ad9389bbf 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -50,6 +50,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
+ "date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.469.0",
diff --git a/packages/components/src/new-components.test.ts b/packages/components/src/new-components.test.ts
new file mode 100644
index 000000000..da8061541
--- /dev/null
+++ b/packages/components/src/new-components.test.ts
@@ -0,0 +1,65 @@
+import { describe, it, expect, beforeAll } from 'vitest';
+import { ComponentRegistry } from '@object-ui/core';
+
+describe('New Components Registration', () => {
+ // Import all renderers to register them
+ beforeAll(async () => {
+ await import('./renderers');
+ });
+
+ describe('Form Components', () => {
+ it('should register date-picker component', () => {
+ const component = ComponentRegistry.getConfig('date-picker');
+ expect(component).toBeDefined();
+ expect(component?.label).toBe('Date Picker');
+ });
+
+ it('should register file-upload component', () => {
+ const component = ComponentRegistry.getConfig('file-upload');
+ expect(component).toBeDefined();
+ expect(component?.label).toBe('File Upload');
+ });
+ });
+
+ describe('Data Display Components', () => {
+ it('should register list component', () => {
+ const component = ComponentRegistry.getConfig('list');
+ expect(component).toBeDefined();
+ expect(component?.label).toBe('List');
+ });
+
+ it('should register tree-view component', () => {
+ const component = ComponentRegistry.getConfig('tree-view');
+ expect(component).toBeDefined();
+ expect(component?.label).toBe('Tree View');
+ });
+ });
+
+ describe('Layout Components', () => {
+ it('should register grid component', () => {
+ const component = ComponentRegistry.getConfig('grid');
+ expect(component).toBeDefined();
+ expect(component?.label).toBe('Grid Layout');
+ });
+
+ it('should register flex component', () => {
+ const component = ComponentRegistry.getConfig('flex');
+ expect(component).toBeDefined();
+ expect(component?.label).toBe('Flex Layout');
+ });
+
+ it('should register container component', () => {
+ const component = ComponentRegistry.getConfig('container');
+ expect(component).toBeDefined();
+ expect(component?.label).toBe('Container');
+ });
+ });
+
+ describe('Feedback Components', () => {
+ it('should register loading component', () => {
+ const component = ComponentRegistry.getConfig('loading');
+ expect(component).toBeDefined();
+ expect(component?.label).toBe('Loading');
+ });
+ });
+});
diff --git a/packages/components/src/renderers/data-display/index.ts b/packages/components/src/renderers/data-display/index.ts
index 4209fd719..7b1a3bb0c 100644
--- a/packages/components/src/renderers/data-display/index.ts
+++ b/packages/components/src/renderers/data-display/index.ts
@@ -2,3 +2,5 @@ import './badge';
import './avatar';
import './alert';
import './chart';
+import './list';
+import './tree-view';
diff --git a/packages/components/src/renderers/data-display/list.tsx b/packages/components/src/renderers/data-display/list.tsx
new file mode 100644
index 000000000..08bd0fd5b
--- /dev/null
+++ b/packages/components/src/renderers/data-display/list.tsx
@@ -0,0 +1,55 @@
+import { ComponentRegistry } from '@object-ui/core';
+import { renderChildren } from '../../lib/utils';
+import { cn } from '@/lib/utils';
+
+ComponentRegistry.register('list',
+ ({ schema, className, ...props }) => {
+ const ListTag = schema.ordered ? 'ol' : 'ul';
+
+ return (
+
+ {schema.title && (
+
{schema.title}
+ )}
+
+ {schema.items?.map((item: any, index: number) => (
+
+ {typeof item === 'string' ? item : item.content || renderChildren(item.body)}
+
+ ))}
+
+
+ );
+ },
+ {
+ label: 'List',
+ inputs: [
+ { name: 'title', type: 'string', label: 'Title' },
+ { name: 'ordered', type: 'boolean', label: 'Ordered List (numbered)', defaultValue: false },
+ {
+ name: 'items',
+ type: 'array',
+ label: 'List Items',
+ description: 'Array of strings or objects with content/body'
+ },
+ { name: 'className', type: 'string', label: 'CSS Class' }
+ ],
+ defaultProps: {
+ ordered: false,
+ items: [
+ 'First item',
+ 'Second item',
+ 'Third item'
+ ],
+ className: 'text-sm'
+ }
+ }
+);
diff --git a/packages/components/src/renderers/data-display/tree-view.tsx b/packages/components/src/renderers/data-display/tree-view.tsx
new file mode 100644
index 000000000..732ba04de
--- /dev/null
+++ b/packages/components/src/renderers/data-display/tree-view.tsx
@@ -0,0 +1,152 @@
+import { ComponentRegistry } from '@object-ui/core';
+import { ChevronRight, ChevronDown, Folder, File } from 'lucide-react';
+import { useState } from 'react';
+import { cn } from '@/lib/utils';
+
+interface TreeNode {
+ id: string;
+ label: string;
+ icon?: string;
+ children?: TreeNode[];
+ data?: any;
+}
+
+const TreeNodeComponent = ({
+ node,
+ level = 0,
+ onNodeClick
+}: {
+ node: TreeNode;
+ level?: number;
+ onNodeClick?: (node: TreeNode) => void;
+}) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const hasChildren = node.children && node.children.length > 0;
+
+ const handleToggle = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setIsOpen(!isOpen);
+ };
+
+ const handleClick = () => {
+ if (onNodeClick) {
+ onNodeClick(node);
+ }
+ };
+
+ return (
+
+
+ {hasChildren ? (
+
+ ) : (
+
+ )}
+ {node.icon === 'folder' ? (
+
+ ) : node.icon === 'file' ? (
+
+ ) : null}
+ {node.label}
+
+ {hasChildren && isOpen && (
+
+ {node.children!.map((child) => (
+
+ ))}
+
+ )}
+
+ );
+};
+
+ComponentRegistry.register('tree-view',
+ ({ schema, className, ...props }) => {
+ const handleNodeClick = (node: TreeNode) => {
+ if (schema.onNodeClick) {
+ schema.onNodeClick(node);
+ }
+ };
+
+ return (
+
+ {schema.title && (
+
{schema.title}
+ )}
+
+ {schema.nodes?.map((node: TreeNode) => (
+
+ ))}
+
+
+ );
+ },
+ {
+ label: 'Tree View',
+ inputs: [
+ { name: 'title', type: 'string', label: 'Title' },
+ {
+ name: 'nodes',
+ type: 'array',
+ label: 'Tree Nodes',
+ description: 'Array of { id, label, icon, children, data }'
+ },
+ { name: 'className', type: 'string', label: 'CSS Class' }
+ ],
+ defaultProps: {
+ title: 'File Explorer',
+ nodes: [
+ {
+ id: '1',
+ label: 'Documents',
+ icon: 'folder',
+ children: [
+ { id: '1-1', label: 'Resume.pdf', icon: 'file' },
+ { id: '1-2', label: 'Cover Letter.docx', icon: 'file' }
+ ]
+ },
+ {
+ id: '2',
+ label: 'Photos',
+ icon: 'folder',
+ children: [
+ { id: '2-1', label: 'Vacation', icon: 'folder', children: [
+ { id: '2-1-1', label: 'Beach.jpg', icon: 'file' }
+ ]},
+ { id: '2-2', label: 'Family.jpg', icon: 'file' }
+ ]
+ },
+ {
+ id: '3',
+ label: 'README.md',
+ icon: 'file'
+ }
+ ]
+ }
+ }
+);
diff --git a/packages/components/src/renderers/feedback/index.ts b/packages/components/src/renderers/feedback/index.ts
index 93f2a701e..eedbfddd0 100644
--- a/packages/components/src/renderers/feedback/index.ts
+++ b/packages/components/src/renderers/feedback/index.ts
@@ -1,3 +1,4 @@
import './progress';
import './skeleton';
import './toaster';
+import './loading';
diff --git a/packages/components/src/renderers/feedback/loading.tsx b/packages/components/src/renderers/feedback/loading.tsx
new file mode 100644
index 000000000..45c91f048
--- /dev/null
+++ b/packages/components/src/renderers/feedback/loading.tsx
@@ -0,0 +1,68 @@
+import { ComponentRegistry } from '@object-ui/core';
+import { Spinner } from '@/ui';
+import { cn } from '@/lib/utils';
+
+ComponentRegistry.register('loading',
+ ({ schema, className, ...props }) => {
+ const size = schema.size || 'md';
+ const fullscreen = schema.fullscreen || false;
+
+ const loadingContent = (
+
+
+ {schema.text && (
+
{schema.text}
+ )}
+
+ );
+
+ if (fullscreen) {
+ return (
+
+ {loadingContent}
+
+ );
+ }
+
+ return (
+
+ {loadingContent}
+
+ );
+ },
+ {
+ label: 'Loading',
+ inputs: [
+ { name: 'text', type: 'string', label: 'Loading Text' },
+ {
+ name: 'size',
+ type: 'enum',
+ enum: ['sm', 'md', 'lg', 'xl'],
+ label: 'Size',
+ defaultValue: 'md'
+ },
+ {
+ name: 'fullscreen',
+ type: 'boolean',
+ label: 'Fullscreen Overlay',
+ defaultValue: false
+ },
+ { name: 'className', type: 'string', label: 'CSS Class' }
+ ],
+ defaultProps: {
+ text: 'Loading...',
+ size: 'md',
+ fullscreen: false
+ }
+ }
+);
diff --git a/packages/components/src/renderers/form/date-picker.tsx b/packages/components/src/renderers/form/date-picker.tsx
new file mode 100644
index 000000000..1864c8530
--- /dev/null
+++ b/packages/components/src/renderers/form/date-picker.tsx
@@ -0,0 +1,61 @@
+import { ComponentRegistry } from '@object-ui/core';
+import { Calendar, Button, Popover, PopoverTrigger, PopoverContent, Label } from '@/ui';
+import { CalendarIcon } from 'lucide-react';
+import { format } from 'date-fns';
+import { cn } from '@/lib/utils';
+
+ComponentRegistry.register('date-picker',
+ ({ schema, className, value, onChange, ...props }) => {
+ const handleSelect = (date: Date | undefined) => {
+ if (onChange) {
+ onChange(date);
+ }
+ };
+
+ return (
+
+ {schema.label &&
}
+
+
+
+
+
+
+
+
+
+ );
+ },
+ {
+ label: 'Date Picker',
+ inputs: [
+ { name: 'label', type: 'string', label: 'Label' },
+ { name: 'placeholder', type: 'string', label: 'Placeholder' },
+ { name: 'format', type: 'string', label: 'Date Format', description: 'date-fns format string (e.g., "PPP", "yyyy-MM-dd")' },
+ { name: 'id', type: 'string', label: 'ID', required: true }
+ ],
+ defaultProps: {
+ label: 'Date',
+ placeholder: 'Pick a date',
+ format: 'PPP',
+ id: 'date-picker-field'
+ }
+ }
+);
diff --git a/packages/components/src/renderers/form/file-upload.tsx b/packages/components/src/renderers/form/file-upload.tsx
new file mode 100644
index 000000000..d182f2694
--- /dev/null
+++ b/packages/components/src/renderers/form/file-upload.tsx
@@ -0,0 +1,97 @@
+import { ComponentRegistry } from '@object-ui/core';
+import { Label, Button } from '@/ui';
+import { Upload, X } from 'lucide-react';
+import { useState, useRef } from 'react';
+import { cn } from '@/lib/utils';
+
+ComponentRegistry.register('file-upload',
+ ({ schema, className, value, onChange, ...props }) => {
+ const [files, setFiles] = useState(value || []);
+ const inputRef = useRef(null);
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ const newFiles = Array.from(e.target.files || []);
+ const updatedFiles = schema.multiple ? [...files, ...newFiles] : newFiles;
+ setFiles(updatedFiles);
+ if (onChange) {
+ onChange(updatedFiles);
+ }
+ };
+
+ const handleRemoveFile = (index: number) => {
+ const updatedFiles = files.filter((_, i) => i !== index);
+ setFiles(updatedFiles);
+ if (onChange) {
+ onChange(updatedFiles);
+ }
+ };
+
+ const handleClick = () => {
+ inputRef.current?.click();
+ };
+
+ return (
+
+ {schema.label &&
}
+
+
+
+ {files.length > 0 && (
+
+ {files.map((file, index) => (
+
+ {file.name}
+
+
+ ))}
+
+ )}
+
+
+ );
+ },
+ {
+ label: 'File Upload',
+ inputs: [
+ { name: 'label', type: 'string', label: 'Label' },
+ { name: 'buttonText', type: 'string', label: 'Button Text' },
+ { name: 'accept', type: 'string', label: 'Accepted File Types', description: 'MIME types (e.g., "image/*,application/pdf")' },
+ { name: 'multiple', type: 'boolean', label: 'Allow Multiple Files' },
+ { name: 'id', type: 'string', label: 'ID', required: true }
+ ],
+ defaultProps: {
+ label: 'Upload files',
+ buttonText: 'Choose files',
+ multiple: true,
+ id: 'file-upload-field'
+ }
+ }
+);
diff --git a/packages/components/src/renderers/form/index.ts b/packages/components/src/renderers/form/index.ts
index 5d068db89..bdd27fce8 100644
--- a/packages/components/src/renderers/form/index.ts
+++ b/packages/components/src/renderers/form/index.ts
@@ -9,3 +9,5 @@ import './slider';
import './toggle';
import './input-otp';
import './calendar';
+import './date-picker';
+import './file-upload';
diff --git a/packages/components/src/renderers/layout/container.tsx b/packages/components/src/renderers/layout/container.tsx
new file mode 100644
index 000000000..3e2846920
--- /dev/null
+++ b/packages/components/src/renderers/layout/container.tsx
@@ -0,0 +1,85 @@
+import { ComponentRegistry } from '@object-ui/core';
+import { renderChildren } from '../../lib/utils';
+import { cn } from '@/lib/utils';
+
+ComponentRegistry.register('container',
+ ({ schema, className, ...props }) => {
+ const maxWidth = schema.maxWidth || 'xl';
+ const padding = schema.padding || 4;
+ const centered = schema.centered !== false; // Default to true
+
+ const containerClass = cn(
+ // Base container
+ 'w-full',
+ // Max width
+ maxWidth === 'sm' && 'max-w-sm',
+ maxWidth === 'md' && 'max-w-md',
+ maxWidth === 'lg' && 'max-w-lg',
+ maxWidth === 'xl' && 'max-w-xl',
+ maxWidth === '2xl' && 'max-w-2xl',
+ maxWidth === '3xl' && 'max-w-3xl',
+ maxWidth === '4xl' && 'max-w-4xl',
+ maxWidth === '5xl' && 'max-w-5xl',
+ maxWidth === '6xl' && 'max-w-6xl',
+ maxWidth === '7xl' && 'max-w-7xl',
+ maxWidth === 'full' && 'max-w-full',
+ maxWidth === 'screen' && 'max-w-screen-2xl',
+ // Centering
+ centered && 'mx-auto',
+ // Padding
+ padding === 0 && 'p-0',
+ padding === 1 && 'p-1',
+ padding === 2 && 'p-2',
+ padding === 3 && 'p-3',
+ padding === 4 && 'p-4',
+ padding === 5 && 'p-5',
+ padding === 6 && 'p-6',
+ padding === 7 && 'p-7',
+ padding === 8 && 'p-8',
+ padding === 10 && 'p-10',
+ padding === 12 && 'p-12',
+ padding === 16 && 'p-16',
+ className
+ );
+
+ return (
+
+ {schema.children && renderChildren(schema.children)}
+
+ );
+ },
+ {
+ label: 'Container',
+ inputs: [
+ {
+ name: 'maxWidth',
+ type: 'enum',
+ enum: ['sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl', '7xl', 'full', 'screen'],
+ label: 'Max Width',
+ defaultValue: 'xl'
+ },
+ {
+ name: 'padding',
+ type: 'number',
+ label: 'Padding',
+ defaultValue: 4,
+ description: 'Padding value (0, 1-8, 10, 12, 16)'
+ },
+ {
+ name: 'centered',
+ type: 'boolean',
+ label: 'Center Horizontally',
+ defaultValue: true
+ },
+ { name: 'className', type: 'string', label: 'CSS Class' }
+ ],
+ defaultProps: {
+ maxWidth: 'xl',
+ padding: 4,
+ centered: true,
+ children: [
+ { type: 'text', content: 'Container content goes here' }
+ ]
+ }
+ }
+);
diff --git a/packages/components/src/renderers/layout/flex.tsx b/packages/components/src/renderers/layout/flex.tsx
new file mode 100644
index 000000000..c905d86eb
--- /dev/null
+++ b/packages/components/src/renderers/layout/flex.tsx
@@ -0,0 +1,107 @@
+import { ComponentRegistry } from '@object-ui/core';
+import { renderChildren } from '../../lib/utils';
+import { cn } from '@/lib/utils';
+
+ComponentRegistry.register('flex',
+ ({ schema, className, ...props }) => {
+ const direction = schema.direction || 'row';
+ const justify = schema.justify || 'start';
+ const align = schema.align || 'start';
+ const gap = schema.gap || 2;
+ const wrap = schema.wrap || false;
+
+ const flexClass = cn(
+ 'flex',
+ // Direction
+ direction === 'row' && 'flex-row',
+ direction === 'col' && 'flex-col',
+ direction === 'row-reverse' && 'flex-row-reverse',
+ direction === 'col-reverse' && 'flex-col-reverse',
+ // Justify content
+ justify === 'start' && 'justify-start',
+ justify === 'end' && 'justify-end',
+ justify === 'center' && 'justify-center',
+ justify === 'between' && 'justify-between',
+ justify === 'around' && 'justify-around',
+ justify === 'evenly' && 'justify-evenly',
+ // Align items
+ align === 'start' && 'items-start',
+ align === 'end' && 'items-end',
+ align === 'center' && 'items-center',
+ align === 'baseline' && 'items-baseline',
+ align === 'stretch' && 'items-stretch',
+ // Gap
+ gap === 0 && 'gap-0',
+ gap === 1 && 'gap-1',
+ gap === 2 && 'gap-2',
+ gap === 3 && 'gap-3',
+ gap === 4 && 'gap-4',
+ gap === 5 && 'gap-5',
+ gap === 6 && 'gap-6',
+ gap === 7 && 'gap-7',
+ gap === 8 && 'gap-8',
+ // Wrap
+ wrap && 'flex-wrap',
+ className
+ );
+
+ return (
+
+ {schema.children && renderChildren(schema.children)}
+
+ );
+ },
+ {
+ label: 'Flex Layout',
+ inputs: [
+ {
+ name: 'direction',
+ type: 'enum',
+ enum: ['row', 'col', 'row-reverse', 'col-reverse'],
+ label: 'Direction',
+ defaultValue: 'row'
+ },
+ {
+ name: 'justify',
+ type: 'enum',
+ enum: ['start', 'end', 'center', 'between', 'around', 'evenly'],
+ label: 'Justify Content',
+ defaultValue: 'start'
+ },
+ {
+ name: 'align',
+ type: 'enum',
+ enum: ['start', 'end', 'center', 'baseline', 'stretch'],
+ label: 'Align Items',
+ defaultValue: 'start'
+ },
+ {
+ name: 'gap',
+ type: 'number',
+ label: 'Gap',
+ defaultValue: 2,
+ description: 'Gap between items (0-8)'
+ },
+ {
+ name: 'wrap',
+ type: 'boolean',
+ label: 'Wrap',
+ defaultValue: false,
+ description: 'Allow flex items to wrap'
+ },
+ { name: 'className', type: 'string', label: 'CSS Class' }
+ ],
+ defaultProps: {
+ direction: 'row',
+ justify: 'start',
+ align: 'center',
+ gap: 2,
+ wrap: false,
+ children: [
+ { type: 'button', label: 'Button 1' },
+ { type: 'button', label: 'Button 2' },
+ { type: 'button', label: 'Button 3' }
+ ]
+ }
+ }
+);
diff --git a/packages/components/src/renderers/layout/grid.tsx b/packages/components/src/renderers/layout/grid.tsx
new file mode 100644
index 000000000..d74dbc5c7
--- /dev/null
+++ b/packages/components/src/renderers/layout/grid.tsx
@@ -0,0 +1,90 @@
+import { ComponentRegistry } from '@object-ui/core';
+import { renderChildren } from '../../lib/utils';
+import { cn } from '@/lib/utils';
+
+ComponentRegistry.register('grid',
+ ({ schema, className, ...props }) => {
+ const gridCols = schema.columns || 2;
+ const gap = schema.gap || 4;
+
+ // Generate Tailwind grid classes
+ const gridClass = cn(
+ 'grid',
+ // Grid columns classes
+ gridCols === 1 && 'grid-cols-1',
+ gridCols === 2 && 'grid-cols-2',
+ gridCols === 3 && 'grid-cols-3',
+ gridCols === 4 && 'grid-cols-4',
+ gridCols === 5 && 'grid-cols-5',
+ gridCols === 6 && 'grid-cols-6',
+ gridCols === 7 && 'grid-cols-7',
+ gridCols === 8 && 'grid-cols-8',
+ gridCols === 9 && 'grid-cols-9',
+ gridCols === 10 && 'grid-cols-10',
+ gridCols === 11 && 'grid-cols-11',
+ gridCols === 12 && 'grid-cols-12',
+ // Gap classes
+ gap === 0 && 'gap-0',
+ gap === 1 && 'gap-1',
+ gap === 2 && 'gap-2',
+ gap === 3 && 'gap-3',
+ gap === 4 && 'gap-4',
+ gap === 5 && 'gap-5',
+ gap === 6 && 'gap-6',
+ gap === 7 && 'gap-7',
+ gap === 8 && 'gap-8',
+ // Responsive columns
+ schema.mdColumns && `md:grid-cols-${schema.mdColumns}`,
+ schema.lgColumns && `lg:grid-cols-${schema.lgColumns}`,
+ className
+ );
+
+ return (
+
+ {schema.children && renderChildren(schema.children)}
+
+ );
+ },
+ {
+ label: 'Grid Layout',
+ inputs: [
+ {
+ name: 'columns',
+ type: 'number',
+ label: 'Columns',
+ defaultValue: 2,
+ description: 'Number of columns (1-12)'
+ },
+ {
+ name: 'mdColumns',
+ type: 'number',
+ label: 'MD Breakpoint Columns',
+ description: 'Columns at md breakpoint (optional)'
+ },
+ {
+ name: 'lgColumns',
+ type: 'number',
+ label: 'LG Breakpoint Columns',
+ description: 'Columns at lg breakpoint (optional)'
+ },
+ {
+ name: 'gap',
+ type: 'number',
+ label: 'Gap',
+ defaultValue: 4,
+ description: 'Gap between items (0-8)'
+ },
+ { name: 'className', type: 'string', label: 'CSS Class' }
+ ],
+ defaultProps: {
+ columns: 2,
+ gap: 4,
+ children: [
+ { type: 'card', title: 'Card 1', description: 'First card' },
+ { type: 'card', title: 'Card 2', description: 'Second card' },
+ { type: 'card', title: 'Card 3', description: 'Third card' },
+ { type: 'card', title: 'Card 4', description: 'Fourth card' }
+ ]
+ }
+ }
+);
diff --git a/packages/components/src/renderers/layout/index.ts b/packages/components/src/renderers/layout/index.ts
index f04175c50..061a601e9 100644
--- a/packages/components/src/renderers/layout/index.ts
+++ b/packages/components/src/renderers/layout/index.ts
@@ -1,2 +1,5 @@
import './card';
import './tabs';
+import './grid';
+import './flex';
+import './container';
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index da91e8c72..90c91dedc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -304,6 +304,9 @@ importers:
cmdk:
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ date-fns:
+ specifier: ^4.1.0
+ version: 4.1.0
embla-carousel-react:
specifier: ^8.6.0
version: 8.6.0(react@18.3.1)