@ryndesign/preview 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,12 +6,14 @@ import { PreviewPanel } from './components/PreviewPanel';
6
6
  import { CodeViewer } from './components/CodeViewer';
7
7
  import { TokenEditor } from './components/TokenEditor';
8
8
  import { ThemeSwitcher } from './components/ThemeSwitcher';
9
+ import { ExamplePages } from './components/ExamplePages';
9
10
 
10
11
  export default function App() {
11
12
  const [currentTheme, setCurrentTheme] = useState('light');
12
13
  const [currentPlatform, setCurrentPlatform] = useState<'react' | 'swiftui'>('react');
13
14
  const [selectedComponent, setSelectedComponent] = useState<string | null>(null);
14
15
  const [splitView, setSplitView] = useState(false);
16
+ const [viewMode, setViewMode] = useState<'components' | 'examples'>('components');
15
17
 
16
18
  const { connected, send, tokenSet, components, snippets, requestSnippets } = useWebSocket();
17
19
  const { updateToken } = useTokens(send);
@@ -75,35 +77,59 @@ export default function App() {
75
77
  </aside>
76
78
 
77
79
  <main className="main">
78
- <div className="platform-tabs">
79
- {(['react', 'swiftui'] as const).map(p => (
80
+ <div style={{ display: 'flex', gap: 4, marginBottom: 20 }}>
81
+ <div className="platform-tabs" style={{ marginBottom: 0 }}>
80
82
  <button
81
- key={p}
82
- className={`platform-tab ${p === currentPlatform ? 'active' : ''}`}
83
- onClick={() => handlePlatformChange(p)}
83
+ className={`platform-tab ${viewMode === 'components' ? 'active' : ''}`}
84
+ onClick={() => setViewMode('components')}
84
85
  >
85
- {p === 'react' ? 'React' : 'SwiftUI'}
86
+ Components
86
87
  </button>
87
- ))}
88
+ <button
89
+ className={`platform-tab ${viewMode === 'examples' ? 'active' : ''}`}
90
+ onClick={() => setViewMode('examples')}
91
+ >
92
+ Examples
93
+ </button>
94
+ </div>
95
+ {viewMode === 'components' && (
96
+ <div className="platform-tabs" style={{ marginBottom: 0, marginLeft: 16 }}>
97
+ {(['react', 'swiftui'] as const).map(p => (
98
+ <button
99
+ key={p}
100
+ className={`platform-tab ${p === currentPlatform ? 'active' : ''}`}
101
+ onClick={() => handlePlatformChange(p)}
102
+ >
103
+ {p === 'react' ? 'React' : 'SwiftUI'}
104
+ </button>
105
+ ))}
106
+ </div>
107
+ )}
88
108
  </div>
89
109
 
90
- <PreviewPanel
91
- tokenSet={tokenSet}
92
- components={components}
93
- selectedComponent={selectedComponent}
94
- splitView={splitView}
95
- currentTheme={currentTheme}
96
- />
110
+ {viewMode === 'components' ? (
111
+ <>
112
+ <PreviewPanel
113
+ tokenSet={tokenSet}
114
+ components={components}
115
+ selectedComponent={selectedComponent}
116
+ splitView={splitView}
117
+ currentTheme={currentTheme}
118
+ />
97
119
 
98
- <div style={{ marginTop: 24 }}>
99
- <h3 style={{ fontSize: 14, marginBottom: 12 }}>Generated Code</h3>
100
- <CodeViewer
101
- snippets={snippets}
102
- platform={currentPlatform}
103
- component={selectedComponent}
104
- tokenSet={tokenSet}
105
- />
106
- </div>
120
+ <div style={{ marginTop: 24 }}>
121
+ <h3 style={{ fontSize: 14, marginBottom: 12 }}>Generated Code</h3>
122
+ <CodeViewer
123
+ snippets={snippets}
124
+ platform={currentPlatform}
125
+ component={selectedComponent}
126
+ tokenSet={tokenSet}
127
+ />
128
+ </div>
129
+ </>
130
+ ) : (
131
+ <ExamplePages tokenSet={tokenSet} components={components} />
132
+ )}
107
133
  </main>
108
134
 
109
135
  <footer className="footer">
@@ -0,0 +1,431 @@
1
+ import React, { useState } from 'react';
2
+ import { getThemeColors } from '../utils/tokenHelpers';
3
+
4
+ interface ExamplePagesProps {
5
+ tokenSet: any;
6
+ components: any[];
7
+ }
8
+
9
+ type PageId = 'login' | 'profile' | 'list' | 'dashboard';
10
+
11
+ export function ExamplePages({ tokenSet, components }: ExamplePagesProps) {
12
+ const [activePage, setActivePage] = useState<PageId>('login');
13
+ const tokens = tokenSet?.tokens ?? [];
14
+ const c = getThemeColors(tokens);
15
+
16
+ const tabStyle = (active: boolean): React.CSSProperties => ({
17
+ padding: '8px 20px',
18
+ fontSize: 14,
19
+ fontWeight: active ? 600 : 400,
20
+ color: active ? c.primary : c.baseContent,
21
+ background: active ? c.base100 : 'transparent',
22
+ border: active ? `1px solid ${c.borderColor}` : '1px solid transparent',
23
+ borderBottom: active ? `2px solid ${c.primary}` : '2px solid transparent',
24
+ borderRadius: '8px 8px 0 0',
25
+ cursor: 'pointer',
26
+ transition: 'all 0.15s ease',
27
+ });
28
+
29
+ const cardStyle: React.CSSProperties = {
30
+ background: c.base100,
31
+ borderRadius: 12,
32
+ boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
33
+ padding: 24,
34
+ };
35
+
36
+ const inputStyle: React.CSSProperties = {
37
+ border: `1px solid ${c.borderColor}`,
38
+ borderRadius: 8,
39
+ padding: '10px 14px',
40
+ background: c.base100,
41
+ color: c.baseContent,
42
+ fontSize: 14,
43
+ width: '100%',
44
+ boxSizing: 'border-box',
45
+ outline: 'none',
46
+ };
47
+
48
+ const btnPrimary: React.CSSProperties = {
49
+ background: c.primary,
50
+ color: c.primaryContent,
51
+ border: 'none',
52
+ borderRadius: 8,
53
+ padding: '10px 20px',
54
+ fontSize: 14,
55
+ fontWeight: 600,
56
+ cursor: 'pointer',
57
+ width: '100%',
58
+ textAlign: 'center',
59
+ };
60
+
61
+ const btnOutline: React.CSSProperties = {
62
+ background: 'transparent',
63
+ border: `1px solid ${c.borderColor}`,
64
+ color: c.baseContent,
65
+ borderRadius: 8,
66
+ padding: '10px 20px',
67
+ fontSize: 14,
68
+ fontWeight: 600,
69
+ cursor: 'pointer',
70
+ width: '100%',
71
+ textAlign: 'center',
72
+ };
73
+
74
+ const badgeStyle = (bg: string, color: string): React.CSSProperties => ({
75
+ borderRadius: 9999,
76
+ padding: '2px 10px',
77
+ fontSize: 12,
78
+ fontWeight: 600,
79
+ background: bg,
80
+ color,
81
+ display: 'inline-block',
82
+ });
83
+
84
+ const avatarStyle = (size: number): React.CSSProperties => ({
85
+ width: size,
86
+ height: size,
87
+ borderRadius: '50%',
88
+ background: c.primary,
89
+ color: c.primaryContent,
90
+ fontWeight: 600,
91
+ display: 'flex',
92
+ alignItems: 'center',
93
+ justifyContent: 'center',
94
+ fontSize: size * 0.4,
95
+ flexShrink: 0,
96
+ });
97
+
98
+ const labelStyle: React.CSSProperties = {
99
+ fontSize: 14,
100
+ fontWeight: 500,
101
+ color: c.baseContent,
102
+ marginBottom: 6,
103
+ display: 'block',
104
+ };
105
+
106
+ // ─── Login Page ──────────────────────────────────────────
107
+ const renderLogin = () => (
108
+ <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 500 }}>
109
+ <div style={{ ...cardStyle, maxWidth: 400, width: '100%', padding: 32 }}>
110
+ <div style={{ textAlign: 'center', marginBottom: 24 }}>
111
+ <div style={{ ...avatarStyle(48), margin: '0 auto 16px', background: c.primary }}>R</div>
112
+ <h2 style={{ fontSize: 24, fontWeight: 700, color: c.baseContent, margin: 0 }}>Welcome back</h2>
113
+ <p style={{ fontSize: 14, color: c.neutral, marginTop: 6 }}>Sign in to your account to continue</p>
114
+ </div>
115
+
116
+ <div style={{ marginBottom: 16 }}>
117
+ <label style={labelStyle}>Email</label>
118
+ <input style={inputStyle} type="text" value="user@example.com" readOnly />
119
+ </div>
120
+
121
+ <div style={{ marginBottom: 16 }}>
122
+ <label style={labelStyle}>Password</label>
123
+ <input style={inputStyle} type="password" value="********" readOnly />
124
+ </div>
125
+
126
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20, fontSize: 14 }}>
127
+ <label style={{ display: 'flex', alignItems: 'center', gap: 6, color: c.baseContent, cursor: 'pointer' }}>
128
+ <span style={{
129
+ width: 16, height: 16, borderRadius: 4,
130
+ border: `2px solid ${c.primary}`, background: c.primary,
131
+ display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
132
+ color: c.primaryContent, fontSize: 10, fontWeight: 700,
133
+ }}>&#10003;</span>
134
+ Remember me
135
+ </label>
136
+ <span style={{ color: c.primary, cursor: 'pointer' }}>Forgot password?</span>
137
+ </div>
138
+
139
+ <button style={btnPrimary}>Sign In</button>
140
+
141
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12, margin: '20px 0' }}>
142
+ <div style={{ flex: 1, height: 1, background: c.borderColor }} />
143
+ <span style={{ fontSize: 12, color: c.neutral }}>or</span>
144
+ <div style={{ flex: 1, height: 1, background: c.borderColor }} />
145
+ </div>
146
+
147
+ <button style={btnOutline}>Create Account</button>
148
+ </div>
149
+ </div>
150
+ );
151
+
152
+ // ─── Profile Page ────────────────────────────────────────
153
+ const renderProfile = () => (
154
+ <div style={{ maxWidth: 600, margin: '0 auto', display: 'flex', flexDirection: 'column', gap: 24 }}>
155
+ {/* Header */}
156
+ <div style={{ ...cardStyle, display: 'flex', alignItems: 'center', gap: 20 }}>
157
+ <div style={avatarStyle(80)}>JD</div>
158
+ <div style={{ flex: 1 }}>
159
+ <h2 style={{ fontSize: 24, fontWeight: 700, color: c.baseContent, margin: 0 }}>John Doe</h2>
160
+ <p style={{ fontSize: 14, color: c.neutral, margin: '4px 0 10px' }}>Senior Designer</p>
161
+ <div style={{ display: 'flex', gap: 8 }}>
162
+ <span style={badgeStyle(c.primary + '22', c.primary)}>Admin</span>
163
+ <span style={badgeStyle(c.secondary + '22', c.secondary)}>Pro</span>
164
+ <span style={badgeStyle(c.success + '22', c.success)}>Active</span>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ {/* Stats */}
170
+ <div style={{ display: 'flex', gap: 16 }}>
171
+ {[
172
+ { label: 'Posts', value: '128' },
173
+ { label: 'Followers', value: '1.2k' },
174
+ { label: 'Following', value: '348' },
175
+ ].map((s) => (
176
+ <div key={s.label} style={{ ...cardStyle, flex: 1, textAlign: 'center' }}>
177
+ <div style={{ fontSize: 24, fontWeight: 700, color: c.baseContent }}>{s.value}</div>
178
+ <div style={{ fontSize: 12, color: c.neutral, marginTop: 4 }}>{s.label}</div>
179
+ </div>
180
+ ))}
181
+ </div>
182
+
183
+ {/* Edit Section */}
184
+ <div style={cardStyle}>
185
+ <h3 style={{ fontSize: 16, fontWeight: 600, color: c.baseContent, margin: '0 0 16px' }}>Edit Profile</h3>
186
+ <div style={{ marginBottom: 16 }}>
187
+ <label style={labelStyle}>Name</label>
188
+ <input style={inputStyle} type="text" value="John Doe" readOnly />
189
+ </div>
190
+ <div style={{ marginBottom: 16 }}>
191
+ <label style={labelStyle}>Bio</label>
192
+ <textarea
193
+ style={{ ...inputStyle, minHeight: 80, resize: 'none', fontFamily: 'inherit' }}
194
+ readOnly
195
+ placeholder="Tell us about yourself..."
196
+ />
197
+ </div>
198
+ <button style={{ ...btnPrimary, width: 'auto' }}>Save Changes</button>
199
+ </div>
200
+ </div>
201
+ );
202
+
203
+ // ─── List Page ───────────────────────────────────────────
204
+ const listItems = [
205
+ { name: 'Alice Johnson', desc: 'Product Designer', status: 'Active', statusColor: c.success },
206
+ { name: 'Bob Smith', desc: 'Frontend Engineer', status: 'Active', statusColor: c.success },
207
+ { name: 'Carol White', desc: 'Project Manager', status: 'Pending', statusColor: c.warning },
208
+ { name: 'David Kim', desc: 'Backend Engineer', status: 'Inactive', statusColor: c.error },
209
+ { name: 'Emma Lee', desc: 'UX Researcher', status: 'Active', statusColor: c.success },
210
+ ];
211
+
212
+ const toggleSwitch = (on: boolean): React.CSSProperties => ({
213
+ width: 40, height: 22, borderRadius: 11,
214
+ background: on ? c.primary : c.borderColor,
215
+ position: 'relative',
216
+ cursor: 'pointer',
217
+ flexShrink: 0,
218
+ });
219
+
220
+ const toggleKnob = (on: boolean): React.CSSProperties => ({
221
+ width: 16, height: 16, borderRadius: '50%',
222
+ background: c.base100,
223
+ position: 'absolute',
224
+ top: 3,
225
+ left: on ? 21 : 3,
226
+ transition: 'left 0.15s ease',
227
+ boxShadow: '0 1px 2px rgba(0,0,0,0.15)',
228
+ });
229
+
230
+ const renderList = () => (
231
+ <div style={{ maxWidth: 700, margin: '0 auto', display: 'flex', flexDirection: 'column', gap: 24 }}>
232
+ {/* Navbar */}
233
+ <div style={{
234
+ ...cardStyle, padding: '12px 20px', borderRadius: 12,
235
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
236
+ }}>
237
+ <span style={{ fontSize: 16, fontWeight: 700, color: c.primary }}>AppName</span>
238
+ <input style={{ ...inputStyle, width: 240 }} type="text" value="Search..." readOnly />
239
+ <div style={avatarStyle(32)}>JD</div>
240
+ </div>
241
+
242
+ {/* List Items */}
243
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
244
+ {listItems.map((item, i) => (
245
+ <div key={i} style={{
246
+ ...cardStyle, padding: '14px 20px',
247
+ display: 'flex', alignItems: 'center', gap: 14,
248
+ }}>
249
+ <div style={avatarStyle(40)}>{item.name.split(' ').map(n => n[0]).join('')}</div>
250
+ <div style={{ flex: 1 }}>
251
+ <div style={{ fontSize: 14, fontWeight: 600, color: c.baseContent }}>{item.name}</div>
252
+ <div style={{ fontSize: 12, color: c.neutral, marginTop: 2 }}>{item.desc}</div>
253
+ </div>
254
+ <span style={badgeStyle(item.statusColor + '22', item.statusColor)}>{item.status}</span>
255
+ <div style={toggleSwitch(item.status === 'Active')}>
256
+ <div style={toggleKnob(item.status === 'Active')} />
257
+ </div>
258
+ </div>
259
+ ))}
260
+ </div>
261
+
262
+ {/* Pagination */}
263
+ <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: 4 }}>
264
+ {['<', '1', '2', '3', '4', '5', '>'].map((p) => (
265
+ <span key={p} style={{
266
+ width: 36, height: 36, display: 'flex', alignItems: 'center', justifyContent: 'center',
267
+ borderRadius: 8, fontSize: 14, fontWeight: p === '2' ? 700 : 400, cursor: 'pointer',
268
+ background: p === '2' ? c.primary : 'transparent',
269
+ color: p === '2' ? c.primaryContent : c.baseContent,
270
+ border: p === '2' ? 'none' : `1px solid ${c.borderColor}`,
271
+ }}>{p}</span>
272
+ ))}
273
+ </div>
274
+ </div>
275
+ );
276
+
277
+ // ─── Dashboard Page ──────────────────────────────────────
278
+ const stats = [
279
+ { label: 'Revenue', value: '$12,345', change: '+12%', color: c.success },
280
+ { label: 'Users', value: '1,234', change: '+5%', color: c.success },
281
+ { label: 'Orders', value: '567', change: '-2%', color: c.error },
282
+ { label: 'Conversion', value: '3.2%', change: '+0.5%', color: c.success },
283
+ ];
284
+
285
+ const orders = [
286
+ { id: '#ORD-001', customer: 'Alice Johnson', amount: '$234.50', status: 'Completed', statusColor: c.success, date: '2026-03-20' },
287
+ { id: '#ORD-002', customer: 'Bob Smith', amount: '$129.00', status: 'Processing', statusColor: c.warning, date: '2026-03-21' },
288
+ { id: '#ORD-003', customer: 'Carol White', amount: '$567.80', status: 'Completed', statusColor: c.success, date: '2026-03-21' },
289
+ { id: '#ORD-004', customer: 'David Kim', amount: '$89.99', status: 'Cancelled', statusColor: c.error, date: '2026-03-22' },
290
+ { id: '#ORD-005', customer: 'Emma Lee', amount: '$345.00', status: 'Processing', statusColor: c.warning, date: '2026-03-23' },
291
+ ];
292
+
293
+ const renderDashboard = () => (
294
+ <div style={{ maxWidth: 800, margin: '0 auto', display: 'flex', flexDirection: 'column', gap: 24 }}>
295
+ {/* Navbar */}
296
+ <div style={{
297
+ ...cardStyle, padding: '12px 20px', borderRadius: 12,
298
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
299
+ }}>
300
+ <span style={{ fontSize: 16, fontWeight: 700, color: c.baseContent }}>Dashboard</span>
301
+ <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
302
+ <div style={{ position: 'relative' }}>
303
+ <span style={{ fontSize: 18, cursor: 'pointer' }}>&#128276;</span>
304
+ <span style={{
305
+ position: 'absolute', top: -4, right: -6,
306
+ width: 16, height: 16, borderRadius: '50%',
307
+ background: c.error, color: c.primaryContent,
308
+ fontSize: 10, fontWeight: 700,
309
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
310
+ }}>3</span>
311
+ </div>
312
+ <div style={avatarStyle(32)}>JD</div>
313
+ </div>
314
+ </div>
315
+
316
+ {/* Stat Cards 2x2 */}
317
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
318
+ {stats.map((s) => (
319
+ <div key={s.label} style={cardStyle}>
320
+ <div style={{ fontSize: 12, color: c.neutral, marginBottom: 4 }}>{s.label}</div>
321
+ <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
322
+ <span style={{ fontSize: 24, fontWeight: 700, color: c.baseContent }}>{s.value}</span>
323
+ <span style={{ fontSize: 12, fontWeight: 600, color: s.color }}>{s.change}</span>
324
+ </div>
325
+ </div>
326
+ ))}
327
+ </div>
328
+
329
+ {/* Alert Banner */}
330
+ <div style={{
331
+ background: c.info + '18',
332
+ border: `1px solid ${c.info}44`,
333
+ borderRadius: 8,
334
+ padding: '12px 16px',
335
+ display: 'flex',
336
+ alignItems: 'center',
337
+ gap: 10,
338
+ fontSize: 14,
339
+ color: c.baseContent,
340
+ }}>
341
+ <span style={{
342
+ width: 22, height: 22, borderRadius: '50%',
343
+ background: c.info, color: c.primaryContent,
344
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
345
+ fontSize: 12, fontWeight: 700, flexShrink: 0,
346
+ }}>i</span>
347
+ System maintenance scheduled for tonight at 11:00 PM UTC.
348
+ </div>
349
+
350
+ {/* Orders Table */}
351
+ <div style={{ ...cardStyle, padding: 0, overflow: 'hidden' }}>
352
+ <div style={{ padding: '16px 20px', borderBottom: `1px solid ${c.borderColor}` }}>
353
+ <h3 style={{ fontSize: 16, fontWeight: 600, color: c.baseContent, margin: 0 }}>Recent Orders</h3>
354
+ </div>
355
+ <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
356
+ <thead>
357
+ <tr style={{ background: c.base200 }}>
358
+ {['Order ID', 'Customer', 'Amount', 'Status', 'Date'].map((h) => (
359
+ <th key={h} style={{
360
+ padding: '10px 16px', textAlign: 'left',
361
+ fontWeight: 600, color: c.neutral, fontSize: 12,
362
+ borderBottom: `1px solid ${c.borderColor}`,
363
+ }}>{h}</th>
364
+ ))}
365
+ </tr>
366
+ </thead>
367
+ <tbody>
368
+ {orders.map((o) => (
369
+ <tr key={o.id} style={{ borderBottom: `1px solid ${c.borderColor}` }}>
370
+ <td style={{ padding: '10px 16px', fontWeight: 600, color: c.baseContent }}>{o.id}</td>
371
+ <td style={{ padding: '10px 16px', color: c.baseContent }}>{o.customer}</td>
372
+ <td style={{ padding: '10px 16px', color: c.baseContent }}>{o.amount}</td>
373
+ <td style={{ padding: '10px 16px' }}>
374
+ <span style={badgeStyle(o.statusColor + '22', o.statusColor)}>{o.status}</span>
375
+ </td>
376
+ <td style={{ padding: '10px 16px', color: c.neutral }}>{o.date}</td>
377
+ </tr>
378
+ ))}
379
+ </tbody>
380
+ </table>
381
+ </div>
382
+
383
+ {/* Progress Bar */}
384
+ <div style={cardStyle}>
385
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
386
+ <span style={{ fontSize: 14, fontWeight: 600, color: c.baseContent }}>Monthly Target</span>
387
+ <span style={{ fontSize: 14, fontWeight: 600, color: c.primary }}>67%</span>
388
+ </div>
389
+ <div style={{
390
+ height: 10, borderRadius: 5, background: c.base200, overflow: 'hidden',
391
+ }}>
392
+ <div style={{
393
+ width: '67%', height: '100%', borderRadius: 5,
394
+ background: `linear-gradient(90deg, ${c.primary}, ${c.secondary})`,
395
+ }} />
396
+ </div>
397
+ <div style={{ fontSize: 12, color: c.neutral, marginTop: 6 }}>67% complete</div>
398
+ </div>
399
+ </div>
400
+ );
401
+
402
+ const pages: Record<PageId, { label: string; render: () => React.ReactNode }> = {
403
+ login: { label: 'Login', render: renderLogin },
404
+ profile: { label: 'Profile', render: renderProfile },
405
+ list: { label: 'List', render: renderList },
406
+ dashboard: { label: 'Dashboard', render: renderDashboard },
407
+ };
408
+
409
+ return (
410
+ <div style={{ fontSize: 14, color: c.baseContent }}>
411
+ {/* Tab Bar */}
412
+ <div style={{
413
+ display: 'flex', gap: 4, borderBottom: `1px solid ${c.borderColor}`,
414
+ marginBottom: 24, paddingBottom: 0,
415
+ }}>
416
+ {(Object.keys(pages) as PageId[]).map((id) => (
417
+ <button
418
+ key={id}
419
+ style={tabStyle(activePage === id)}
420
+ onClick={() => setActivePage(id)}
421
+ >
422
+ {pages[id].label}
423
+ </button>
424
+ ))}
425
+ </div>
426
+
427
+ {/* Page Content */}
428
+ {pages[activePage].render()}
429
+ </div>
430
+ );
431
+ }