Skip to content

Commit 59f1217

Browse files
committed
feat: enhance UI components and styles
- Updated App.css with new color variables for improved theming. - Refactored MetadataExplorer and ObjectDataTable components to use new Card and Button components for a consistent UI. - Introduced ScrollArea component for better scrolling experience in lists. - Added utility functions for class name merging. - Implemented process polyfill for better compatibility in browser environments. - Updated Tailwind CSS configuration to support new design tokens. - Enhanced tests to handle new response structures from API.
1 parent 1741a09 commit 59f1217

19 files changed

Lines changed: 533 additions & 110 deletions

examples/app-react-crud/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,14 @@
2323
"@objectstack/plugin-msw": "workspace:*",
2424
"@objectstack/runtime": "workspace:*",
2525
"@objectstack/spec": "workspace:*",
26+
"@radix-ui/react-scroll-area": "^1.2.10",
27+
"@radix-ui/react-slot": "^1.2.4",
28+
"class-variance-authority": "^0.7.1",
29+
"clsx": "^2.1.1",
30+
"lucide-react": "^0.562.0",
2631
"react": "^18.3.1",
27-
"react-dom": "^18.3.1"
32+
"react-dom": "^18.3.1",
33+
"tailwind-merge": "^3.4.0"
2834
},
2935
"devDependencies": {
3036
"@objectstack/cli": "workspace:*",

examples/app-react-crud/src/App.css

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,34 @@
33
@layer base {
44
:root {
55
--background: #ffffff;
6-
--foreground: #000000;
6+
--foreground: #09090b;
7+
8+
--card: #ffffff;
9+
--card-foreground: #09090b;
10+
11+
--popover: #ffffff;
12+
--popover-foreground: #09090b;
13+
14+
--primary: #18181b;
15+
--primary-foreground: #fafafa;
16+
17+
--secondary: #f4f4f5;
18+
--secondary-foreground: #18181b;
19+
20+
--muted: #f4f4f5;
21+
--muted-foreground: #71717a;
22+
23+
--accent: #f4f4f5;
24+
--accent-foreground: #18181b;
25+
26+
--destructive: #ef4444;
27+
--destructive-foreground: #fafafa;
28+
29+
--border: #e4e4e7;
30+
--input: #e4e4e7;
31+
--ring: #18181b;
32+
33+
--radius: 0.5rem;
734

835
--accents-1: #fafafa;
936
--accents-2: #eaeaea;
Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { useState, useEffect } from 'react';
22
import { ObjectStackClient } from '@objectstack/client';
3+
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card";
4+
import { ScrollArea } from "@/components/ui/scroll-area";
5+
import { Button } from "@/components/ui/button";
6+
import { Database, Package } from 'lucide-react';
37

48
interface MetadataExplorerProps {
59
client: ObjectStackClient;
@@ -40,40 +44,52 @@ export function MetadataExplorer({ client, selectedObject, onSelectObject }: Met
4044
}, [client]);
4145

4246
return (
43-
<div className="bg-white rounded-lg border border-accents-2 shadow-sm h-full flex flex-col">
44-
<div className="p-4 border-b border-accents-2 bg-gray-50">
45-
<h3 className="font-bold text-gray-900">Registered Objects</h3>
46-
</div>
47-
<div className="flex-1 overflow-y-auto p-2 space-y-1">
48-
{loading && <div className="p-4 text-center text-gray-500">Loading objects...</div>}
49-
50-
{objects.map(obj => (
51-
<button
52-
key={obj.name}
53-
onClick={() => onSelectObject(obj.name)}
54-
className={`w-full text-left px-3 py-2 rounded-md transition-colors flex items-center justify-between group
55-
${selectedObject === obj.name
56-
? 'bg-primary/5 text-primary font-medium'
57-
: 'text-gray-700 hover:bg-gray-100'
58-
}`}
59-
>
60-
<div className="flex items-center space-x-2">
61-
{/* Icon placeholder could go here */}
62-
<span>{obj.label}</span>
63-
</div>
64-
<span className="text-xs text-gray-400 group-hover:text-gray-600 font-mono">
65-
{obj.name}
66-
</span>
67-
</button>
68-
))}
47+
<Card className="h-full flex flex-col border-border/60">
48+
<CardHeader className="p-4 border-b bg-muted/20">
49+
<CardTitle className="text-sm font-bold flex items-center gap-2">
50+
<Database className="h-4 w-4" />
51+
Registered Objects
52+
</CardTitle>
53+
</CardHeader>
54+
<CardContent className="flex-1 p-0 overflow-hidden">
55+
<ScrollArea className="h-full">
56+
<div className="p-2 space-y-1">
57+
{loading && (
58+
<div className="p-4 text-center text-sm text-muted-foreground animate-pulse">
59+
Loading objects...
60+
</div>
61+
)}
62+
63+
{objects.map(obj => (
64+
<Button
65+
key={obj.name}
66+
variant={selectedObject === obj.name ? "secondary" : "ghost"}
67+
size="sm"
68+
onClick={() => onSelectObject(obj.name)}
69+
className="w-full justify-between font-normal h-9"
70+
>
71+
<div className="flex items-center gap-2 truncate">
72+
<Package className="h-3.5 w-3.5 text-muted-foreground" />
73+
<span>{obj.label}</span>
74+
</div>
75+
<span className="text-xs text-muted-foreground font-mono opacity-50 ml-2 shrink-0">
76+
{obj.name}
77+
</span>
78+
</Button>
79+
))}
6980

70-
{!loading && objects.length === 0 && (
71-
<div className="p-4 text-center text-gray-500 text-sm">No objects found in Metadata Service.</div>
72-
)}
73-
</div>
74-
<div className="p-3 border-t border-accents-2 bg-gray-50 text-xs text-center text-gray-500">
81+
{!loading && objects.length === 0 && (
82+
<div className="p-8 text-center text-muted-foreground text-sm flex flex-col items-center gap-2">
83+
<Database className="h-8 w-8 opacity-20" />
84+
<p>No objects found</p>
85+
</div>
86+
)}
87+
</div>
88+
</ScrollArea>
89+
</CardContent>
90+
<CardFooter className="p-2 border-t bg-muted/20 text-xs text-muted-foreground justify-center">
7591
Total: {objects.length} Objects
76-
</div>
77-
</div>
92+
</CardFooter>
93+
</Card>
7894
);
7995
}

examples/app-react-crud/src/components/ObjectDataTable.tsx

Lines changed: 80 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { useState, useEffect } from 'react';
22
import { ObjectStackClient } from '@objectstack/client';
3+
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card";
4+
import { Button } from "@/components/ui/button";
5+
import { ArrowLeft, ArrowRight, Edit, Trash2, Plus } from 'lucide-react';
36

47
interface ObjectDataTableProps {
58
client: ObjectStackClient;
@@ -42,7 +45,7 @@ export function ObjectDataTable({ client, objectApiName, onEdit }: ObjectDataTab
4245
if (!client) return;
4346
setLoading(true);
4447
try {
45-
const result = await client.data.find(objectApiName, {
48+
const result: any = await client.data.find(objectApiName, {
4649
filters: {
4750
top: pageSize,
4851
skip: (page - 1) * pageSize,
@@ -95,18 +98,9 @@ export function ObjectDataTable({ client, objectApiName, onEdit }: ObjectDataTab
9598
}
9699
}
97100

98-
// Helper to allow debugging and simple formatting
99-
function formatRecords(recs: any[], cols: any[]) {
100-
if (!recs || recs.length === 0) return [];
101-
// Optional: Ensure fields that match columns exist or default to ''
102-
return recs;
103-
}
104-
105-
if (!def) return <div className="p-4 text-accents-5">Loading metadata for {objectApiName}...</div>;
101+
if (!def) return <div className="p-4 text-muted-foreground animate-pulse">Loading metadata for {objectApiName}...</div>;
106102

107103
// Determine columns from fields
108-
// fields is usually a map or array depending on the internal structure.
109-
// Based on previous logs: objects: [{ fields: { ... } }]
110104
const fields = def.fields || {};
111105
const columns = Object.keys(fields).map(key => {
112106
const f = fields[key];
@@ -115,78 +109,107 @@ export function ObjectDataTable({ client, objectApiName, onEdit }: ObjectDataTab
115109
label: f.label || key,
116110
type: f.type || 'text'
117111
};
118-
}).filter(c => !['formatted_summary'].includes(c.name)); // hide system fields if any
112+
}).filter(c => !['formatted_summary'].includes(c.name));
119113

120114
return (
121-
<div className="bg-background rounded-lg border border-accents-2 overflow-hidden shadow-sm">
122-
<div className="p-4 border-b border-accents-2 flex justify-between items-center bg-gray-50">
123-
<h3 className="font-bold text-lg">{def.label} ({def.name})</h3>
124-
<span className="text-sm text-accents-5">
125-
Records: {total > 0 ? total : records.length}
126-
</span>
127-
</div>
115+
<Card className="flex flex-col h-full border-border/60 shadow-sm">
116+
<CardHeader className="flex flex-row items-center justify-between p-4 border-b space-y-0 bg-muted/20">
117+
<div className="space-y-1">
118+
<CardTitle className="text-xl font-semibold tracking-tight">
119+
{def.label}
120+
</CardTitle>
121+
<p className="text-sm text-muted-foreground">
122+
{def.name}{total > 0 ? total : records.length} records
123+
</p>
124+
</div>
125+
<Button onClick={() => onEdit({})} size="sm" className="gap-1">
126+
<Plus className="h-4 w-4" />
127+
New
128+
</Button>
129+
</CardHeader>
128130

129-
<div className="overflow-x-auto">
130-
<table className="w-full text-sm text-left">
131-
<thead className="bg-gray-100 text-accents-6 uppercase font-medium">
132-
<tr>
131+
<CardContent className="flex-1 p-0 overflow-auto">
132+
<table className="w-full caption-bottom text-sm text-left">
133+
<thead className="[&_tr]:border-b">
134+
<tr className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
133135
{columns.map(col => (
134-
<th key={col.name} className="px-4 py-3 whitespace-nowrap">{col.label}</th>
136+
<th key={col.name} className="h-10 px-4 align-middle font-medium text-muted-foreground">
137+
{col.label}
138+
</th>
135139
))}
136-
<th className="px-4 py-3 text-right">Actions</th>
140+
<th className="h-10 px-4 align-middle font-medium text-muted-foreground text-right">Actions</th>
137141
</tr>
138142
</thead>
139-
<tbody className="divide-y divide-accents-2">
143+
<tbody className="[&_tr:last-child]:border-0">
140144
{loading && records.length === 0 ? (
141-
<tr><td colSpan={columns.length + 1} className="p-4 text-center">Loading...</td></tr>
142-
) : formatRecords(records, columns).map(record => (
143-
<tr key={record.id || record._id} className="hover:bg-gray-50 transition-colors">
145+
<tr><td colSpan={columns.length + 1} className="p-4 text-center text-muted-foreground">Loading...</td></tr>
146+
) : records.map(record => (
147+
<tr key={record.id || record._id} className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
144148
{columns.map(col => (
145-
<td key={col.name} className="px-4 py-3 whitespace-nowrap">
149+
<td key={col.name} className="p-4 align-middle">
146150
{String(record[col.name] !== undefined ? record[col.name] : '')}
147151
</td>
148152
))}
149-
<td className="px-4 py-3 text-right space-x-2">
150-
<button
151-
onClick={() => onEdit(record)}
152-
className="text-primary hover:text-primary-dark font-medium"
153-
>
154-
Edit
155-
</button>
156-
<button
157-
onClick={() => handleDelete(record.id || record._id)}
158-
className="text-error hover:text-red-700 font-medium"
159-
>
160-
Delete
161-
</button>
153+
<td className="p-4 align-middle text-right">
154+
<div className="flex justify-end gap-2">
155+
<Button
156+
variant="ghost"
157+
size="sm"
158+
className="h-8 w-8 p-0"
159+
onClick={() => onEdit(record)}
160+
>
161+
<Edit className="h-4 w-4 text-muted-foreground" />
162+
<span className="sr-only">Edit</span>
163+
</Button>
164+
<Button
165+
variant="ghost"
166+
size="sm"
167+
className="h-8 w-8 p-0"
168+
onClick={() => handleDelete(record.id || record._id)}
169+
>
170+
<Trash2 className="h-4 w-4 text-destructive" />
171+
<span className="sr-only">Delete</span>
172+
</Button>
173+
</div>
162174
</td>
163175
</tr>
164176
))}
165177
{!loading && records.length === 0 && (
166-
<tr><td colSpan={columns.length + 1} className="p-8 text-center text-accents-5">No records found</td></tr>
178+
<tr>
179+
<td colSpan={columns.length + 1} className="p-8 text-center text-muted-foreground">
180+
No records found
181+
</td>
182+
</tr>
167183
)}
168184
</tbody>
169185
</table>
170-
</div>
186+
</CardContent>
171187

172-
{/* Pagination Controls */}
173-
<div className="p-3 border-t border-accents-2 flex justify-end items-center gap-2 bg-gray-50">
174-
<button
188+
<CardFooter className="p-2 border-t bg-muted/20 flex justify-end items-center gap-2">
189+
<Button
190+
variant="outline"
191+
size="sm"
175192
disabled={page === 1}
176193
onClick={() => setPage(p => Math.max(1, p - 1))}
177-
className="px-3 py-1 border border-accents-3 rounded text-sm disabled:opacity-50 bg-white"
194+
className="h-8 gap-1"
178195
>
196+
<ArrowLeft className="h-3.5 w-3.5" />
179197
Previous
180-
</button>
181-
<span className="text-sm font-medium px-2">Page {page}</span>
182-
<button
183-
disabled={records.length < pageSize} // Simple check
198+
</Button>
199+
<div className="text-sm font-medium text-muted-foreground min-w-[3rem] text-center">
200+
Page {page}
201+
</div>
202+
<Button
203+
variant="outline"
204+
size="sm"
205+
disabled={records.length < pageSize}
184206
onClick={() => setPage(p => p + 1)}
185-
className="px-3 py-1 border border-accents-3 rounded text-sm disabled:opacity-50 bg-white"
207+
className="h-8 gap-1"
186208
>
187209
Next
188-
</button>
189-
</div>
190-
</div>
210+
<ArrowRight className="h-3.5 w-3.5" />
211+
</Button>
212+
</CardFooter>
213+
</Card>
191214
);
192215
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import * as React from "react"
2+
import { Slot } from "@radix-ui/react-slot"
3+
import { cva, type VariantProps } from "class-variance-authority"
4+
5+
import { cn } from "@/lib/utils"
6+
7+
const buttonVariants = cva(
8+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9+
{
10+
variants: {
11+
variant: {
12+
default:
13+
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
14+
destructive:
15+
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16+
outline:
17+
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18+
secondary:
19+
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20+
ghost: "hover:bg-accent hover:text-accent-foreground",
21+
link: "text-primary underline-offset-4 hover:underline",
22+
},
23+
size: {
24+
default: "h-9 px-4 py-2",
25+
sm: "h-8 rounded-md px-3 text-xs",
26+
lg: "h-10 rounded-md px-8",
27+
icon: "h-9 w-9",
28+
},
29+
},
30+
defaultVariants: {
31+
variant: "default",
32+
size: "default",
33+
},
34+
}
35+
)
36+
37+
export interface ButtonProps
38+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
39+
VariantProps<typeof buttonVariants> {
40+
asChild?: boolean
41+
}
42+
43+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44+
({ className, variant, size, asChild = false, ...props }, ref) => {
45+
const Comp = asChild ? Slot : "button"
46+
return (
47+
<Comp
48+
className={cn(buttonVariants({ variant, size, className }))}
49+
ref={ref}
50+
{...props}
51+
/>
52+
)
53+
}
54+
)
55+
Button.displayName = "Button"
56+
57+
export { Button, buttonVariants }

0 commit comments

Comments
 (0)