Skip to content

Commit c4637f4

Browse files
authored
Merge pull request #12 from objectql/copilot/complete-ui-component-setup
2 parents d00d85f + 5739191 commit c4637f4

15 files changed

Lines changed: 792 additions & 0 deletions

File tree

packages/components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"class-variance-authority": "^0.7.1",
5151
"clsx": "^2.1.1",
5252
"cmdk": "^1.1.1",
53+
"date-fns": "^4.1.0",
5354
"embla-carousel-react": "^8.6.0",
5455
"input-otp": "^1.4.2",
5556
"lucide-react": "^0.469.0",
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { describe, it, expect, beforeAll } from 'vitest';
2+
import { ComponentRegistry } from '@object-ui/core';
3+
4+
describe('New Components Registration', () => {
5+
// Import all renderers to register them
6+
beforeAll(async () => {
7+
await import('./renderers');
8+
});
9+
10+
describe('Form Components', () => {
11+
it('should register date-picker component', () => {
12+
const component = ComponentRegistry.getConfig('date-picker');
13+
expect(component).toBeDefined();
14+
expect(component?.label).toBe('Date Picker');
15+
});
16+
17+
it('should register file-upload component', () => {
18+
const component = ComponentRegistry.getConfig('file-upload');
19+
expect(component).toBeDefined();
20+
expect(component?.label).toBe('File Upload');
21+
});
22+
});
23+
24+
describe('Data Display Components', () => {
25+
it('should register list component', () => {
26+
const component = ComponentRegistry.getConfig('list');
27+
expect(component).toBeDefined();
28+
expect(component?.label).toBe('List');
29+
});
30+
31+
it('should register tree-view component', () => {
32+
const component = ComponentRegistry.getConfig('tree-view');
33+
expect(component).toBeDefined();
34+
expect(component?.label).toBe('Tree View');
35+
});
36+
});
37+
38+
describe('Layout Components', () => {
39+
it('should register grid component', () => {
40+
const component = ComponentRegistry.getConfig('grid');
41+
expect(component).toBeDefined();
42+
expect(component?.label).toBe('Grid Layout');
43+
});
44+
45+
it('should register flex component', () => {
46+
const component = ComponentRegistry.getConfig('flex');
47+
expect(component).toBeDefined();
48+
expect(component?.label).toBe('Flex Layout');
49+
});
50+
51+
it('should register container component', () => {
52+
const component = ComponentRegistry.getConfig('container');
53+
expect(component).toBeDefined();
54+
expect(component?.label).toBe('Container');
55+
});
56+
});
57+
58+
describe('Feedback Components', () => {
59+
it('should register loading component', () => {
60+
const component = ComponentRegistry.getConfig('loading');
61+
expect(component).toBeDefined();
62+
expect(component?.label).toBe('Loading');
63+
});
64+
});
65+
});

