Skip to content

Commit ac2ca08

Browse files
committed
Refactor playground to use React Router and modular pages
Replaces the monolithic App component in the playground with a React Router-based structure, introducing separate Home and Studio pages. Adds react-router-dom as a dependency, updates Designer exports to allow embedding the toolbar externally, and removes the internal Toolbar from Designer. This enables a more scalable, modular playground architecture with improved navigation and separation of concerns.
1 parent 4b468a9 commit ac2ca08

7 files changed

Lines changed: 597 additions & 335 deletions

File tree

apps/playground/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@
1313
"dependencies": {
1414
"@object-ui/components": "workspace:*",
1515
"@object-ui/core": "workspace:*",
16-
"@object-ui/react": "workspace:*",
1716
"@object-ui/designer": "workspace:*",
17+
"@object-ui/react": "workspace:*",
1818
"lucide-react": "^0.469.0",
1919
"react": "^18.3.1",
20-
"react-dom": "^18.3.1"
20+
"react-dom": "^18.3.1",
21+
"react-router-dom": "^7.12.0"
2122
},
2223
"devDependencies": {
2324
"@eslint/js": "^9.39.1",

apps/playground/src/App.tsx

Lines changed: 15 additions & 325 deletions
Original file line numberDiff line numberDiff line change
@@ -1,330 +1,20 @@
1-
import { useState, useEffect } from 'react';
2-
import { SchemaRenderer } from '@object-ui/react';
3-
import { Designer } from '@object-ui/designer';
4-
import { SchemaNode } from '@object-ui/core';
5-
import '@object-ui/components';
6-
import { examples, exampleCategories, ExampleKey } from './data/examples';
7-
import { Monitor, Tablet, Smartphone, Copy, Check, Code2, PenTool, LayoutTemplate } from 'lucide-react';
1+
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
2+
import { Home } from './pages/Home';
3+
import { Studio } from './pages/Studio';
4+
import '@object-ui/components';
85

9-
type ViewportSize = 'desktop' | 'tablet' | 'mobile';
10-
type ViewMode = 'code' | 'design' | 'preview';
11-
12-
export default function Playground() {
13-
const [selectedExample, setSelectedExample] = useState<ExampleKey>('dashboard');
14-
const [viewMode, setViewMode] = useState<ViewMode>('code');
15-
16-
// Initialize state based on default example
17-
const initialCode = examples['dashboard'];
18-
const [code, setCode] = useState(initialCode);
19-
20-
const [schema, setSchema] = useState<SchemaNode | null>(() => {
21-
try {
22-
return JSON.parse(initialCode) as SchemaNode;
23-
} catch {
24-
return null;
25-
}
26-
});
27-
28-
// Sync code to schema when switching examples
29-
useEffect(() => {
30-
// When selectedExample changes, updating 'code' triggers parsing.
31-
// But we also need to ensure 'schema' is fresh for the Designer.
32-
try {
33-
const newSchema = JSON.parse(code) as SchemaNode;
34-
setSchema(newSchema);
35-
} catch {
36-
// ignore
37-
}
38-
}, [code]);
39-
40-
const [jsonError, setJsonError] = useState<string | null>(null);
41-
const [viewportSize, setViewportSize] = useState<ViewportSize>('desktop');
42-
const [copied, setCopied] = useState(false);
43-
44-
const updateCode = (newCode: string) => {
45-
setCode(newCode);
46-
try {
47-
const parsed = JSON.parse(newCode) as SchemaNode;
48-
setSchema(parsed);
49-
setJsonError(null);
50-
} catch (e) {
51-
setJsonError((e as Error).message);
52-
// Keep previous schema on error
53-
}
54-
};
55-
56-
const handleExampleChange = (key: ExampleKey) => {
57-
setSelectedExample(key);
58-
const newCode = examples[key];
59-
setCode(newCode);
60-
updateCode(newCode);
61-
};
62-
63-
const handleDesignerChange = (newSchema: SchemaNode) => {
64-
const newCode = JSON.stringify(newSchema, null, 2);
65-
setCode(newCode);
66-
setSchema(newSchema);
67-
};
68-
69-
const handleCopySchema = async () => {
70-
try {
71-
await navigator.clipboard.writeText(code);
72-
setCopied(true);
73-
setTimeout(() => setCopied(false), 2000);
74-
} catch (err) {
75-
console.error('Failed to copy:', err);
76-
}
77-
};
78-
79-
const viewportStyles: Record<ViewportSize, string> = {
80-
desktop: 'w-full',
81-
tablet: 'max-w-3xl mx-auto',
82-
mobile: 'max-w-md mx-auto'
83-
};
6+
// Import core styles
7+
import './index.css';
848

9+
export default function App() {
8510
return (
86-
<div className="flex h-screen w-screen bg-background text-foreground">
87-
{/* 1. Sidebar: Example Selector */}
88-
<aside className="w-64 bg-muted/30 border-r flex flex-col">
89-
<div className="p-6 border-b">
90-
<h1 className="text-xl font-bold">Object UI</h1>
91-
<p className="text-sm text-muted-foreground mt-1">Live Playground</p>
92-
93-
<div className="mt-4 flex p-1 bg-muted rounded-md border">
94-
<button
95-
onClick={() => setViewMode('code')}
96-
className={`flex-1 flex items-center justify-center gap-2 px-2 py-1.5 text-xs font-medium rounded-sm transition-all ${viewMode === 'code' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'}`}
97-
title="Code Editor"
98-
>
99-
<Code2 className="w-3.5 h-3.5" />
100-
Code
101-
</button>
102-
<button
103-
onClick={() => setViewMode('design')}
104-
className={`flex-1 flex items-center justify-center gap-2 px-2 py-1.5 text-xs font-medium rounded-sm transition-all ${viewMode === 'design' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'}`}
105-
title="Visual Designer"
106-
>
107-
<PenTool className="w-3.5 h-3.5" />
108-
Design
109-
</button>
110-
<button
111-
onClick={() => setViewMode('preview')}
112-
className={`flex-1 flex items-center justify-center gap-2 px-2 py-1.5 text-xs font-medium rounded-sm transition-all ${viewMode === 'preview' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'}`}
113-
title="Live Preview"
114-
>
115-
<Monitor className="w-3.5 h-3.5" />
116-
Preview
117-
</button>
118-
</div>
119-
</div>
120-
121-
<div className="flex-1 overflow-auto p-4 space-y-6">
122-
{Object.entries(exampleCategories).map(([category, keys]) => (
123-
<div key={category}>
124-
<h2 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
125-
{category}
126-
</h2>
127-
<div className="space-y-1">
128-
{keys.map((key) => (
129-
<button
130-
key={key}
131-
onClick={() => handleExampleChange(key as ExampleKey)}
132-
className={`
133-
block w-full text-left px-3 py-2 rounded-md text-sm transition-colors
134-
${key === selectedExample
135-
? 'bg-primary text-primary-foreground font-medium'
136-
: 'hover:bg-muted text-foreground'
137-
}
138-
`}
139-
>
140-
{key.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}
141-
</button>
142-
))}
143-
</div>
144-
</div>
145-
))}
146-
</div>
147-
</aside>
148-
149-
{/* Main Content Area */}
150-
{viewMode === 'design' ? (
151-
<div className="flex-1 h-full overflow-hidden bg-background">
152-
{schema && (
153-
<Designer
154-
key={selectedExample} // Force re-mount on example change to reset designer state
155-
initialSchema={schema}
156-
onSchemaChange={handleDesignerChange}
157-
/>
158-
)}
159-
</div>
160-
) : viewMode === 'preview' ? (
161-
<div className="flex-1 h-full flex flex-col bg-muted/10 overflow-hidden">
162-
<div className="border-b px-4 py-3 bg-background flex items-center justify-between shadow-sm z-10">
163-
<h2 className="text-sm font-semibold flex items-center gap-2">
164-
<Monitor className="w-4 h-4 text-primary" />
165-
Live Preview
166-
</h2>
167-
168-
{/* Viewport Size Toggles */}
169-
<div className="flex items-center gap-1 bg-muted rounded-md p-1 border">
170-
<button
171-
onClick={() => setViewportSize('desktop')}
172-
className={`p-1.5 rounded transition-colors ${
173-
viewportSize === 'desktop' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:bg-background/50'
174-
}`}
175-
title="Desktop View"
176-
>
177-
<Monitor className="h-4 w-4" />
178-
</button>
179-
<button
180-
onClick={() => setViewportSize('tablet')}
181-
className={`p-1.5 rounded transition-colors ${
182-
viewportSize === 'tablet' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:bg-background/50'
183-
}`}
184-
title="Tablet View"
185-
>
186-
<Tablet className="h-4 w-4" />
187-
</button>
188-
<button
189-
onClick={() => setViewportSize('mobile')}
190-
className={`p-1.5 rounded transition-colors ${
191-
viewportSize === 'mobile' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:bg-background/50'
192-
}`}
193-
title="Mobile View"
194-
>
195-
<Smartphone className="h-4 w-4" />
196-
</button>
197-
</div>
198-
</div>
199-
200-
<div className="flex-1 overflow-auto p-8 flex justify-center">
201-
<div className={`${viewportStyles[viewportSize]} transition-all duration-300`}>
202-
<div className="rounded-lg border shadow-sm bg-background p-6 min-h-[500px]">
203-
{schema && !jsonError ? (
204-
<SchemaRenderer schema={schema} />
205-
) : (
206-
<div className="text-center py-12 text-muted-foreground">
207-
{jsonError ? (
208-
<div className="space-y-2">
209-
<p className="text-red-500 font-semibold">Invalid JSON</p>
210-
<p className="text-sm">Fix the syntax error to see the preview</p>
211-
</div>
212-
) : (
213-
<p>Select an example to get started</p>
214-
)}
215-
</div>
216-
)}
217-
</div>
218-
</div>
219-
</div>
220-
</div>
221-
) : (
222-
<div className="flex-1 flex h-full overflow-hidden">
223-
{/* 2. Middle: Code Editor */}
224-
<div className="w-1/2 h-full border-r flex flex-col">
225-
<div className="border-b px-4 py-3 bg-muted/20 flex items-center justify-between">
226-
<div className="flex items-center gap-2">
227-
<LayoutTemplate className="h-4 w-4 text-muted-foreground" />
228-
<h2 className="text-sm font-semibold">JSON Schema</h2>
229-
</div>
230-
<button
231-
onClick={handleCopySchema}
232-
className="flex items-center gap-2 px-3 py-1.5 text-xs rounded-md bg-background hover:bg-muted transition-colors border"
233-
>
234-
{copied ? (
235-
<>
236-
<Check className="h-3 w-3" />
237-
Copied!
238-
</>
239-
) : (
240-
<>
241-
<Copy className="h-3 w-3" />
242-
Copy Schema
243-
</>
244-
)}
245-
</button>
246-
</div>
247-
248-
{jsonError && (
249-
<div className="px-4 py-2 bg-red-50 border-b border-red-200 text-red-700 text-sm">
250-
<strong>JSON Error:</strong> {jsonError}
251-
</div>
252-
)}
253-
254-
<div className="flex-1 overflow-hidden">
255-
<textarea
256-
value={code}
257-
onChange={(e) => updateCode(e.target.value)}
258-
className="w-full h-full p-4 font-mono text-sm resize-none focus:outline-none border-0 bg-background text-foreground"
259-
spellCheck={false}
260-
style={{
261-
tabSize: 2,
262-
lineHeight: '1.6'
263-
}}
264-
/>
265-
</div>
266-
</div>
267-
268-
{/* 3. Right: Live Preview */}
269-
<div className="w-1/2 h-full flex flex-col bg-muted/10">
270-
<div className="border-b px-4 py-3 bg-muted/20 flex items-center justify-between">
271-
<h2 className="text-sm font-semibold">Preview</h2>
272-
273-
{/* Viewport Size Toggles */}
274-
<div className="flex items-center gap-1 bg-background rounded-md p-1 border">
275-
<button
276-
onClick={() => setViewportSize('desktop')}
277-
className={`p-1.5 rounded transition-colors ${
278-
viewportSize === 'desktop' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'
279-
}`}
280-
title="Desktop View"
281-
>
282-
<Monitor className="h-4 w-4" />
283-
</button>
284-
<button
285-
onClick={() => setViewportSize('tablet')}
286-
className={`p-1.5 rounded transition-colors ${
287-
viewportSize === 'tablet' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'
288-
}`}
289-
title="Tablet View"
290-
>
291-
<Tablet className="h-4 w-4" />
292-
</button>
293-
<button
294-
onClick={() => setViewportSize('mobile')}
295-
className={`p-1.5 rounded transition-colors ${
296-
viewportSize === 'mobile' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'
297-
}`}
298-
title="Mobile View"
299-
>
300-
<Smartphone className="h-4 w-4" />
301-
</button>
302-
</div>
303-
</div>
304-
305-
<div className="flex-1 overflow-auto p-8">
306-
<div className={`${viewportStyles[viewportSize]} transition-all duration-300`}>
307-
<div className="rounded-lg border shadow-sm bg-background p-6">
308-
{schema && !jsonError ? (
309-
<SchemaRenderer schema={schema} />
310-
) : (
311-
<div className="text-center py-12 text-muted-foreground">
312-
{jsonError ? (
313-
<div className="space-y-2">
314-
<p className="text-red-500 font-semibold">Invalid JSON</p>
315-
<p className="text-sm">Fix the syntax error to see the preview</p>
316-
</div>
317-
) : (
318-
<p>Select an example to get started</p>
319-
)}
320-
</div>
321-
)}
322-
</div>
323-
</div>
324-
</div>
325-
</div>
326-
</div>
327-
)}
328-
</div>
11+
<Router>
12+
<Routes>
13+
<Route path="/" element={<Home />} />
14+
<Route path="/studio/:id" element={<Studio />} />
15+
{/* Default redirect to first example if typed manually specifically */}
16+
<Route path="/studio" element={<Navigate to="/studio/dashboard" replace />} />
17+
</Routes>
18+
</Router>
32919
);
33020
}

0 commit comments

Comments
 (0)