Skip to content

Commit 90586fe

Browse files
committed
feat: add reusable UI components including Dialog, Resizable, Separator, Sheet, and Tooltip
- Implemented Dialog component with overlay, content, header, footer, title, and description. - Created ResizablePanelGroup, ResizablePanel, and ResizableHandle for flexible panel layouts. - Added Separator component for visual separation in layouts. - Developed Sheet component with customizable content and overlay, including header, footer, title, and description. - Introduced Tooltip component for displaying additional information on hover.
1 parent cb1956a commit 90586fe

9 files changed

Lines changed: 767 additions & 74 deletions

File tree

examples/app-react-crud/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,18 @@
2323
"@objectstack/plugin-msw": "workspace:*",
2424
"@objectstack/runtime": "workspace:*",
2525
"@objectstack/spec": "workspace:*",
26+
"@radix-ui/react-dialog": "^1.1.15",
27+
"@radix-ui/react-label": "^2.1.8",
2628
"@radix-ui/react-scroll-area": "^1.2.10",
29+
"@radix-ui/react-separator": "^1.1.8",
2730
"@radix-ui/react-slot": "^1.2.4",
31+
"@radix-ui/react-tooltip": "^1.2.8",
2832
"class-variance-authority": "^0.7.1",
2933
"clsx": "^2.1.1",
3034
"lucide-react": "^0.562.0",
3135
"react": "^18.3.1",
3236
"react-dom": "^18.3.1",
37+
"react-resizable-panels": "^4.6.1",
3338
"tailwind-merge": "^3.4.0"
3439
},
3540
"devDependencies": {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import { MetadataExplorer } from './components/MetadataExplorer';
1414
import { ObjectDataTable } from './components/ObjectDataTable';
1515
import { ObjectDataForm } from './components/ObjectDataForm';
1616
import { Badge } from "@/components/ui/badge";
17+
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
18+
import { ScrollArea } from "@/components/ui/scroll-area";
19+
import { Separator } from "@/components/ui/separator";
1720

1821
export function App() {
1922
const [client, setClient] = useState<ObjectStackClient | null>(null);

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

Lines changed: 82 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { Input } from "@/components/ui/input";
55
import { Textarea } from "@/components/ui/textarea";
66
import { Label } from "@/components/ui/label";
77
import { SelectNative } from "@/components/ui/select-native";
8-
import { X, Save } from "lucide-react";
8+
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter } from "@/components/ui/sheet";
9+
import { ScrollArea } from "@/components/ui/scroll-area";
10+
import { Save, Loader2 } from "lucide-react";
911

1012
interface ObjectDataFormProps {
1113
client: ObjectStackClient;
@@ -101,94 +103,100 @@ export function ObjectDataForm({ client, objectApiName, record, onSuccess, onCan
101103
});
102104

103105
return (
104-
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center p-4 z-50">
105-
<div className="bg-background rounded-lg border border-border shadow-lg w-full max-w-lg overflow-hidden flex flex-col max-h-[90vh]">
106-
<div className="p-4 border-b border-border bg-muted/40 flex justify-between items-center">
107-
<h3 className="font-semibold text-lg">
106+
<Sheet open={true} onOpenChange={(open) => !open && onCancel()}>
107+
<SheetContent className="w-full sm:max-w-xl flex flex-col p-0 gap-0">
108+
<SheetHeader className="p-6 border-b bg-muted/10">
109+
<SheetTitle>
108110
{record ? `Edit ${def.label}` : `New ${def.label}`}
109-
</h3>
110-
<Button variant="ghost" size="icon" onClick={onCancel}>
111-
<X className="h-4 w-4" />
112-
</Button>
113-
</div>
111+
</SheetTitle>
112+
<SheetDescription>
113+
{record ? `Make changes to your ${def.label.toLowerCase()} here.` : `Add a new ${def.label.toLowerCase()} to your database.`}
114+
</SheetDescription>
115+
</SheetHeader>
114116

115117
<form onSubmit={handleSubmit} className="flex-1 flex flex-col overflow-hidden">
116-
<div className="flex-1 overflow-y-auto p-6 space-y-4">
117-
{error && (
118-
<div className="bg-destructive/15 text-destructive p-3 rounded text-sm mb-4">
119-
{error}
120-
</div>
121-
)}
122-
123-
{fieldKeys.map(key => {
124-
const field = fields[key];
125-
const label = field.label || key;
126-
const required = field.required;
118+
<ScrollArea className="flex-1">
119+
<div className="p-6 space-y-6">
120+
{error && (
121+
<div className="bg-destructive/15 text-destructive p-3 rounded text-sm mb-4">
122+
{error}
123+
</div>
124+
)}
125+
126+
{fieldKeys.map(key => {
127+
const field = fields[key];
128+
const label = field.label || key;
129+
const required = field.required;
127130

128-
return (
129-
<div key={key} className="space-y-2">
130-
<Label>
131-
{label} {required && <span className="text-destructive">*</span>}
132-
</Label>
133-
134-
{field.type === 'boolean' ? (
135-
<div className="flex items-center space-x-2">
136-
<input
137-
type="checkbox"
138-
checked={!!formData[key]}
139-
onChange={e => handleChange(key, e.target.checked)}
140-
className="h-4 w-4 rounded border-primary text-primary focus:ring-ring"
131+
return (
132+
<div key={key} className="space-y-2">
133+
<Label>
134+
{label} {required && <span className="text-destructive">*</span>}
135+
</Label>
136+
137+
{field.type === 'boolean' ? (
138+
<div className="flex items-center space-x-2">
139+
<input
140+
type="checkbox"
141+
checked={!!formData[key]}
142+
onChange={e => handleChange(key, e.target.checked)}
143+
className="h-4 w-4 rounded border-primary text-primary focus:ring-ring"
144+
/>
145+
<span className="text-sm text-muted-foreground">Enabled</span>
146+
</div>
147+
) : field.type === 'select' ? (
148+
<SelectNative
149+
value={formData[key] || ''}
150+
onChange={e => handleChange(key, e.target.value)}
151+
required={required}
152+
>
153+
<option value="">-- Select --</option>
154+
{field.options?.map((opt: any) => (
155+
<option key={opt.value} value={opt.value}>{opt.label}</option>
156+
))}
157+
</SelectNative>
158+
) : field.type === 'textarea' ? (
159+
<Textarea
160+
value={formData[key] || ''}
161+
onChange={e => handleChange(key, e.target.value)}
162+
rows={3}
163+
required={required}
141164
/>
142-
<span className="text-sm text-muted-foreground">Enabled</span>
143-
</div>
144-
) : field.type === 'select' ? (
145-
<SelectNative
146-
value={formData[key] || ''}
147-
onChange={e => handleChange(key, e.target.value)}
148-
required={required}
149-
>
150-
<option value="">-- Select --</option>
151-
{field.options?.map((opt: any) => (
152-
<option key={opt.value} value={opt.value}>{opt.label}</option>
153-
))}
154-
</SelectNative>
155-
) : field.type === 'textarea' ? (
156-
<Textarea
157-
value={formData[key] || ''}
158-
onChange={e => handleChange(key, e.target.value)}
159-
rows={3}
160-
required={required}
161-
/>
162-
) : (
163-
<Input
164-
type={field.type === 'number' ? 'number' : 'text'}
165-
value={formData[key] || ''}
166-
onChange={e => handleChange(key, e.target.value)}
167-
required={required}
168-
/>
169-
)}
170-
{field.description && (
171-
<p className="text-xs text-muted-foreground">{field.description}</p>
172-
)}
173-
</div>
174-
);
175-
})}
176-
</div>
165+
) : (
166+
<Input
167+
type={field.type === 'number' ? 'number' : 'text'}
168+
value={formData[key] || ''}
169+
onChange={e => handleChange(key, e.target.value)}
170+
required={required}
171+
/>
172+
)}
173+
{field.description && (
174+
<p className="text-xs text-muted-foreground">{field.description}</p>
175+
)}
176+
</div>
177+
);
178+
})}
179+
</div>
180+
</ScrollArea>
177181

178-
<div className="p-4 border-t border-border bg-muted/40 flex items-center justify-end space-x-3">
182+
<SheetFooter className="p-6 border-t bg-muted/10 items-center gap-2 sm:justify-end">
179183
<Button variant="outline" onClick={onCancel} type="button">
180184
Cancel
181185
</Button>
182186
<Button type="submit" disabled={loading}>
183-
{loading ? 'Saving...' : (
187+
{loading ? (
188+
<>
189+
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Saving...
190+
</>
191+
) : (
184192
<>
185193
<Save className="mr-2 h-4 w-4" /> Save
186194
</>
187195
)}
188196
</Button>
189-
</div>
197+
</SheetFooter>
190198
</form>
191-
</div>
192-
</div>
199+
</SheetContent>
200+
</Sheet>
193201
);
194202
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import * as DialogPrimitive from "@radix-ui/react-dialog"
5+
import { X } from "lucide-react"
6+
7+
import { cn } from "@/lib/utils"
8+
9+
const Dialog = DialogPrimitive.Root
10+
11+
const DialogTrigger = DialogPrimitive.Trigger
12+
13+
const DialogPortal = DialogPrimitive.Portal
14+
15+
const DialogClose = DialogPrimitive.Close
16+
17+
const DialogOverlay = React.forwardRef<
18+
React.ElementRef<typeof DialogPrimitive.Overlay>,
19+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
20+
>(({ className, ...props }, ref) => (
21+
<DialogPrimitive.Overlay
22+
ref={ref}
23+
className={cn(
24+
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
25+
className
26+
)}
27+
{...props}
28+
/>
29+
))
30+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31+
32+
const DialogContent = React.forwardRef<
33+
React.ElementRef<typeof DialogPrimitive.Content>,
34+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
35+
>(({ className, children, ...props }, ref) => (
36+
<DialogPortal>
37+
<DialogOverlay />
38+
<DialogPrimitive.Content
39+
ref={ref}
40+
className={cn(
41+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-1/2 data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-1/2 sm:rounded-lg",
42+
className
43+
)}
44+
{...props}
45+
>
46+
{children}
47+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
48+
<X className="h-4 w-4" />
49+
<span className="sr-only">Close</span>
50+
</DialogPrimitive.Close>
51+
</DialogPrimitive.Content>
52+
</DialogPortal>
53+
))
54+
DialogContent.displayName = DialogPrimitive.Content.displayName
55+
56+
const DialogHeader = ({
57+
className,
58+
...props
59+
}: React.HTMLAttributes<HTMLDivElement>) => (
60+
<div
61+
className={cn(
62+
"flex flex-col space-y-1.5 text-center sm:text-left",
63+
className
64+
)}
65+
{...props}
66+
/>
67+
)
68+
DialogHeader.displayName = "DialogHeader"
69+
70+
const DialogFooter = ({
71+
className,
72+
...props
73+
}: React.HTMLAttributes<HTMLDivElement>) => (
74+
<div
75+
className={cn(
76+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
77+
className
78+
)}
79+
{...props}
80+
/>
81+
)
82+
DialogFooter.displayName = "DialogFooter"
83+
84+
const DialogTitle = React.forwardRef<
85+
React.ElementRef<typeof DialogPrimitive.Title>,
86+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
87+
>(({ className, ...props }, ref) => (
88+
<DialogPrimitive.Title
89+
ref={ref}
90+
className={cn(
91+
"text-lg font-semibold leading-none tracking-tight",
92+
className
93+
)}
94+
{...props}
95+
/>
96+
))
97+
DialogTitle.displayName = DialogPrimitive.Title.displayName
98+
99+
const DialogDescription = React.forwardRef<
100+
React.ElementRef<typeof DialogPrimitive.Description>,
101+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
102+
>(({ className, ...props }, ref) => (
103+
<DialogPrimitive.Description
104+
ref={ref}
105+
className={cn("text-sm text-muted-foreground", className)}
106+
{...props}
107+
/>
108+
))
109+
DialogDescription.displayName = DialogPrimitive.Description.displayName
110+
111+
export {
112+
Dialog,
113+
DialogPortal,
114+
DialogOverlay,
115+
DialogClose,
116+
DialogTrigger,
117+
DialogContent,
118+
DialogHeader,
119+
DialogFooter,
120+
DialogTitle,
121+
DialogDescription,
122+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import { PanelResizeHandle as PanelResizeHandlePrimitive } from "react-resizable-panels"
5+
6+
import { cn } from "@/lib/utils"
7+
8+
const ResizablePanelGroup = ({
9+
className,
10+
...props
11+
}: React.ComponentProps<typeof import("react-resizable-panels").PanelGroup>) => (
12+
<import("react-resizable-panels").PanelGroup
13+
className={cn(
14+
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
15+
className
16+
)}
17+
{...props}
18+
/>
19+
)
20+
21+
const ResizablePanel = import("react-resizable-panels").Panel
22+
23+
const ResizableHandle = ({
24+
withHandle,
25+
className,
26+
...props
27+
}: React.ComponentProps<typeof PanelResizeHandlePrimitive> & {
28+
withHandle?: boolean
29+
}) => (
30+
<PanelResizeHandlePrimitive
31+
className={cn(
32+
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
33+
className
34+
)}
35+
{...props}
36+
>
37+
{withHandle && (
38+
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
39+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" className="h-2.5 w-2.5"><path d="M5.5 4.625C5.5 4.41789 5.33211 4.25 5.125 4.25C4.91789 4.25 4.75 4.41789 4.75 4.625V10.375C4.75 10.5821 4.91789 10.75 5.125 10.75C5.33211 10.75 5.5 10.5821 5.5 10.375V4.625ZM9.5 4.625C9.5 4.41789 9.33211 4.25 9.125 4.25C8.91789 4.25 8.75 4.41789 8.75 4.625V10.375C8.75 10.5821 8.91789 10.75 9.125 10.75C9.33211 10.75 9.5 10.5821 9.5 10.375V4.625Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"></path></svg>
40+
</div>
41+
)}
42+
</PanelResizeHandlePrimitive>
43+
)
44+
45+
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

0 commit comments

Comments
 (0)