packages/components/src/renderers/data-display/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ import './badge';
22
import './avatar';
33
import './alert';
44
import './chart';
5+
import './list';
6+
import './tree-view';
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { ComponentRegistry } from '@object-ui/core';
2+
import { renderChildren } from '../../lib/utils';
3+
import { cn } from '@/lib/utils';
4+
5+
ComponentRegistry.register('list',
6+
({ schema, className, ...props }) => {
7+
const ListTag = schema.ordered ? 'ol' : 'ul';
8+
9+
return (
10+
<div className={schema.wrapperClass}>
11+
{schema.title && (
12+
<h3 className="text-lg font-semibold mb-2">{schema.title}</h3>
13+
)}
14+
<ListTag
15+
className={cn(
16+
schema.ordered
17+
? 'list-decimal list-inside space-y-2'
18+
: 'list-disc list-inside space-y-2',
19+
className
20+
)}
21+
{...props}
22+
>
23+
{schema.items?.map((item: any, index: number) => (
24+
<li key={index} className={item.className}>
25+
{typeof item === 'string' ? item : item.content || renderChildren(item.body)}
26+
</li>
27+
))}
28+
</ListTag>
29+
</div>
30+
);
31+
},
32+
{
33+
label: 'List',
34+
inputs: [
35+
{ name: 'title', type: 'string', label: 'Title' },
36+
{ name: 'ordered', type: 'boolean', label: 'Ordered List (numbered)', defaultValue: false },
37+
{
38+
name: 'items',
39+
type: 'array',
40+
label: 'List Items',
41+
description: 'Array of strings or objects with content/body'
42+
},
43+
{ name: 'className', type: 'string', label: 'CSS Class' }
44+
],
45+
defaultProps: {
46+
ordered: false,
47+
items: [
48+
'First item',
49+
'Second item',
50+
'Third item'
51+
],
52+
className: 'text-sm'
53+
}
54+
}
55+
);
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { ComponentRegistry } from '@object-ui/core';
2+
import { ChevronRight, ChevronDown, Folder, File } from 'lucide-react';
3+
import { useState } from 'react';
4+
import { cn } from '@/lib/utils';
5+
6+
interface TreeNode {
7+
id: string;
8+
label: string;
9+
icon?: string;
10+
children?: TreeNode[];
11+
data?: any;
12+
}
13+
14+
const TreeNodeComponent = ({
15+
node,
16+
level = 0,
17+
onNodeClick
18+
}: {
19+
node: TreeNode;
20+
level?: number;
21+
onNodeClick?: (node: TreeNode) => void;
22+
}) => {
23+
const [isOpen, setIsOpen] = useState(false);
24+
const hasChildren = node.children && node.children.length > 0;
25+
26+
const handleToggle = (e: React.MouseEvent) => {
27+
e.stopPropagation();
28+
setIsOpen(!isOpen);
29+
};
30+
31+
const handleClick = () => {
32+
if (onNodeClick) {
33+
onNodeClick(node);
34+
}
35+
};
36+
37+
return (
38+
<div>
39+
<div
40+
className={cn(
41+
'flex items-center py-1.5 px-2 hover:bg-accent rounded-md cursor-pointer',
42+
'transition-colors'
43+
)}
44+
style={{ paddingLeft: `${level * 16 + 8}px` }}
45+
onClick={handleClick}
46+
>
47+
{hasChildren ? (
48+
<button
49+
onClick={handleToggle}
50+
className="mr-1 p-0 h-4 w-4 flex items-center justify-center hover:bg-accent/50 rounded"
51+
>
52+
{isOpen ? (
53+
<ChevronDown className="h-4 w-4" />
54+
) : (
55+
<ChevronRight className="h-4 w-4" />
56+
)}
57+
</button>
58+
) : (
59+
<span className="mr-1 w-4" />
60+
)}
61+
{node.icon === 'folder' ? (
62+
<Folder className="h-4 w-4 mr-2 text-muted-foreground" />
63+
) : node.icon === 'file' ? (
64+
<File className="h-4 w-4 mr-2 text-muted-foreground" />
65+
) : null}
66+
<span className="text-sm">{node.label}</span>
67+
</div>
68+
{hasChildren && isOpen && (
69+
<div>
70+
{node.children!.map((child) => (
71+
<TreeNodeComponent
72+
key={child.id}
73+
node={child}
74+
level={level + 1}
75+
onNodeClick={onNodeClick}
76+
/>
77+
))}
78+
</div>
79+
)}
80+
</div>
81+
);
82+
};
83+
84+
ComponentRegistry.register('tree-view',
85+
({ schema, className, ...props }) => {
86+
const handleNodeClick = (node: TreeNode) => {
87+
if (schema.onNodeClick) {
88+
schema.onNodeClick(node);
89+
}
90+
};
91+
92+
return (
93+
<div className={cn('border rounded-md p-2 bg-background', className)} {...props}>
94+
{schema.title && (
95+
<h3 className="text-sm font-semibold mb-2 px-2">{schema.title}</h3>
96+
)}
97+
<div className="space-y-0.5">
98+
{schema.nodes?.map((node: TreeNode) => (
99+
<TreeNodeComponent
100+
key={node.id}
101+
node={node}
102+
onNodeClick={handleNodeClick}
103+
/>
104+
))}
105+
</div>
106+
</div>
107+
);
108+
},
109+
{
110+
label: 'Tree View',
111+
inputs: [
112+
{ name: 'title', type: 'string', label: 'Title' },
113+
{
114+
name: 'nodes',
115+
type: 'array',
116+
label: 'Tree Nodes',
117+
description: 'Array of { id, label, icon, children, data }'
118+
},
119+
{ name: 'className', type: 'string', label: 'CSS Class' }
120+
],
121+
defaultProps: {
122+
title: 'File Explorer',
123+
nodes: [
124+
{
125+
id: '1',
126+
label: 'Documents',
127+
icon: 'folder',
128+
children: [
129+
{ id: '1-1', label: 'Resume.pdf', icon: 'file' },
130+
{ id: '1-2', label: 'Cover Letter.docx', icon: 'file' }
131+
]
132+
},
133+
{
134+
id: '2',
135+
label: 'Photos',
136+
icon: 'folder',
137+
children: [
138+
{ id: '2-1', label: 'Vacation', icon: 'folder', children: [
139+
{ id: '2-1-1', label: 'Beach.jpg', icon: 'file' }
140+
]},
141+
{ id: '2-2', label: 'Family.jpg', icon: 'file' }
142+
]
143+
},
144+
{
145+
id: '3',
146+
label: 'README.md',
147+
icon: 'file'
148+
}
149+
]
150+
}
151+
}
152+
);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
import './progress';
22
import './skeleton';
33
import './toaster';
4+
import './loading';
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { ComponentRegistry } from '@object-ui/core';
2+
import { Spinner } from '@/ui';
3+
import { cn } from '@/lib/utils';
4+
5+
ComponentRegistry.register('loading',
6+
({ schema, className, ...props }) => {
7+
const size = schema.size || 'md';
8+
const fullscreen = schema.fullscreen || false;
9+
10+
const loadingContent = (
11+
<div className={cn('flex flex-col items-center justify-center gap-2', className)}>
12+
<Spinner
13+
className={cn(
14+
size === 'sm' && 'h-4 w-4',
15+
size === 'md' && 'h-8 w-8',
16+
size === 'lg' && 'h-12 w-12',
17+
size === 'xl' && 'h-16 w-16'
18+
)}
19+
/>
20+
{schema.text && (
21+
<p className="text-sm text-muted-foreground">{schema.text}</p>
22+
)}
23+
</div>
24+
);
25+
26+
if (fullscreen) {
27+
return (
28+
<div
29+
className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm"
30+
{...props}
31+
>
32+
{loadingContent}
33+
</div>
34+
);
35+
}
36+
37+
return (
38+
<div className="flex items-center justify-center p-8" {...props}>
39+
{loadingContent}
40+
</div>
41+
);
42+
},
43+
{
44+
label: 'Loading',
45+
inputs: [
46+
{ name: 'text', type: 'string', label: 'Loading Text' },
47+
{
48+
name: 'size',
49+
type: 'enum',
50+
enum: ['sm', 'md', 'lg', 'xl'],
51+
label: 'Size',
52+
defaultValue: 'md'
53+
},
54+
{
55+
name: 'fullscreen',
56+
type: 'boolean',
57+
label: 'Fullscreen Overlay',
58+
defaultValue: false
59+
},
60+
{ name: 'className', type: 'string', label: 'CSS Class' }
61+
],
62+
defaultProps: {
63+
text: 'Loading...',
64+
size: 'md',
65+
fullscreen: false
66+
}
67+
}
68+
);

0 commit comments

Comments
 (0)