Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
65 changes: 65 additions & 0 deletions packages/components/src/new-components.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
2 changes: 2 additions & 0 deletions packages/components/src/renderers/data-display/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ import './badge';
import './avatar';
import './alert';
import './chart';
import './list';
import './tree-view';
55 changes: 55 additions & 0 deletions packages/components/src/renderers/data-display/list.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={schema.wrapperClass}>
{schema.title && (
<h3 className="text-lg font-semibold mb-2">{schema.title}</h3>
)}
<ListTag
className={cn(
schema.ordered
? 'list-decimal list-inside space-y-2'
: 'list-disc list-inside space-y-2',
className
)}
{...props}
>
{schema.items?.map((item: any, index: number) => (
<li key={index} className={item.className}>
{typeof item === 'string' ? item : item.content || renderChildren(item.body)}
</li>
))}
</ListTag>
</div>
);
},
{
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'
}
}
);
152 changes: 152 additions & 0 deletions packages/components/src/renderers/data-display/tree-view.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<div
className={cn(
'flex items-center py-1.5 px-2 hover:bg-accent rounded-md cursor-pointer',
'transition-colors'
)}
style={{ paddingLeft: `${level * 16 + 8}px` }}
onClick={handleClick}
>
{hasChildren ? (
<button
onClick={handleToggle}
className="mr-1 p-0 h-4 w-4 flex items-center justify-center hover:bg-accent/50 rounded"
>
{isOpen ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</button>
) : (
<span className="mr-1 w-4" />
)}
{node.icon === 'folder' ? (
<Folder className="h-4 w-4 mr-2 text-muted-foreground" />
) : node.icon === 'file' ? (
<File className="h-4 w-4 mr-2 text-muted-foreground" />
) : null}
<span className="text-sm">{node.label}</span>
</div>
{hasChildren && isOpen && (
<div>
{node.children!.map((child) => (
<TreeNodeComponent
key={child.id}
node={child}
level={level + 1}
onNodeClick={onNodeClick}
/>
))}
</div>
)}
</div>
);
};

ComponentRegistry.register('tree-view',
({ schema, className, ...props }) => {
const handleNodeClick = (node: TreeNode) => {
if (schema.onNodeClick) {
schema.onNodeClick(node);
}
};

return (
<div className={cn('border rounded-md p-2 bg-background', className)} {...props}>
{schema.title && (
<h3 className="text-sm font-semibold mb-2 px-2">{schema.title}</h3>
)}
<div className="space-y-0.5">
{schema.nodes?.map((node: TreeNode) => (
<TreeNodeComponent
key={node.id}
node={node}
onNodeClick={handleNodeClick}
/>
))}
</div>
</div>
);
},
{
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'
}
]
}
}
);
1 change: 1 addition & 0 deletions packages/components/src/renderers/feedback/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import './progress';
import './skeleton';
import './toaster';
import './loading';
68 changes: 68 additions & 0 deletions packages/components/src/renderers/feedback/loading.tsx
Original file line number Diff line number Diff line change
@@ -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 = (
<div className={cn('flex flex-col items-center justify-center gap-2', className)}>
<Spinner
className={cn(
size === 'sm' && 'h-4 w-4',
size === 'md' && 'h-8 w-8',
size === 'lg' && 'h-12 w-12',
size === 'xl' && 'h-16 w-16'
)}
/>
{schema.text && (
<p className="text-sm text-muted-foreground">{schema.text}</p>
)}
</div>
);

if (fullscreen) {
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm"
{...props}
>
{loadingContent}
</div>
);
}

return (
<div className="flex items-center justify-center p-8" {...props}>
{loadingContent}
</div>
);
},
{
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
}
}
);
Loading