Skip to content

Commit e5e899d

Browse files
committed
feat: enhance list, tree-view, and various field components with data binding support and improved styling
1 parent 9afc309 commit e5e899d

File tree

6 files changed

+111
-76
lines changed

6 files changed

+111
-76
lines changed

packages/components/src/renderers/data-display/list.tsx

Lines changed: 21 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,71 +6,45 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9+
import { ComponentRegistry } from '@object-ui/core';
10+
import compact from 'lodash/compact';
911
import { ComponentRegistry } from '@object-ui/core';
1012
import type { ListSchema } from '@object-ui/types';
13+
import { useDataScope } from '@object-ui/react';
1114
import { renderChildren } from '../../lib/utils';
1215
import { cn } from '../../lib/utils';
13-
import { ChevronRight, Hexagon, Terminal } from 'lucide-react';
1416

1517
ComponentRegistry.register('list',
1618
({ schema, className, ...props }: { schema: ListSchema; className?: string; [key: string]: any }) => {
17-
// We use 'ul' for both to control semantics manually with visuals
18-
const ListTag = 'ul';
19+
// Support data binding
20+
const boundData = useDataScope(schema.bind);
21+
const items = boundData || schema.items || [];
1922

20-
return (
21-
<div className={cn("relative p-4 rounded-lg bg-slate-950/30 border border-slate-800/50 backdrop-blur-sm", schema.wrapperClass)}>
22-
{/* Decorative corner accents for container */}
23-
<div className="absolute top-0 left-0 w-2 h-2 border-l-2 border-t-2 border-cyan-500/50 rounded-tl-sm" />
24-
<div className="absolute top-0 right-0 w-2 h-2 border-r-2 border-t-2 border-cyan-500/50 rounded-tr-sm" />
25-
<div className="absolute bottom-0 left-0 w-2 h-2 border-l-2 border-b-2 border-cyan-500/50 rounded-bl-sm" />
26-
<div className="absolute bottom-0 right-0 w-2 h-2 border-r-2 border-b-2 border-cyan-500/50 rounded-br-sm" />
23+
// We use 'ol' or 'ul' based on ordered prop
24+
const ListTag = schema.ordered ? 'ol' : 'ul';
25+
26+
// Default styles for ordered/unordered
27+
const listStyle = schema.ordered ? "list-decimal" : "list-disc";
2728

29+
return (
30+
<div className={cn("space-y-2", schema.wrapperClass)}>
2831
{schema.title && (
29-
<div className="flex items-center gap-2 mb-4 pb-2 border-b border-cyan-900/30">
30-
<Terminal className="w-4 h-4 text-cyan-500" />
31-
<h3 className="text-sm font-bold uppercase tracking-widest text-cyan-400 drop-shadow-[0_0_5px_rgba(6,182,212,0.5)]">
32-
{schema.title}
33-
</h3>
34-
</div>
32+
<h3 className="text-lg font-semibold tracking-tight">
33+
{schema.title}
34+
</h3>
3535
)}
3636

3737
<ListTag
3838
className={cn(
39-
"space-y-1",
39+
"ml-6 [&>li]:mt-2",
40+
listStyle,
4041
className
4142
)}
4243
{...props}
4344
>
44-
{schema.items?.map((item: any, index: number) => (
45-
<li
46-
key={index}
47-
className={cn(
48-
"group flex items-start gap-3 p-2 rounded-sm transition-all duration-300",
49-
"hover:bg-cyan-950/20 hover:pl-3",
50-
typeof item === 'object' && item.className
51-
)}
52-
>
53-
{/* Marker Area */}
54-
<div className="flex-shrink-0 mt-0.5">
55-
{schema.ordered ? (
56-
<span className="flex items-center justify-center w-5 h-5 text-[10px] font-mono font-bold text-slate-950 bg-cyan-600 rounded-sm shadow-[0_0_8px_cyan] group-hover:bg-cyan-400 group-hover:scale-110 transition-all">
57-
{String(index + 1).padStart(2, '0')}
58-
</span>
59-
) : (
60-
<div className="relative flex items-center justify-center w-5 h-5">
61-
<Hexagon className="w-3 h-3 text-cyan-600 group-hover:text-cyan-400 group-hover:rotate-90 transition-transform duration-500" />
62-
<div className="absolute inset-0 bg-cyan-500/20 blur-[2px] rounded-full opacity-0 group-hover:opacity-100 transition-opacity" />
63-
</div>
64-
)}
65-
</div>
66-
67-
{/* Content Area */}
68-
<div className="flex-1 text-sm text-slate-400 group-hover:text-cyan-100 font-medium font-sans leading-tight transition-colors">
69-
{typeof item === 'string' ? item : item.content || renderChildren(item.body)}
70-
</div>
71-
72-
{/* Hover Indicator */}
73-
<ChevronRight className="w-4 h-4 text-cyan-500/0 -translate-x-2 group-hover:text-cyan-500 group-hover:translate-x-0 transition-all duration-300 opacity-0 group-hover:opacity-100" />
45+
{items.map((item: any, index: number) => (
46+
<li key={index} className={cn(typeof item === 'object' && item.className)}>
47+
{typeof item === 'string' ? item : item.content || renderChildren(item.body)}
7448
</li>
7549
))}
7650
</ListTag>

packages/components/src/renderers/data-display/tree-view.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { TreeViewSchema, TreeNode } from '@object-ui/types';
1111
import { ChevronRight, ChevronDown, Folder, File, FolderOpen, CircuitBoard } from 'lucide-react';
1212
import { useState } from 'react';
1313
import { cn } from '../../lib/utils';
14+
import { useDataScope } from '@object-ui/react';
1415

1516
const TreeNodeComponent = ({
1617
node,
@@ -99,6 +100,10 @@ ComponentRegistry.register('tree-view',
99100
}
100101
};
101102

103+
// Support data binding
104+
const boundData = useDataScope(schema.bind);
105+
const nodes = boundData || schema.nodes || schema.data || [];
106+
102107
return (
103108
<div className={cn(
104109
'relative border rounded-lg p-3 bg-card text-card-foreground',
@@ -112,7 +117,7 @@ ComponentRegistry.register('tree-view',
112117
</div>
113118
)}
114119
<div className="space-y-1">
115-
{(schema.nodes || schema.data)?.map((node: TreeNode) => (
120+
{nodes.map((node: TreeNode) => (
116121
<TreeNodeComponent
117122
key={node.id}
118123
node={node}

packages/fields/src/widgets/CodeField.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { Textarea } from '@object-ui/components';
2+
import { Textarea, cn } from '@object-ui/components';
33
import { FieldWidgetProps } from './types';
44

55
/**
@@ -14,7 +14,7 @@ export function CodeField({ value, onChange, field, readonly, ...props }: FieldW
1414

1515
if (readonly) {
1616
return (
17-
<pre className="text-sm bg-muted p-2 rounded overflow-x-auto border">
17+
<pre className={cn("text-sm bg-muted p-2 rounded overflow-x-auto border", props.className)}>
1818
<code>{value || '-'}</code>
1919
</pre>
2020
);
@@ -26,7 +26,7 @@ export function CodeField({ value, onChange, field, readonly, ...props }: FieldW
2626
onChange={(e) => onChange(e.target.value)}
2727
placeholder={config?.placeholder || `// Write ${language} code here...`}
2828
disabled={readonly || props.disabled}
29-
className={`font-mono text-sm ${props.className}`}
29+
className={cn("font-mono text-sm", props.className)}
3030
rows={12}
3131
spellCheck={false}
3232
/>

packages/fields/src/widgets/MasterDetailField.tsx

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { Button, Badge } from '@object-ui/components';
2+
import { Button, Badge, cn } from '@object-ui/components';
33
import { Plus, X, ExternalLink } from 'lucide-react';
44
import { FieldWidgetProps } from './types';
55

@@ -19,9 +19,13 @@ export interface MasterDetailValue {
1919
export function MasterDetailField({
2020
value,
2121
onChange,
22-
readonly
22+
field,
23+
readonly,
24+
className,
25+
...props
2326
}: FieldWidgetProps<MasterDetailValue[]>) {
2427
const items = value || [];
28+
const config = field || (props as any).schema;
2529

2630
const handleAdd = () => {
2731
// This would typically open a dialog to select/create related records
@@ -44,7 +48,7 @@ export function MasterDetailField({
4448

4549
if (readonly) {
4650
return (
47-
<div className="space-y-2">
51+
<div className={cn("space-y-2", className)}>
4852
{items.length === 0 ? (
4953
<span className="text-sm text-muted-foreground">No related records</span>
5054
) : (
@@ -73,7 +77,7 @@ export function MasterDetailField({
7377
}
7478

7579
return (
76-
<div className="space-y-3">
80+
<div className={cn("space-y-3", className)}>
7781
<div className="space-y-2">
7882
{items.map((item) => (
7983
<div
@@ -98,7 +102,33 @@ export function MasterDetailField({
98102
variant="ghost"
99103
size="sm"
100104
onClick={() => handleRemove(item.id)}
101-
disabled={readonly || props.disabled}
105+
disabled={props.disabled}
106+
>
107+
<X className="w-4 h-4" />
108+
</Button>
109+
</div>
110+
</div>
111+
))}
112+
{items.length === 0 && (
113+
<div className="text-sm text-muted-foreground text-center py-4 border border-dashed rounded bg-muted/20">
114+
No related records
115+
</div>
116+
)}
117+
</div>
118+
119+
<Button
120+
type="button"
121+
variant="outline"
122+
className="w-full"
123+
onClick={handleAdd}
124+
disabled={props.disabled}
125+
>
126+
<Plus className="w-4 h-4 mr-2" />
127+
Add {config?.label || 'Record'}
128+
</Button>
129+
</div>
130+
);
131+
} disabled={readonly || props.disabled}
102132
>
103133
<X className="w-4 h-4" />
104134
</Button>
Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React from 'react';
2-
import { Textarea } from '@object-ui/components';
1+
import React, { useState, useEffect } from 'react';
2+
import { Textarea, cn } from '@object-ui/components';
33
import { FieldWidgetProps } from './types';
44

55
/**
@@ -8,20 +8,42 @@ import { FieldWidgetProps } from './types';
88
*/
99
export function ObjectField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<any>) {
1010
const config = field || (props as any).schema;
11+
const [jsonString, setJsonString] = useState('');
12+
const [error, setError] = useState<string | null>(null);
13+
14+
// Initialize/Sync internal string state when value changes externally
15+
useEffect(() => {
16+
try {
17+
if (value === undefined || value === null) {
18+
setJsonString('');
19+
return;
20+
}
21+
// Only update if the parsed internal state doesn't match the new value
22+
// This prevents cursor jumping/reformatting while typing valid JSON
23+
const currentParsed = jsonString ? JSON.parse(jsonString) : null;
24+
if (JSON.stringify(currentParsed) !== JSON.stringify(value)) {
25+
setJsonString(JSON.stringify(value, null, 2));
26+
}
27+
} catch (e) {
28+
// Fallback if internal state was invalid JSON
29+
setJsonString(JSON.stringify(value, null, 2));
30+
}
31+
}, [value]);
32+
1133
if (readonly) {
1234
if (!value) return <span className="text-sm">-</span>;
1335
return (
14-
<pre className="text-xs bg-gray-50 p-2 rounded border border-gray-200 overflow-auto max-h-40">
36+
<pre className={cn("text-xs bg-gray-50 p-2 rounded border border-gray-200 overflow-auto max-h-40", props.className)}>
1537
{JSON.stringify(value, null, 2)}
1638
</pre>
1739
);
1840
}
1941

20-
// Convert object to JSON string for editing
21-
const jsonString = value ? JSON.stringify(value, null, 2) : '';
22-
2342
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
2443
const str = e.target.value;
44+
setJsonString(str);
45+
setError(null);
46+
2547
if (!str.trim()) {
2648
onChange(null);
2749
return;
@@ -30,20 +52,23 @@ export function ObjectField({ value, onChange, field, readonly, ...props }: Fiel
3052
try {
3153
const parsed = JSON.parse(str);
3254
onChange(parsed);
33-
} catch {
34-
// Invalid JSON - keep the current valid value, don't update
35-
// User will need to fix JSON before it's saved
55+
} catch (e) {
56+
// Invalid JSON - don't propagate change to parent, but keep local state
57+
setError("Invalid JSON");
3658
}
3759
};
3860

3961
return (
40-
<Textarea
41-
value={jsonString}
42-
onChange={handleChange}
43-
placeholder={config?.placeholder || '{\n "key": "value"\n}'}
44-
disabled={readonly || props.disabled}
45-
className={`font-mono text-xs ${props.className || ''}`}
46-
rows={6}
47-
/>
62+
<div className="space-y-1">
63+
<Textarea
64+
value={jsonString}
65+
onChange={handleChange}
66+
placeholder={config?.placeholder || '{\n "key": "value"\n}'}
67+
disabled={readonly || props.disabled}
68+
className={cn("font-mono text-xs", error ? "border-red-500 focus-visible:ring-red-500" : "", props.className)}
69+
rows={6}
70+
/>
71+
{error && <p className="text-xs text-red-500">{error}</p>}
72+
</div>
4873
);
4974
}

packages/fields/src/widgets/RatingField.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import React from 'react';
22
import { Star } from 'lucide-react';
3+
import { cn } from '@object-ui/components';
34
import { FieldWidgetProps } from './types';
45

56
/**
67
* Rating field widget - provides a star rating input
78
* Supports numeric values from 0 to max (default 5)
89
*/
9-
export function RatingField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<number>) {
10+
export function RatingField({ value, onChange, field, readonly, className, ...props }: FieldWidgetProps<number>) {
1011
// Get rating-specific configuration from field metadata
1112
const ratingField = (field || (props as any).schema) as any;
1213
const max = ratingField?.max ?? 5;
@@ -18,7 +19,7 @@ export function RatingField({ value, onChange, field, readonly, ...props }: Fiel
1819

1920
if (readonly) {
2021
return (
21-
<div className="flex items-center gap-1">
22+
<div className={cn("flex items-center gap-1", className)}>
2223
{Array.from({ length: max }, (_, i) => (
2324
<Star
2425
key={i}
@@ -37,7 +38,7 @@ export function RatingField({ value, onChange, field, readonly, ...props }: Fiel
3738
}
3839

3940
return (
40-
<div className="flex items-center gap-1">
41+
<div className={cn("flex items-center gap-1", className)}>
4142
{Array.from({ length: max }, (_, i) => (
4243
<button
4344
key={i}

0 commit comments

Comments
 (0)