Skip to content

Commit 8a2c443

Browse files
authored
Merge pull request #8 from objectql/copilot/add-live-playground
2 parents 71bd191 + 57cab78 commit 8a2c443

16 files changed

Lines changed: 1225 additions & 0 deletions

apps/playground/.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

apps/playground/README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Object UI Playground
2+
3+
A live, interactive playground to showcase Object UI's schema-driven rendering capabilities.
4+
5+
## Features
6+
7+
- **Split View Editor**: JSON editor with syntax validation on the left, live preview on the right
8+
- **Real-time Rendering**: See your changes instantly as you edit JSON schemas
9+
- **Example Gallery**: Curated examples organized by category (Primitives, Layouts, Forms)
10+
- **Responsive Preview**: Toggle between desktop, tablet, and mobile viewports
11+
- **Copy Schema**: One-click copy to clipboard for easy integration
12+
- **Error Highlighting**: Clear JSON syntax error messages
13+
14+
## Examples Included
15+
16+
### Primitives
17+
- Simple page layouts
18+
- Input component states (required, disabled, email)
19+
- Button variants (destructive, outline, ghost, etc.)
20+
21+
### Layouts
22+
- Responsive grid layouts
23+
- Analytics dashboard with KPI cards
24+
- Tabs component demonstration
25+
26+
### Forms
27+
- User registration form with various input types
28+
- Grid-based form layouts
29+
30+
## Running the Playground
31+
32+
From the monorepo root:
33+
34+
```bash
35+
pnpm install
36+
pnpm --filter @apps/playground dev
37+
```
38+
39+
Or from this directory:
40+
41+
```bash
42+
pnpm install
43+
pnpm dev
44+
```
45+
46+
The playground will be available at `http://localhost:5174`
47+
48+
## Building for Production
49+
50+
```bash
51+
pnpm build
52+
```
53+
54+
## Purpose
55+
56+
This playground serves as:
57+
58+
1. **Product Demo**: Show what Object UI can do without any backend
59+
2. **Learning Tool**: Help developers understand schema structure
60+
3. **Testing Ground**: Experiment with different configurations
61+
4. **Documentation**: Live, interactive examples are better than static code samples
62+
63+
## Key Selling Points Demonstrated
64+
65+
-**Tailwind Native**: Edit `className` properties and see instant results
66+
-**Schema-Driven**: Everything is pure JSON - no JSX needed
67+
-**Responsive**: Built-in responsive grid layouts
68+
-**Complete**: From simple buttons to complex dashboards
69+
-**Standalone**: No backend required - works with any data source

apps/playground/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Object UI - Live Playground</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>

apps/playground/package.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "@apps/playground",
3+
"private": true,
4+
"license": "MIT",
5+
"version": "0.0.0",
6+
"type": "module",
7+
"scripts": {
8+
"dev": "vite",
9+
"build": "tsc -b && vite build",
10+
"lint": "eslint .",
11+
"preview": "vite preview"
12+
},
13+
"dependencies": {
14+
"@object-ui/components": "workspace:*",
15+
"@object-ui/core": "workspace:*",
16+
"@object-ui/react": "workspace:*",
17+
"lucide-react": "^0.469.0",
18+
"react": "^18.3.1",
19+
"react-dom": "^18.3.1"
20+
},
21+
"devDependencies": {
22+
"@types/react": "^18.3.12",
23+
"@types/react-dom": "^18.3.1",
24+
"@vitejs/plugin-react": "^5.1.1",
25+
"autoprefixer": "^10.4.23",
26+
"postcss": "^8.5.6",
27+
"tailwindcss": "^3.4.19",
28+
"tailwindcss-animate": "^1.0.7",
29+
"typescript": "~5.9.3",
30+
"vite": "^7.2.4"
31+
}
32+
}

apps/playground/postcss.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default {
2+
plugins: {
3+
tailwindcss: {},
4+
autoprefixer: {},
5+
},
6+
}

