@ryndesign/preview 0.1.1

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RynDesign
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>RynDesign Preview</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/index.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,261 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import { useWebSocket } from './hooks/useWebSocket';
3
+ import { useTokens } from './hooks/useTokens';
4
+ import { ComponentBrowser } from './components/ComponentBrowser';
5
+ import { PreviewPanel } from './components/PreviewPanel';
6
+ import { CodeViewer } from './components/CodeViewer';
7
+ import { TokenEditor } from './components/TokenEditor';
8
+ import { ThemeSwitcher } from './components/ThemeSwitcher';
9
+
10
+ export default function App() {
11
+ const [currentTheme, setCurrentTheme] = useState('light');
12
+ const [currentPlatform, setCurrentPlatform] = useState<'react' | 'swiftui'>('react');
13
+ const [selectedComponent, setSelectedComponent] = useState<string | null>(null);
14
+ const [splitView, setSplitView] = useState(false);
15
+
16
+ const { connected, send, tokenSet, components, snippets, requestSnippets } = useWebSocket();
17
+ const { updateToken } = useTokens(send);
18
+
19
+ const handleThemeChange = useCallback((theme: string) => {
20
+ setCurrentTheme(theme);
21
+ document.documentElement.setAttribute('data-app-theme', theme === 'light' ? '' : theme);
22
+ send({ type: 'theme-change', theme });
23
+ }, [send]);
24
+
25
+ const handleTokenUpdate = useCallback((path: string, value: unknown) => {
26
+ updateToken(path, value, currentTheme === 'light' ? undefined : currentTheme);
27
+ }, [updateToken, currentTheme]);
28
+
29
+ const handleComponentSelect = useCallback((name: string) => {
30
+ setSelectedComponent(name);
31
+ requestSnippets(currentPlatform, name);
32
+ }, [currentPlatform, requestSnippets]);
33
+
34
+ const handlePlatformChange = useCallback((platform: 'react' | 'swiftui') => {
35
+ setCurrentPlatform(platform);
36
+ if (selectedComponent) {
37
+ requestSnippets(platform, selectedComponent);
38
+ }
39
+ }, [selectedComponent, requestSnippets]);
40
+
41
+ return (
42
+ <div className="app">
43
+ <header className="header">
44
+ <h1>RynDesign Preview</h1>
45
+ <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
46
+ <ThemeSwitcher
47
+ currentTheme={currentTheme}
48
+ themes={tokenSet ? ['light', ...Object.keys(tokenSet.themes?.themes || {})] : ['light']}
49
+ onThemeChange={handleThemeChange}
50
+ onSplitView={() => setSplitView(!splitView)}
51
+ />
52
+ <div className="status">
53
+ <span className={`status-dot ${connected ? 'connected' : 'disconnected'}`} />
54
+ <span>{connected ? 'Connected' : 'Disconnected'}</span>
55
+ </div>
56
+ </div>
57
+ </header>
58
+
59
+ <aside className="sidebar">
60
+ <div className="sidebar-section">
61
+ <h2 className="sidebar-title">Components</h2>
62
+ <ComponentBrowser
63
+ components={components}
64
+ selected={selectedComponent}
65
+ onSelect={handleComponentSelect}
66
+ />
67
+ </div>
68
+ <div className="sidebar-section">
69
+ <h2 className="sidebar-title">Token Editor</h2>
70
+ <TokenEditor
71
+ tokens={tokenSet?.tokens ?? []}
72
+ onTokenUpdate={handleTokenUpdate}
73
+ />
74
+ </div>
75
+ </aside>
76
+
77
+ <main className="main">
78
+ <div className="platform-tabs">
79
+ {(['react', 'swiftui'] as const).map(p => (
80
+ <button
81
+ key={p}
82
+ className={`platform-tab ${p === currentPlatform ? 'active' : ''}`}
83
+ onClick={() => handlePlatformChange(p)}
84
+ >
85
+ {p === 'react' ? 'React' : 'SwiftUI'}
86
+ </button>
87
+ ))}
88
+ </div>
89
+
90
+ <PreviewPanel
91
+ tokenSet={tokenSet}
92
+ components={components}
93
+ selectedComponent={selectedComponent}
94
+ splitView={splitView}
95
+ currentTheme={currentTheme}
96
+ />
97
+
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>
107
+ </main>
108
+
109
+ <footer className="footer">
110
+ <span>RynDesign v0.1.0</span>
111
+ </footer>
112
+
113
+ <style>{globalStyles}</style>
114
+ </div>
115
+ );
116
+ }
117
+
118
+ const globalStyles = `
119
+ * { margin: 0; padding: 0; box-sizing: border-box; }
120
+ :root {
121
+ --bg-primary: #ffffff;
122
+ --bg-secondary: #f5f5f5;
123
+ --bg-sidebar: #fafafa;
124
+ --text-primary: #1a1a1a;
125
+ --text-secondary: #666;
126
+ --border-color: #e5e5e5;
127
+ --accent: #3B82F6;
128
+ }
129
+ [data-app-theme="dark"] {
130
+ --bg-primary: #1a1a2e;
131
+ --bg-secondary: #16213e;
132
+ --bg-sidebar: #0f3460;
133
+ --text-primary: #e5e5e5;
134
+ --text-secondary: #a0a0a0;
135
+ --border-color: #333;
136
+ --accent: #60a5fa;
137
+ }
138
+ body {
139
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
140
+ background: var(--bg-primary);
141
+ color: var(--text-primary);
142
+ }
143
+ .app {
144
+ display: grid;
145
+ grid-template: "header header" auto "sidebar main" 1fr "sidebar footer" auto / 300px 1fr;
146
+ height: 100vh;
147
+ }
148
+ .header {
149
+ grid-area: header; padding: 12px 20px; background: var(--bg-secondary);
150
+ border-bottom: 1px solid var(--border-color); display: flex;
151
+ align-items: center; justify-content: space-between;
152
+ }
153
+ .header h1 { font-size: 18px; font-weight: 600; }
154
+ .sidebar {
155
+ grid-area: sidebar; background: var(--bg-sidebar);
156
+ border-right: 1px solid var(--border-color); overflow-y: auto; padding: 16px;
157
+ }
158
+ .sidebar-section { margin-bottom: 24px; }
159
+ .sidebar-title { font-size: 14px; margin-bottom: 12px; font-weight: 600; }
160
+ .main { grid-area: main; overflow-y: auto; padding: 24px; }
161
+ .footer {
162
+ grid-area: footer; padding: 12px 20px; background: var(--bg-secondary);
163
+ border-top: 1px solid var(--border-color); font-size: 12px;
164
+ }
165
+ .status { display: flex; align-items: center; gap: 6px; font-size: 12px; }
166
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; }
167
+ .status-dot.connected { background: #10b981; }
168
+ .status-dot.disconnected { background: #ef4444; }
169
+ .platform-tabs { display: flex; gap: 4px; margin-bottom: 20px; }
170
+ .platform-tab {
171
+ padding: 8px 16px; border: 1px solid var(--border-color); border-radius: 6px;
172
+ background: var(--bg-secondary); cursor: pointer; font-size: 13px;
173
+ color: var(--text-primary);
174
+ }
175
+ .platform-tab.active { background: var(--accent); color: white; border-color: var(--accent); }
176
+ .code-viewer {
177
+ background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px;
178
+ font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: 13px;
179
+ line-height: 1.6; overflow-x: auto; white-space: pre; position: relative;
180
+ }
181
+ .code-viewer .copy-btn {
182
+ position: absolute; top: 8px; right: 8px; padding: 4px 10px;
183
+ background: #333; color: #ccc; border: 1px solid #555; border-radius: 4px;
184
+ cursor: pointer; font-size: 12px;
185
+ }
186
+ .code-viewer .copy-btn:hover { background: #444; }
187
+ .component-grid {
188
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
189
+ gap: 16px; margin-bottom: 24px;
190
+ }
191
+ .component-card {
192
+ border: 1px solid var(--border-color); border-radius: 8px;
193
+ padding: 20px; background: var(--bg-secondary);
194
+ }
195
+ .component-card h4 { margin-bottom: 12px; font-size: 14px; }
196
+ .split-view { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
197
+ .split-pane {
198
+ border: 1px solid var(--border-color); border-radius: 8px; padding: 16px;
199
+ }
200
+ .split-pane h4 { margin-bottom: 12px; font-size: 13px; color: var(--text-secondary); }
201
+ .theme-switcher { display: flex; gap: 8px; }
202
+ .theme-btn {
203
+ padding: 6px 12px; border: 1px solid var(--border-color); border-radius: 6px;
204
+ background: var(--bg-primary); color: var(--text-primary); cursor: pointer; font-size: 13px;
205
+ }
206
+ .theme-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
207
+ .token-group { margin-bottom: 16px; }
208
+ .token-group h3 {
209
+ font-size: 13px; font-weight: 600; margin-bottom: 8px;
210
+ color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px;
211
+ cursor: pointer; user-select: none;
212
+ }
213
+ .token-item {
214
+ display: flex; align-items: center; gap: 8px; padding: 6px 0;
215
+ border-bottom: 1px solid var(--border-color);
216
+ }
217
+ .token-name { flex: 1; font-size: 13px; font-family: monospace; }
218
+ .token-value { font-size: 12px; color: var(--text-secondary); }
219
+ .color-swatch {
220
+ width: 24px; height: 24px; border-radius: 4px;
221
+ border: 1px solid var(--border-color); cursor: pointer; position: relative;
222
+ overflow: hidden;
223
+ }
224
+ .color-swatch input {
225
+ opacity: 0; width: 24px; height: 24px; cursor: pointer;
226
+ position: absolute; top: 0; left: 0;
227
+ }
228
+ .token-input {
229
+ width: 70px; padding: 2px 6px; border: 1px solid var(--border-color);
230
+ border-radius: 4px; font-size: 12px; background: var(--bg-primary);
231
+ color: var(--text-primary);
232
+ }
233
+ .component-list { list-style: none; }
234
+ .component-list li {
235
+ padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 13px;
236
+ margin-bottom: 2px;
237
+ }
238
+ .component-list li:hover { background: var(--bg-secondary); }
239
+ .component-list li.active { background: var(--accent); color: white; }
240
+ .search-input {
241
+ width: 100%; padding: 8px 12px; border: 1px solid var(--border-color);
242
+ border-radius: 6px; font-size: 13px; margin-bottom: 12px;
243
+ background: var(--bg-primary); color: var(--text-primary);
244
+ }
245
+ .code-tabs { display: flex; gap: 2px; margin-bottom: 0; }
246
+ .code-tab {
247
+ padding: 6px 12px; border: 1px solid var(--border-color); border-bottom: none;
248
+ border-radius: 6px 6px 0 0; background: var(--bg-secondary); cursor: pointer;
249
+ font-size: 12px; color: var(--text-primary);
250
+ }
251
+ .code-tab.active { background: #1e1e1e; color: #d4d4d4; border-color: #333; }
252
+ .variant-grid {
253
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
254
+ gap: 12px; margin-bottom: 16px;
255
+ }
256
+ .variant-cell {
257
+ display: flex; flex-direction: column; align-items: center; gap: 8px;
258
+ padding: 12px; border: 1px solid var(--border-color); border-radius: 8px;
259
+ background: var(--bg-primary); font-size: 11px; color: var(--text-secondary);
260
+ }
261
+ `;
@@ -0,0 +1,88 @@
1
+ import React, { useState, useCallback } from 'react';
2
+
3
+ interface Props {
4
+ snippets: Record<string, string>;
5
+ platform: string;
6
+ component: string | null;
7
+ tokenSet: any;
8
+ }
9
+
10
+ type CodeTab = 'tokens' | 'component';
11
+
12
+ function formatTokenValue(value: any): string {
13
+ if (!value) return '';
14
+ switch (value.type) {
15
+ case 'color': return value.hex;
16
+ case 'dimension': return `${value.value}${value.unit}`;
17
+ case 'fontWeight': return String(value.value);
18
+ case 'duration': return `${value.value}${value.unit}`;
19
+ case 'number': return String(value.value);
20
+ default: return JSON.stringify(value);
21
+ }
22
+ }
23
+
24
+ function generateTokenPreview(tokenSet: any, platform: string): string {
25
+ if (!tokenSet?.tokens) return '// No tokens loaded';
26
+
27
+ if (platform === 'react') {
28
+ let css = '/* tokens.css */\n:root {\n';
29
+ for (const token of tokenSet.tokens.slice(0, 20)) {
30
+ css += ` --${token.path.join('-')}: ${formatTokenValue(token.$value)};\n`;
31
+ }
32
+ if (tokenSet.tokens.length > 20) css += ` /* ... ${tokenSet.tokens.length - 20} more tokens */\n`;
33
+ css += '}';
34
+ return css;
35
+ }
36
+
37
+ if (platform === 'swiftui') {
38
+ let swift = '// DesignTokens+Color.swift\nimport SwiftUI\n\nextension Color {\n enum DesignSystem {\n';
39
+ for (const token of tokenSet.tokens.filter((t: any) => t.$type === 'color').slice(0, 10)) {
40
+ swift += ` static let ${token.path.join('_').replace(/[.-]/g, '_')} = Color(hex: "${token.$value.hex}")\n`;
41
+ }
42
+ swift += ' }\n}';
43
+ return swift;
44
+ }
45
+
46
+ return `// ${platform} code generation`;
47
+ }
48
+
49
+ export function CodeViewer({ snippets, platform, component, tokenSet }: Props) {
50
+ const [activeTab, setActiveTab] = useState<CodeTab>('tokens');
51
+ const [copied, setCopied] = useState(false);
52
+
53
+ const code = activeTab === 'component' && component
54
+ ? snippets[`${platform}:${component}`] || `// Select a component to view generated code`
55
+ : snippets[`${platform}:tokens`] || generateTokenPreview(tokenSet, platform);
56
+
57
+ const handleCopy = useCallback(() => {
58
+ navigator.clipboard.writeText(code).then(() => {
59
+ setCopied(true);
60
+ setTimeout(() => setCopied(false), 2000);
61
+ }).catch(() => {});
62
+ }, [code]);
63
+
64
+ return (
65
+ <div>
66
+ <div className="code-tabs">
67
+ <button
68
+ className={`code-tab ${activeTab === 'tokens' ? 'active' : ''}`}
69
+ onClick={() => setActiveTab('tokens')}
70
+ >
71
+ Tokens
72
+ </button>
73
+ <button
74
+ className={`code-tab ${activeTab === 'component' ? 'active' : ''}`}
75
+ onClick={() => setActiveTab('component')}
76
+ >
77
+ Component
78
+ </button>
79
+ </div>
80
+ <div className="code-viewer">
81
+ <button className="copy-btn" onClick={handleCopy}>
82
+ {copied ? 'Copied!' : 'Copy'}
83
+ </button>
84
+ {code}
85
+ </div>
86
+ </div>
87
+ );
88
+ }
@@ -0,0 +1,54 @@
1
+ import React, { useState } from 'react';
2
+
3
+ interface Props {
4
+ components: any[];
5
+ selected: string | null;
6
+ onSelect: (name: string) => void;
7
+ }
8
+
9
+ export function ComponentBrowser({ components, selected, onSelect }: Props) {
10
+ const [search, setSearch] = useState('');
11
+
12
+ const filtered = components.filter(c =>
13
+ c.name?.toLowerCase().includes(search.toLowerCase()) ||
14
+ c.definition?.name?.toLowerCase().includes(search.toLowerCase())
15
+ );
16
+
17
+ const getName = (c: any) => c.definition?.name ?? c.name ?? 'Unknown';
18
+
19
+ return (
20
+ <div>
21
+ <input
22
+ className="search-input"
23
+ type="text"
24
+ placeholder="Search components..."
25
+ value={search}
26
+ onChange={e => setSearch(e.target.value)}
27
+ />
28
+ <ul className="component-list">
29
+ {filtered.length === 0 && (
30
+ <li style={{ color: 'var(--text-secondary)', fontSize: 13 }}>
31
+ {components.length === 0 ? 'No components loaded' : 'No matches'}
32
+ </li>
33
+ )}
34
+ {filtered.map(c => {
35
+ const name = getName(c);
36
+ return (
37
+ <li
38
+ key={name}
39
+ className={selected === name ? 'active' : ''}
40
+ onClick={() => onSelect(name)}
41
+ >
42
+ {name}
43
+ {c.definition?.category && (
44
+ <span style={{ fontSize: 11, opacity: 0.6, marginLeft: 8 }}>
45
+ {c.definition.category}
46
+ </span>
47
+ )}
48
+ </li>
49
+ );
50
+ })}
51
+ </ul>
52
+ </div>
53
+ );
54
+ }
@@ -0,0 +1,188 @@
1
+ import React from 'react';
2
+
3
+ interface Props {
4
+ tokenSet: any;
5
+ components: any[];
6
+ selectedComponent: string | null;
7
+ splitView: boolean;
8
+ currentTheme: string;
9
+ }
10
+
11
+ function formatTokenValue(value: any): string {
12
+ if (!value) return '';
13
+ switch (value.type) {
14
+ case 'color': return value.hex;
15
+ case 'dimension': return `${value.value}${value.unit}`;
16
+ case 'fontWeight': return String(value.value);
17
+ case 'duration': return `${value.value}${value.unit}`;
18
+ case 'number': return String(value.value);
19
+ default: return JSON.stringify(value);
20
+ }
21
+ }
22
+
23
+ function getToken(tokens: any[], path: string): string {
24
+ const t = tokens?.find((t: any) => t.path.join('.') === path);
25
+ return t ? formatTokenValue(t.$value) : '#ccc';
26
+ }
27
+
28
+ function renderComponentPreview(comp: any, tokens: any[]) {
29
+ const def = comp.definition ?? comp;
30
+ const name = def.name;
31
+ const variants = def.variants?.variant?.values ?? ['default'];
32
+ const sizes = def.variants?.size?.values ?? ['default'];
33
+ const variantTokens = comp.variantTokens;
34
+
35
+ return (
36
+ <div>
37
+ <h3 style={{ fontSize: 16, marginBottom: 16 }}>{name}</h3>
38
+ <div className="variant-grid">
39
+ {variants.map((variant: string) =>
40
+ sizes.map((size: string) => {
41
+ // Try to get tokens from variantTokens
42
+ const vt = variantTokens?.[variant]?.[size];
43
+ const bg = vt?.background?.$value?.hex
44
+ ?? getToken(tokens, `component.${name.toLowerCase()}.${variant}.background`);
45
+ const textColor = vt?.textColor?.$value?.hex
46
+ ?? (variant === 'primary' || variant === 'secondary' ? '#fff' : bg);
47
+ const fontSize = vt?.fontSize?.$value
48
+ ? `${vt.fontSize.$value.value}${vt.fontSize.$value.unit}`
49
+ : size === 'sm' ? '14px' : size === 'lg' ? '18px' : '16px';
50
+ const paddingX = vt?.paddingX?.$value
51
+ ? `${vt.paddingX.$value.value}${vt.paddingX.$value.unit}`
52
+ : size === 'sm' ? '12px' : size === 'lg' ? '24px' : '16px';
53
+ const paddingY = vt?.paddingY?.$value
54
+ ? `${vt.paddingY.$value.value}${vt.paddingY.$value.unit}`
55
+ : size === 'sm' ? '6px' : size === 'lg' ? '14px' : '10px';
56
+
57
+ const isOutline = variant === 'outline';
58
+ const isGhost = variant === 'ghost';
59
+
60
+ const style: React.CSSProperties = {
61
+ padding: `${paddingY} ${paddingX}`,
62
+ borderRadius: 8,
63
+ border: isOutline ? `1px solid ${bg}` : 'none',
64
+ background: (isOutline || isGhost) ? 'transparent' : bg,
65
+ color: (isOutline || isGhost) ? bg : textColor,
66
+ cursor: 'pointer',
67
+ fontSize,
68
+ display: 'inline-flex',
69
+ alignItems: 'center',
70
+ justifyContent: 'center',
71
+ };
72
+
73
+ return (
74
+ <div className="variant-cell" key={`${variant}-${size}`}>
75
+ <button style={style}>{name}</button>
76
+ <span>{variant}/{size}</span>
77
+ </div>
78
+ );
79
+ })
80
+ )}
81
+ </div>
82
+
83
+ {/* State previews */}
84
+ {def.states && Object.keys(def.states).length > 0 && (
85
+ <div style={{ marginTop: 16 }}>
86
+ <h4 style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 8 }}>States</h4>
87
+ <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
88
+ {Object.keys(def.states).map((state: string) => (
89
+ <div key={state} style={{ textAlign: 'center' }}>
90
+ <button
91
+ style={{
92
+ padding: '10px 16px',
93
+ borderRadius: 8,
94
+ border: 'none',
95
+ background: getToken(tokens, 'color.primary'),
96
+ color: '#fff',
97
+ cursor: state === 'disabled' ? 'not-allowed' : 'pointer',
98
+ opacity: state === 'disabled' ? 0.6 : 1,
99
+ fontSize: 14,
100
+ }}
101
+ >
102
+ {name}
103
+ </button>
104
+ <div style={{ fontSize: 11, color: 'var(--text-secondary)', marginTop: 4 }}>{state}</div>
105
+ </div>
106
+ ))}
107
+ </div>
108
+ </div>
109
+ )}
110
+ </div>
111
+ );
112
+ }
113
+
114
+ export function PreviewPanel({ tokenSet, components, selectedComponent, splitView, currentTheme }: Props) {
115
+ const tokens = tokenSet?.tokens ?? [];
116
+
117
+ if (!tokenSet) {
118
+ return <p>Loading...</p>;
119
+ }
120
+
121
+ const selectedComp = selectedComponent
122
+ ? components.find(c => (c.definition?.name ?? c.name) === selectedComponent)
123
+ : null;
124
+
125
+ if (splitView && selectedComp) {
126
+ return (
127
+ <div className="split-view">
128
+ <div className="split-pane">
129
+ <h4>Light</h4>
130
+ {renderComponentPreview(selectedComp, tokens)}
131
+ </div>
132
+ <div className="split-pane" style={{ background: '#1a1a2e', color: '#e5e5e5' }}>
133
+ <h4>Dark</h4>
134
+ {renderComponentPreview(selectedComp, tokenSet.themes?.themes?.dark?.tokens ?? tokens)}
135
+ </div>
136
+ </div>
137
+ );
138
+ }
139
+
140
+ if (selectedComp) {
141
+ return renderComponentPreview(selectedComp, tokens);
142
+ }
143
+
144
+ // Default: show all components in a grid
145
+ if (components.length === 0) {
146
+ return (
147
+ <div className="component-grid">
148
+ {renderFallbackPreview(tokens)}
149
+ </div>
150
+ );
151
+ }
152
+
153
+ return (
154
+ <div>
155
+ {components.map(comp => {
156
+ const name = comp.definition?.name ?? comp.name;
157
+ return (
158
+ <div key={name} className="component-card" style={{ marginBottom: 16 }}>
159
+ {renderComponentPreview(comp, tokens)}
160
+ </div>
161
+ );
162
+ })}
163
+ </div>
164
+ );
165
+ }
166
+
167
+ function renderFallbackPreview(tokens: any[]) {
168
+ return (
169
+ <div className="component-card">
170
+ <h4>Button</h4>
171
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
172
+ <button style={{
173
+ padding: '8px 16px', borderRadius: 8, border: 'none',
174
+ background: getToken(tokens, 'color.primary'), color: 'white', cursor: 'pointer',
175
+ }}>Primary</button>
176
+ <button style={{
177
+ padding: '8px 16px', borderRadius: 8,
178
+ border: `1px solid ${getToken(tokens, 'color.primary')}`,
179
+ background: 'transparent', color: getToken(tokens, 'color.primary'), cursor: 'pointer',
180
+ }}>Outline</button>
181
+ <button style={{
182
+ padding: '8px 16px', borderRadius: 8, border: 'none',
183
+ background: 'transparent', color: getToken(tokens, 'color.primary'), cursor: 'pointer',
184
+ }}>Ghost</button>
185
+ </div>
186
+ </div>
187
+ );
188
+ }
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+
3
+ interface Props {
4
+ currentTheme: string;
5
+ themes: string[];
6
+ onThemeChange: (theme: string) => void;
7
+ onSplitView: () => void;
8
+ }
9
+
10
+ export function ThemeSwitcher({ currentTheme, themes, onThemeChange, onSplitView }: Props) {
11
+ return (
12
+ <div className="theme-switcher">
13
+ {themes.map(t => (
14
+ <button
15
+ key={t}
16
+ className={`theme-btn ${t === currentTheme ? 'active' : ''}`}
17
+ onClick={() => onThemeChange(t)}
18
+ >
19
+ {t}
20
+ </button>
21
+ ))}
22
+ <button className="theme-btn" onClick={onSplitView}>
23
+ Split
24
+ </button>
25
+ </div>
26
+ );
27
+ }