apps/playground/src/App.tsx

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { useState, useEffect } from 'react';
2+
import { SchemaRenderer } from '@object-ui/react';
3+
import '@object-ui/components';
4+
import { examples, exampleCategories, ExampleKey } from './data/examples';
5+
import { Monitor, Tablet, Smartphone, Copy, Check, Code2 } from 'lucide-react';
6+
7+
type ViewportSize = 'desktop' | 'tablet' | 'mobile';
8+
9+
export default function Playground() {
10+
const [selectedExample, setSelectedExample] = useState<ExampleKey>('dashboard');
11+
const [code, setCode] = useState(examples['dashboard']);
12+
const [schema, setSchema] = useState<any>(null);
13+
const [jsonError, setJsonError] = useState<string | null>(null);
14+
const [viewportSize, setViewportSize] = useState<ViewportSize>('desktop');
15+
const [copied, setCopied] = useState(false);
16+
17+
// Real-time JSON parsing
18+
useEffect(() => {
19+
try {
20+
const parsed = JSON.parse(code);
21+
setSchema(parsed);
22+
setJsonError(null);
23+
} catch (e) {
24+
setJsonError((e as Error).message);
25+
// Keep previous schema on error
26+
}
27+
}, [code]);
28+
29+
const handleExampleChange = (key: ExampleKey) => {
30+
setSelectedExample(key);
31+
setCode(examples[key]);
32+
};
33+
34+
const handleCopySchema = async () => {
35+
try {
36+
await navigator.clipboard.writeText(code);
37+
setCopied(true);
38+
setTimeout(() => setCopied(false), 2000);
39+
} catch (err) {
40+
console.error('Failed to copy:', err);
41+
}
42+
};
43+
44+
const viewportStyles: Record<ViewportSize, string> = {
45+
desktop: 'w-full',
46+
tablet: 'max-w-3xl mx-auto',
47+
mobile: 'max-w-md mx-auto'
48+
};
49+
50+
return (
51+
<div className="flex h-screen w-screen bg-background">
52+
{/* 1. Sidebar: Example Selector */}
53+
<aside className="w-64 bg-muted/30 border-r overflow-auto">
54+
<div className="p-6 border-b">
55+
<h1 className="text-xl font-bold">Object UI</h1>
56+
<p className="text-sm text-muted-foreground mt-1">Live Playground</p>
57+
</div>
58+
59+
<div className="p-4 space-y-6">
60+
{Object.entries(exampleCategories).map(([category, keys]) => (
61+
<div key={category}>
62+
<h2 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
63+
{category}
64+
</h2>
65+
<div className="space-y-1">
66+
{keys.map((key) => (
67+
<button
68+
key={key}
69+
onClick={() => handleExampleChange(key as ExampleKey)}
70+
className={`
71+
block w-full text-left px-3 py-2 rounded-md text-sm transition-colors
72+
${key === selectedExample
73+
? 'bg-primary text-primary-foreground font-medium'
74+
: 'hover:bg-muted text-foreground'
75+
}
76+
`}
77+
>
78+
{key.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}
79+
</button>
80+
))}
81+
</div>
82+
</div>
83+
))}
84+
</div>
85+
</aside>
86+
87+
{/* 2. Middle: Code Editor */}
88+
<div className="w-1/2 h-full border-r flex flex-col">
89+
<div className="border-b px-4 py-3 bg-muted/20 flex items-center justify-between">
90+
<div className="flex items-center gap-2">
91+
<Code2 className="h-4 w-4 text-muted-foreground" />
92+
<h2 className="text-sm font-semibold">JSON Schema</h2>
93+
</div>
94+
<button
95+
onClick={handleCopySchema}
96+
className="flex items-center gap-2 px-3 py-1.5 text-xs rounded-md bg-background hover:bg-muted transition-colors border"
97+
>
98+
{copied ? (
99+
<>
100+
<Check className="h-3 w-3" />
101+
Copied!
102+
</>
103+
) : (
104+
<>
105+
<Copy className="h-3 w-3" />
106+
Copy Schema
107+
</>
108+
)}
109+
</button>
110+
</div>
111+
112+
{jsonError && (
113+
<div className="px-4 py-2 bg-red-50 border-b border-red-200 text-red-700 text-sm">
114+
<strong>JSON Error:</strong> {jsonError}
115+
</div>
116+
)}
117+
118+
<div className="flex-1 overflow-hidden">
119+
<textarea
120+
value={code}
121+
onChange={(e) => setCode(e.target.value)}
122+
className="w-full h-full p-4 font-mono text-sm resize-none focus:outline-none border-0 bg-background"
123+
spellCheck={false}
124+
style={{
125+
tabSize: 2,
126+
lineHeight: '1.6'
127+
}}
128+
/>
129+
</div>
130+
</div>
131+
132+
{/* 3. Right: Live Preview */}
133+
<div className="w-1/2 h-full flex flex-col bg-muted/10">
134+
<div className="border-b px-4 py-3 bg-muted/20 flex items-center justify-between">
135+
<h2 className="text-sm font-semibold">Preview</h2>
136+
137+
{/* Viewport Size Toggles */}
138+
<div className="flex items-center gap-1 bg-background rounded-md p-1 border">
139+
<button
140+
onClick={() => setViewportSize('desktop')}
141+
className={`p-1.5 rounded transition-colors ${
142+
viewportSize === 'desktop' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'
143+
}`}
144+
title="Desktop View"
145+
>
146+
<Monitor className="h-4 w-4" />
147+
</button>
148+
<button
149+
onClick={() => setViewportSize('tablet')}
150+
className={`p-1.5 rounded transition-colors ${
151+
viewportSize === 'tablet' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'
152+
}`}
153+
title="Tablet View"
154+
>
155+
<Tablet className="h-4 w-4" />
156+
</button>
157+
<button
158+
onClick={() => setViewportSize('mobile')}
159+
className={`p-1.5 rounded transition-colors ${
160+
viewportSize === 'mobile' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'
161+
}`}
162+
title="Mobile View"
163+
>
164+
<Smartphone className="h-4 w-4" />
165+
</button>
166+
</div>
167+
</div>
168+
169+
<div className="flex-1 overflow-auto p-8">
170+
<div className={`${viewportStyles[viewportSize]} transition-all duration-300`}>
171+
<div className="rounded-lg border shadow-sm bg-background p-6">
172+
{schema && !jsonError ? (
173+
<SchemaRenderer schema={schema} />
174+
) : (
175+
<div className="text-center py-12 text-muted-foreground">
176+
{jsonError ? (
177+
<div className="space-y-2">
178+
<p className="text-red-500 font-semibold">Invalid JSON</p>
179+
<p className="text-sm">Fix the syntax error to see the preview</p>
180+
</div>
181+
) : (
182+
<p>Select an example to get started</p>
183+
)}
184+
</div>
185+
)}
186+
</div>
187+
</div>
188+
</div>
189+
</div>
190+
</div>
191+
);
192+
}

0 commit comments

Comments
 (0)