@ryndesign/preview 0.1.5 → 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.
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { getVT, shadowToCSS, getThemeColors, getTokenColor } from '../utils/tokenHelpers';
2
3
 
3
4
  interface Props {
4
5
  tokenSet: any;
@@ -8,24 +9,925 @@ interface Props {
8
9
  currentTheme: string;
9
10
  }
10
11
 
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
- }
12
+ type Colors = ReturnType<typeof getThemeColors>;
13
+
14
+ /* ─── shared style constants ─── */
15
+ const R = '0.5rem';
16
+ const PILL = '9999px';
17
+ const SHADOW_SM = '0 1px 3px rgba(0,0,0,0.1)';
18
+ const SHADOW_MD = '0 4px 6px rgba(0,0,0,0.1)';
19
+ const SHADOW_LG = '0 10px 15px rgba(0,0,0,0.1)';
20
+ const TRANSITION = 'all 0.2s ease';
21
+
22
+ function label(text: string): React.ReactNode {
23
+ return <div style={{ fontSize: 11, color: 'var(--text-secondary)', marginTop: 4, textAlign: 'center' }}>{text}</div>;
21
24
  }
22
25
 
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
+ function sectionTitle(text: string): React.ReactNode {
27
+ return <h3 style={{ fontSize: 16, fontWeight: 600, marginBottom: 16 }}>{text}</h3>;
26
28
  }
27
29
 
28
- function renderComponentPreview(comp: any, tokens: any[]) {
30
+ function row(gap = 12, wrap = true): React.CSSProperties {
31
+ return { display: 'flex', gap, flexWrap: wrap ? 'wrap' : 'nowrap', alignItems: 'center' };
32
+ }
33
+
34
+ function col(gap = 8): React.CSSProperties {
35
+ return { display: 'flex', flexDirection: 'column', gap };
36
+ }
37
+
38
+ /* ─── helpers ─── */
39
+ function getVTColor(comp: any, variant: string, size: string, prop: string, fallback: string): string {
40
+ return comp.variantTokens?.[variant]?.[size]?.[prop]?.$value?.hex ?? fallback;
41
+ }
42
+
43
+ function getVTDim(comp: any, variant: string, size: string, prop: string, fallback: string): string {
44
+ const v = comp.variantTokens?.[variant]?.[size]?.[prop]?.$value;
45
+ if (!v) return fallback;
46
+ if (v.value !== undefined && v.unit) return `${v.value}${v.unit}`;
47
+ return fallback;
48
+ }
49
+
50
+ /* ════════════════════════════════════════════════════════════════
51
+ COMPONENT RENDERERS (28)
52
+ ════════════════════════════════════════════════════════════════ */
53
+
54
+ /* ── Actions ── */
55
+
56
+ function renderButton(comp: any, tokens: any[], c: Colors) {
57
+ const def = comp.definition ?? comp;
58
+ const variants: Record<string, React.CSSProperties> = {
59
+ primary: { background: c.primary, color: c.primaryContent, border: 'none' },
60
+ secondary: { background: c.secondary, color: c.primaryContent, border: 'none' },
61
+ outline: { background: 'transparent', color: c.primary, border: `2px solid ${c.primary}` },
62
+ ghost: { background: 'transparent', color: c.primary, border: '2px solid transparent' },
63
+ };
64
+ const sizes: Record<string, React.CSSProperties> = {
65
+ sm: { padding: '6px 14px', fontSize: 13 },
66
+ md: { padding: '10px 20px', fontSize: 15 },
67
+ lg: { padding: '14px 28px', fontSize: 17 },
68
+ };
69
+ const states = ['hover', 'pressed', 'disabled'] as const;
70
+
71
+ return (
72
+ <div>
73
+ {sectionTitle('Button')}
74
+ <div className="variant-grid">
75
+ {Object.entries(variants).map(([v, vs]) =>
76
+ Object.entries(sizes).map(([s, ss]) => {
77
+ const bg = getVTColor(comp, v, s, 'background', (vs.background as string));
78
+ const fg = getVTColor(comp, v, s, 'textColor', (vs.color as string));
79
+ const fs = getVTDim(comp, v, s, 'fontSize', String(ss.fontSize) + 'px');
80
+ const px = getVTDim(comp, v, s, 'paddingX', (ss.padding as string).split(' ')[1]);
81
+ const py = getVTDim(comp, v, s, 'paddingY', (ss.padding as string).split(' ')[0]);
82
+ const isOutline = v === 'outline';
83
+ const isGhost = v === 'ghost';
84
+ return (
85
+ <div className="variant-cell" key={`${v}-${s}`}>
86
+ <button style={{
87
+ padding: `${py} ${px}`, fontSize: fs, borderRadius: PILL, cursor: 'pointer',
88
+ background: (isOutline || isGhost) ? 'transparent' : bg,
89
+ color: (isOutline || isGhost) ? bg : fg,
90
+ border: isOutline ? `2px solid ${bg}` : '2px solid transparent',
91
+ fontWeight: 600, transition: TRANSITION, boxShadow: (isOutline || isGhost) ? 'none' : SHADOW_SM,
92
+ }}>{def.name}</button>
93
+ {label(`${v} / ${s}`)}
94
+ </div>
95
+ );
96
+ })
97
+ )}
98
+ </div>
99
+ <div style={{ marginTop: 16 }}>
100
+ <h4 style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 8 }}>States</h4>
101
+ <div style={row()}>
102
+ {states.map(st => (
103
+ <div key={st} style={{ textAlign: 'center' }}>
104
+ <button style={{
105
+ padding: '10px 20px', borderRadius: PILL, border: 'none', fontWeight: 600,
106
+ background: c.primary, color: c.primaryContent, cursor: st === 'disabled' ? 'not-allowed' : 'pointer',
107
+ opacity: st === 'disabled' ? 0.5 : 1,
108
+ filter: st === 'pressed' ? 'brightness(0.85)' : st === 'hover' ? 'brightness(1.1)' : 'none',
109
+ boxShadow: SHADOW_SM, transition: TRANSITION,
110
+ }}>{def.name}</button>
111
+ {label(st)}
112
+ </div>
113
+ ))}
114
+ </div>
115
+ </div>
116
+ </div>
117
+ );
118
+ }
119
+
120
+ function renderDropdown(_comp: any, _tokens: any[], c: Colors) {
121
+ const items = ['Action 1', 'Action 2', 'Action 3'];
122
+ return (
123
+ <div>
124
+ {sectionTitle('Dropdown')}
125
+ <div style={{ position: 'relative', display: 'inline-block' }}>
126
+ <button style={{
127
+ padding: '10px 20px', borderRadius: R, border: `1px solid ${c.borderColor}`,
128
+ background: c.base100, color: c.baseContent, cursor: 'pointer', fontWeight: 500,
129
+ display: 'flex', alignItems: 'center', gap: 8,
130
+ }}>
131
+ Options <span style={{ fontSize: 10 }}>&#9660;</span>
132
+ </button>
133
+ <div style={{
134
+ marginTop: 4, borderRadius: R, background: c.base100, border: `1px solid ${c.borderColor}`,
135
+ boxShadow: SHADOW_MD, overflow: 'hidden', minWidth: 180,
136
+ }}>
137
+ {items.map((item, i) => (
138
+ <div key={i} style={{
139
+ padding: '10px 16px', cursor: 'pointer', fontSize: 14, color: c.baseContent,
140
+ background: i === 0 ? c.base200 : 'transparent', borderBottom: i < items.length - 1 ? `1px solid ${c.borderColor}` : 'none',
141
+ }}>{item}</div>
142
+ ))}
143
+ </div>
144
+ </div>
145
+ </div>
146
+ );
147
+ }
148
+
149
+ function renderModal(_comp: any, _tokens: any[], c: Colors) {
150
+ return (
151
+ <div>
152
+ {sectionTitle('Modal')}
153
+ <div style={{
154
+ position: 'relative', width: 320, height: 220, borderRadius: R,
155
+ background: 'rgba(0,0,0,0.3)', display: 'flex', alignItems: 'center', justifyContent: 'center',
156
+ }}>
157
+ <div style={{
158
+ background: c.base100, borderRadius: R, padding: 24, width: 260,
159
+ boxShadow: SHADOW_LG,
160
+ }}>
161
+ <h4 style={{ margin: '0 0 8px', fontSize: 16, fontWeight: 700, color: c.baseContent }}>Modal Title</h4>
162
+ <p style={{ margin: '0 0 16px', fontSize: 13, color: c.neutral, opacity: 0.7 }}>
163
+ This is a confirmation dialog. Are you sure you want to proceed?
164
+ </p>
165
+ <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
166
+ <button style={{
167
+ padding: '6px 14px', borderRadius: R, border: `1px solid ${c.borderColor}`,
168
+ background: 'transparent', color: c.baseContent, cursor: 'pointer', fontSize: 13,
169
+ }}>Cancel</button>
170
+ <button style={{
171
+ padding: '6px 14px', borderRadius: R, border: 'none',
172
+ background: c.primary, color: c.primaryContent, cursor: 'pointer', fontSize: 13, fontWeight: 600,
173
+ }}>Confirm</button>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ );
179
+ }
180
+
181
+ function renderSwap(_comp: any, _tokens: any[], c: Colors) {
182
+ return (
183
+ <div>
184
+ {sectionTitle('Swap')}
185
+ <div style={row(24)}>
186
+ {['ON', 'OFF'].map(state => (
187
+ <div key={state} style={{ textAlign: 'center' }}>
188
+ <div style={{
189
+ width: 64, height: 64, borderRadius: R, display: 'flex', alignItems: 'center', justifyContent: 'center',
190
+ background: state === 'ON' ? c.primary : c.base200, color: state === 'ON' ? c.primaryContent : c.baseContent,
191
+ fontSize: 22, fontWeight: 700, boxShadow: SHADOW_SM, transition: TRANSITION,
192
+ transform: state === 'ON' ? 'rotate(0deg)' : 'rotate(180deg)',
193
+ }}>
194
+ {state === 'ON' ? '\u2605' : '\u2606'}
195
+ </div>
196
+ {label(state === 'ON' ? 'Rotate ON' : 'Flip OFF')}
197
+ </div>
198
+ ))}
199
+ </div>
200
+ </div>
201
+ );
202
+ }
203
+
204
+ function renderThemeController(_comp: any, _tokens: any[], c: Colors) {
205
+ return (
206
+ <div>
207
+ {sectionTitle('ThemeController')}
208
+ <div style={row(16)}>
209
+ <div style={{
210
+ width: 56, height: 28, borderRadius: PILL, background: c.primary, position: 'relative',
211
+ cursor: 'pointer', boxShadow: 'inset 0 1px 3px rgba(0,0,0,0.2)',
212
+ }}>
213
+ <div style={{
214
+ width: 22, height: 22, borderRadius: '50%', background: c.base100,
215
+ position: 'absolute', top: 3, left: 31, boxShadow: SHADOW_SM, transition: TRANSITION,
216
+ display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12,
217
+ }}>{'\u263E'}</div>
218
+ </div>
219
+ <span style={{ fontSize: 13, color: c.baseContent }}>Dark mode</span>
220
+ <div style={{ marginLeft: 24 }} />
221
+ <div style={{
222
+ width: 56, height: 28, borderRadius: PILL, background: c.base300, position: 'relative',
223
+ cursor: 'pointer', boxShadow: 'inset 0 1px 3px rgba(0,0,0,0.1)',
224
+ }}>
225
+ <div style={{
226
+ width: 22, height: 22, borderRadius: '50%', background: c.base100,
227
+ position: 'absolute', top: 3, left: 3, boxShadow: SHADOW_SM, transition: TRANSITION,
228
+ display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12,
229
+ }}>{'\u2600'}</div>
230
+ </div>
231
+ <span style={{ fontSize: 13, color: c.baseContent }}>Light mode</span>
232
+ </div>
233
+ </div>
234
+ );
235
+ }
236
+
237
+ /* ── Data Display ── */
238
+
239
+ function renderAccordion(_comp: any, _tokens: any[], c: Colors) {
240
+ const sections = [
241
+ { title: 'Section 1', content: 'This is the expanded content of section 1. It can contain any information.', open: true },
242
+ { title: 'Section 2', content: '', open: false },
243
+ { title: 'Section 3', content: '', open: false },
244
+ ];
245
+ return (
246
+ <div>
247
+ {sectionTitle('Accordion')}
248
+ <div style={{ width: 360, border: `1px solid ${c.borderColor}`, borderRadius: R, overflow: 'hidden' }}>
249
+ {sections.map((s, i) => (
250
+ <div key={i} style={{ borderBottom: i < sections.length - 1 ? `1px solid ${c.borderColor}` : 'none' }}>
251
+ <div style={{
252
+ padding: '12px 16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
253
+ cursor: 'pointer', background: s.open ? c.base200 : c.base100, fontWeight: 500,
254
+ fontSize: 14, color: c.baseContent,
255
+ }}>
256
+ {s.title}
257
+ <span style={{ fontSize: 12, transform: s.open ? 'rotate(90deg)' : 'rotate(0)', transition: TRANSITION }}>{'\u25B6'}</span>
258
+ </div>
259
+ {s.open && (
260
+ <div style={{ padding: '12px 16px', fontSize: 13, color: c.baseContent, background: c.base100, lineHeight: 1.5 }}>
261
+ {s.content}
262
+ </div>
263
+ )}
264
+ </div>
265
+ ))}
266
+ </div>
267
+ </div>
268
+ );
269
+ }
270
+
271
+ function renderAvatar(_comp: any, _tokens: any[], c: Colors) {
272
+ const sizes: Record<string, number> = { xs: 28, sm: 36, md: 48, lg: 64, xl: 80 };
273
+ return (
274
+ <div>
275
+ {sectionTitle('Avatar')}
276
+ <div style={row(16)}>
277
+ {Object.entries(sizes).map(([s, px]) => (
278
+ <div key={s} style={{ textAlign: 'center' }}>
279
+ <div style={{
280
+ width: px, height: px, borderRadius: '50%', background: c.primary,
281
+ color: c.primaryContent, display: 'flex', alignItems: 'center', justifyContent: 'center',
282
+ fontSize: px * 0.35, fontWeight: 700, boxShadow: SHADOW_SM,
283
+ }}>JD</div>
284
+ {label(s)}
285
+ </div>
286
+ ))}
287
+ <div style={{ textAlign: 'center' }}>
288
+ <div style={{
289
+ width: 48, height: 48, borderRadius: R, background: c.secondary,
290
+ color: c.primaryContent, display: 'flex', alignItems: 'center', justifyContent: 'center',
291
+ fontSize: 16, fontWeight: 700, boxShadow: SHADOW_SM,
292
+ }}>JD</div>
293
+ {label('square')}
294
+ </div>
295
+ </div>
296
+ </div>
297
+ );
298
+ }
299
+
300
+ function renderBadge(_comp: any, _tokens: any[], c: Colors) {
301
+ const colorMap: Record<string, string> = {
302
+ default: c.neutral, primary: c.primary, success: c.success, warning: c.warning, error: c.error,
303
+ };
304
+ const variantStyles = (color: string, variant: string): React.CSSProperties => {
305
+ if (variant === 'solid') return { background: color, color: '#fff' };
306
+ if (variant === 'subtle') return { background: color + '20', color };
307
+ return { background: 'transparent', color, border: `1.5px solid ${color}` };
308
+ };
309
+ return (
310
+ <div>
311
+ {sectionTitle('Badge')}
312
+ <div style={row(8)}>
313
+ {['solid', 'subtle', 'outline'].map(variant =>
314
+ Object.entries(colorMap).map(([name, color]) => (
315
+ <span key={`${variant}-${name}`} style={{
316
+ padding: '3px 10px', borderRadius: PILL, fontSize: 12, fontWeight: 600,
317
+ display: 'inline-block', border: 'none', ...variantStyles(color, variant),
318
+ }}>{name}</span>
319
+ ))
320
+ )}
321
+ </div>
322
+ </div>
323
+ );
324
+ }
325
+
326
+ function renderCard(_comp: any, _tokens: any[], c: Colors) {
327
+ const variants: Record<string, React.CSSProperties> = {
328
+ elevated: { boxShadow: SHADOW_MD, border: 'none', background: c.base100 },
329
+ outlined: { boxShadow: 'none', border: `1px solid ${c.borderColor}`, background: c.base100 },
330
+ filled: { boxShadow: 'none', border: 'none', background: c.base200 },
331
+ };
332
+ return (
333
+ <div>
334
+ {sectionTitle('Card')}
335
+ <div style={row(16)}>
336
+ {Object.entries(variants).map(([v, vs]) => (
337
+ <div key={v} style={{ width: 220, borderRadius: R, overflow: 'hidden', ...vs }}>
338
+ <div style={{ height: 80, background: c.base300, display: 'flex', alignItems: 'center', justifyContent: 'center', color: c.neutral, fontSize: 12 }}>
339
+ Image Placeholder
340
+ </div>
341
+ <div style={{ padding: 16 }}>
342
+ <h4 style={{ margin: '0 0 6px', fontSize: 15, fontWeight: 700, color: c.baseContent }}>Card Title</h4>
343
+ <p style={{ margin: '0 0 12px', fontSize: 13, color: c.baseContent, opacity: 0.7, lineHeight: 1.4 }}>
344
+ Brief card description text.
345
+ </p>
346
+ <button style={{
347
+ padding: '6px 14px', borderRadius: R, border: 'none', background: c.primary,
348
+ color: c.primaryContent, cursor: 'pointer', fontSize: 12, fontWeight: 600,
349
+ }}>Action</button>
350
+ </div>
351
+ {label(v)}
352
+ </div>
353
+ ))}
354
+ </div>
355
+ </div>
356
+ );
357
+ }
358
+
359
+ function renderCarousel(_comp: any, _tokens: any[], c: Colors) {
360
+ return (
361
+ <div>
362
+ {sectionTitle('Carousel')}
363
+ <div style={{ width: 360, borderRadius: R, overflow: 'hidden', background: c.base200, boxShadow: SHADOW_SM }}>
364
+ <div style={{
365
+ height: 160, display: 'flex', alignItems: 'center', justifyContent: 'center',
366
+ background: c.base300, color: c.neutral, fontSize: 14,
367
+ }}>Slide 1 of 3</div>
368
+ <div style={{ display: 'flex', justifyContent: 'center', gap: 8, padding: 12 }}>
369
+ {[0, 1, 2].map(i => (
370
+ <div key={i} style={{
371
+ width: 10, height: 10, borderRadius: '50%',
372
+ background: i === 0 ? c.primary : c.base300,
373
+ }} />
374
+ ))}
375
+ </div>
376
+ </div>
377
+ </div>
378
+ );
379
+ }
380
+
381
+ function renderChatBubble(_comp: any, _tokens: any[], c: Colors) {
382
+ const msgs = [
383
+ { side: 'start' as const, text: 'Hey! How are you?', time: '12:45' },
384
+ { side: 'end' as const, text: 'I\'m good, thanks!', time: '12:46' },
385
+ ];
386
+ return (
387
+ <div>
388
+ {sectionTitle('ChatBubble')}
389
+ <div style={{ ...col(12), width: 360 }}>
390
+ {msgs.map((m, i) => (
391
+ <div key={i} style={{
392
+ display: 'flex', flexDirection: m.side === 'end' ? 'row-reverse' : 'row',
393
+ alignItems: 'flex-end', gap: 8,
394
+ }}>
395
+ <div style={{
396
+ width: 32, height: 32, borderRadius: '50%', background: m.side === 'start' ? c.primary : c.secondary,
397
+ color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700, flexShrink: 0,
398
+ }}>{m.side === 'start' ? 'A' : 'B'}</div>
399
+ <div>
400
+ <div style={{
401
+ padding: '10px 14px', borderRadius: 16,
402
+ borderBottomLeftRadius: m.side === 'start' ? 4 : 16,
403
+ borderBottomRightRadius: m.side === 'end' ? 4 : 16,
404
+ background: m.side === 'start' ? c.base200 : c.primary,
405
+ color: m.side === 'start' ? c.baseContent : c.primaryContent,
406
+ fontSize: 14, maxWidth: 240,
407
+ }}>{m.text}</div>
408
+ <div style={{ fontSize: 10, color: c.baseContent, opacity: 0.5, marginTop: 2, textAlign: m.side === 'end' ? 'right' : 'left' }}>{m.time}</div>
409
+ </div>
410
+ </div>
411
+ ))}
412
+ </div>
413
+ </div>
414
+ );
415
+ }
416
+
417
+ function renderCountdown(_comp: any, _tokens: any[], c: Colors) {
418
+ return (
419
+ <div>
420
+ {sectionTitle('Countdown')}
421
+ <div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
422
+ {['23', '59', '59'].map((n, i) => (
423
+ <React.Fragment key={i}>
424
+ {i > 0 && <span style={{ fontSize: 32, fontWeight: 700, color: c.baseContent }}>:</span>}
425
+ <div style={{
426
+ background: c.base200, borderRadius: R, padding: '12px 16px',
427
+ fontSize: 36, fontWeight: 700, fontFamily: 'monospace', color: c.baseContent,
428
+ minWidth: 60, textAlign: 'center', boxShadow: SHADOW_SM,
429
+ }}>{n}</div>
430
+ </React.Fragment>
431
+ ))}
432
+ </div>
433
+ </div>
434
+ );
435
+ }
436
+
437
+ function renderKbd(_comp: any, _tokens: any[], c: Colors) {
438
+ const keys = ['Ctrl', 'Shift', 'A', '\u2318', 'Esc'];
439
+ return (
440
+ <div>
441
+ {sectionTitle('Kbd')}
442
+ <div style={row(8)}>
443
+ {keys.map(k => (
444
+ <kbd key={k} style={{
445
+ display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
446
+ padding: '6px 12px', minWidth: 36, fontSize: 13, fontWeight: 600, fontFamily: 'inherit',
447
+ background: c.base200, color: c.baseContent, borderRadius: 6,
448
+ border: `1px solid ${c.borderColor}`, borderBottom: `3px solid ${c.borderColor}`,
449
+ boxShadow: '0 1px 0 rgba(0,0,0,0.08)',
450
+ }}>{k}</kbd>
451
+ ))}
452
+ </div>
453
+ </div>
454
+ );
455
+ }
456
+
457
+ function renderStat(_comp: any, _tokens: any[], c: Colors) {
458
+ const stats = [
459
+ { title: 'Total Users', value: '31.2K', desc: '\u2191 12% from last month', descColor: c.success },
460
+ { title: 'Revenue', value: '$4,200', desc: '\u2193 3% from last month', descColor: c.error },
461
+ { title: 'Satisfaction', value: '98%', desc: 'Based on 5K reviews', descColor: c.baseContent },
462
+ ];
463
+ return (
464
+ <div>
465
+ {sectionTitle('Stat')}
466
+ <div style={{ display: 'flex', gap: 0, border: `1px solid ${c.borderColor}`, borderRadius: R, overflow: 'hidden' }}>
467
+ {stats.map((s, i) => (
468
+ <div key={i} style={{
469
+ padding: '16px 24px', borderRight: i < stats.length - 1 ? `1px solid ${c.borderColor}` : 'none',
470
+ background: c.base100,
471
+ }}>
472
+ <div style={{ fontSize: 12, color: c.baseContent, opacity: 0.6, marginBottom: 4 }}>{s.title}</div>
473
+ <div style={{ fontSize: 24, fontWeight: 700, color: c.baseContent, marginBottom: 4 }}>{s.value}</div>
474
+ <div style={{ fontSize: 12, color: s.descColor, opacity: 0.8 }}>{s.desc}</div>
475
+ </div>
476
+ ))}
477
+ </div>
478
+ </div>
479
+ );
480
+ }
481
+
482
+ function renderTable(_comp: any, _tokens: any[], c: Colors) {
483
+ const headers = ['Name', 'Role', 'Status'];
484
+ const rows = [
485
+ ['Alice', 'Engineer', 'Active'],
486
+ ['Bob', 'Designer', 'Away'],
487
+ ['Carol', 'Manager', 'Active'],
488
+ ];
489
+ return (
490
+ <div>
491
+ {sectionTitle('Table')}
492
+ <div style={{ borderRadius: R, overflow: 'hidden', border: `1px solid ${c.borderColor}`, width: 400 }}>
493
+ <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 14 }}>
494
+ <thead>
495
+ <tr style={{ background: c.base200 }}>
496
+ {headers.map(h => (
497
+ <th key={h} style={{ textAlign: 'left', padding: '10px 14px', fontWeight: 600, color: c.baseContent, fontSize: 12, textTransform: 'uppercase', letterSpacing: 0.5 }}>{h}</th>
498
+ ))}
499
+ </tr>
500
+ </thead>
501
+ <tbody>
502
+ {rows.map((r, i) => (
503
+ <tr key={i} style={{ background: i % 2 === 1 ? c.base200 + '80' : c.base100 }}>
504
+ {r.map((cell, j) => (
505
+ <td key={j} style={{ padding: '10px 14px', color: c.baseContent, borderTop: `1px solid ${c.borderColor}` }}>{cell}</td>
506
+ ))}
507
+ </tr>
508
+ ))}
509
+ </tbody>
510
+ </table>
511
+ </div>
512
+ </div>
513
+ );
514
+ }
515
+
516
+ /* ── Navigation ── */
517
+
518
+ function renderBreadcrumbs(_comp: any, _tokens: any[], c: Colors) {
519
+ const items = ['Home', 'Documents', 'Add'];
520
+ return (
521
+ <div>
522
+ {sectionTitle('Breadcrumbs')}
523
+ <nav style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14 }}>
524
+ {items.map((item, i) => (
525
+ <React.Fragment key={i}>
526
+ {i > 0 && <span style={{ color: c.baseContent, opacity: 0.4 }}>{'\u203A'}</span>}
527
+ <a style={{
528
+ color: i < items.length - 1 ? c.primary : c.baseContent,
529
+ textDecoration: 'none', fontWeight: i === items.length - 1 ? 600 : 400,
530
+ cursor: i < items.length - 1 ? 'pointer' : 'default',
531
+ }}>{item}</a>
532
+ </React.Fragment>
533
+ ))}
534
+ </nav>
535
+ </div>
536
+ );
537
+ }
538
+
539
+ function renderMenu(_comp: any, _tokens: any[], c: Colors) {
540
+ const items = [
541
+ { icon: '\u2302', label: 'Dashboard', active: false },
542
+ { icon: '\u2709', label: 'Messages', active: true },
543
+ { icon: '\u2699', label: 'Settings', active: false },
544
+ { icon: '\u2139', label: 'About', active: false },
545
+ ];
546
+ return (
547
+ <div>
548
+ {sectionTitle('Menu')}
549
+ <div style={{ width: 220, borderRadius: R, background: c.base100, border: `1px solid ${c.borderColor}`, overflow: 'hidden', boxShadow: SHADOW_SM }}>
550
+ {items.map((item, i) => (
551
+ <div key={i} style={{
552
+ padding: '10px 16px', display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer',
553
+ background: item.active ? c.primary : 'transparent',
554
+ color: item.active ? c.primaryContent : c.baseContent,
555
+ fontSize: 14, fontWeight: item.active ? 600 : 400,
556
+ }}>
557
+ <span style={{ fontSize: 16 }}>{item.icon}</span>
558
+ {item.label}
559
+ </div>
560
+ ))}
561
+ </div>
562
+ </div>
563
+ );
564
+ }
565
+
566
+ function renderNavbar(_comp: any, _tokens: any[], c: Colors) {
567
+ return (
568
+ <div>
569
+ {sectionTitle('Navbar')}
570
+ <div style={{
571
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
572
+ padding: '12px 20px', background: c.base100, borderRadius: R,
573
+ border: `1px solid ${c.borderColor}`, boxShadow: SHADOW_SM, width: 500,
574
+ }}>
575
+ <div style={{ fontWeight: 700, fontSize: 18, color: c.baseContent }}>Logo</div>
576
+ <div style={{ display: 'flex', gap: 20 }}>
577
+ {['Home', 'About', 'Contact'].map((link, i) => (
578
+ <a key={i} style={{ color: i === 0 ? c.primary : c.baseContent, textDecoration: 'none', fontSize: 14, fontWeight: i === 0 ? 600 : 400, cursor: 'pointer' }}>{link}</a>
579
+ ))}
580
+ </div>
581
+ <button style={{
582
+ padding: '6px 14px', borderRadius: R, border: 'none', background: c.primary,
583
+ color: c.primaryContent, cursor: 'pointer', fontSize: 13, fontWeight: 600,
584
+ }}>Sign In</button>
585
+ </div>
586
+ </div>
587
+ );
588
+ }
589
+
590
+ function renderPagination(_comp: any, _tokens: any[], c: Colors) {
591
+ const pages = [1, 2, 3, 4, 5];
592
+ const active = 3;
593
+ return (
594
+ <div>
595
+ {sectionTitle('Pagination')}
596
+ <div style={{ display: 'flex', gap: 4 }}>
597
+ {['\u2039', ...pages.map(String), '\u203A'].map((p, i) => (
598
+ <button key={i} style={{
599
+ width: 36, height: 36, borderRadius: R, border: `1px solid ${c.borderColor}`,
600
+ background: String(active) === p ? c.primary : c.base100,
601
+ color: String(active) === p ? c.primaryContent : c.baseContent,
602
+ cursor: 'pointer', fontSize: 14, fontWeight: String(active) === p ? 700 : 400,
603
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
604
+ }}>{p}</button>
605
+ ))}
606
+ </div>
607
+ </div>
608
+ );
609
+ }
610
+
611
+ function renderSteps(_comp: any, _tokens: any[], c: Colors) {
612
+ const steps = ['Register', 'Details', 'Review', 'Done'];
613
+ const completed = 2;
614
+ return (
615
+ <div>
616
+ {sectionTitle('Steps')}
617
+ <div style={{ display: 'flex', alignItems: 'center', gap: 0 }}>
618
+ {steps.map((s, i) => (
619
+ <React.Fragment key={i}>
620
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
621
+ <div style={{
622
+ width: 32, height: 32, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center',
623
+ background: i < completed ? c.primary : i === completed ? c.base300 : c.base200,
624
+ color: i < completed ? c.primaryContent : c.baseContent,
625
+ fontSize: 13, fontWeight: 700, boxShadow: i < completed ? SHADOW_SM : 'none',
626
+ }}>{i < completed ? '\u2713' : i + 1}</div>
627
+ <span style={{ fontSize: 11, color: c.baseContent, fontWeight: i < completed ? 600 : 400 }}>{s}</span>
628
+ </div>
629
+ {i < steps.length - 1 && (
630
+ <div style={{
631
+ flex: 1, height: 2, minWidth: 40, background: i < completed - 1 ? c.primary : c.borderColor,
632
+ margin: '0 4px', marginBottom: 20,
633
+ }} />
634
+ )}
635
+ </React.Fragment>
636
+ ))}
637
+ </div>
638
+ </div>
639
+ );
640
+ }
641
+
642
+ function renderTab(_comp: any, _tokens: any[], c: Colors) {
643
+ const tabs = ['Tab 1', 'Tab 2', 'Tab 3'];
644
+ const variants: Record<string, (active: boolean) => React.CSSProperties> = {
645
+ bordered: (a) => ({
646
+ padding: '10px 20px', cursor: 'pointer', fontSize: 14, fontWeight: a ? 600 : 400,
647
+ color: a ? c.primary : c.baseContent, background: 'transparent', border: 'none',
648
+ borderBottom: a ? `2px solid ${c.primary}` : '2px solid transparent',
649
+ }),
650
+ lifted: (a) => ({
651
+ padding: '10px 20px', cursor: 'pointer', fontSize: 14, fontWeight: a ? 600 : 400,
652
+ color: a ? c.baseContent : c.baseContent, background: a ? c.base100 : c.base200,
653
+ border: a ? `1px solid ${c.borderColor}` : 'none', borderBottom: a ? 'none' : `1px solid ${c.borderColor}`,
654
+ borderRadius: a ? `${R} ${R} 0 0` : '0', position: 'relative' as const, bottom: a ? -1 : 0,
655
+ }),
656
+ boxed: (a) => ({
657
+ padding: '10px 20px', cursor: 'pointer', fontSize: 14, fontWeight: a ? 600 : 400,
658
+ color: a ? c.primaryContent : c.baseContent, background: a ? c.primary : 'transparent',
659
+ border: 'none', borderRadius: R,
660
+ }),
661
+ };
662
+ return (
663
+ <div>
664
+ {sectionTitle('Tab')}
665
+ <div style={col(16)}>
666
+ {Object.entries(variants).map(([v, styleFn]) => (
667
+ <div key={v}>
668
+ <div style={{
669
+ display: 'flex', gap: v === 'boxed' ? 4 : 0,
670
+ borderBottom: v !== 'boxed' ? `1px solid ${c.borderColor}` : 'none',
671
+ background: v === 'boxed' ? c.base200 : 'transparent',
672
+ borderRadius: v === 'boxed' ? R : 0, padding: v === 'boxed' ? 4 : 0,
673
+ }}>
674
+ {tabs.map((t, i) => (
675
+ <div key={i} style={styleFn(i === 0)}>{t}</div>
676
+ ))}
677
+ </div>
678
+ {label(v)}
679
+ </div>
680
+ ))}
681
+ </div>
682
+ </div>
683
+ );
684
+ }
685
+
686
+ /* ── Feedback ── */
687
+
688
+ function renderAlert(_comp: any, _tokens: any[], c: Colors) {
689
+ const types: Record<string, { color: string; icon: string }> = {
690
+ info: { color: c.info, icon: '\u2139' },
691
+ success: { color: c.success, icon: '\u2713' },
692
+ warning: { color: c.warning, icon: '\u26A0' },
693
+ error: { color: c.error, icon: '\u2717' },
694
+ };
695
+ return (
696
+ <div>
697
+ {sectionTitle('Alert')}
698
+ <div style={col(8)}>
699
+ {Object.entries(types).map(([name, t]) => (
700
+ <div key={name} style={{
701
+ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 16px',
702
+ borderRadius: R, background: t.color + '18', border: `1px solid ${t.color}40`,
703
+ color: t.color, fontSize: 14, width: 400,
704
+ }}>
705
+ <span style={{ fontSize: 18, fontWeight: 700 }}>{t.icon}</span>
706
+ <span>This is a {name} alert message.</span>
707
+ </div>
708
+ ))}
709
+ </div>
710
+ </div>
711
+ );
712
+ }
713
+
714
+ function renderLoading(_comp: any, _tokens: any[], c: Colors) {
715
+ const spinnerStyle: React.CSSProperties = {
716
+ width: 32, height: 32, border: `3px solid ${c.borderColor}`, borderTopColor: c.primary,
717
+ borderRadius: '50%', animation: 'spin 0.8s linear infinite',
718
+ };
719
+ return (
720
+ <div>
721
+ {sectionTitle('Loading')}
722
+ <style>{`@keyframes spin { to { transform: rotate(360deg); } } @keyframes pulse { 0%,80%,100% { transform: scale(0); } 40% { transform: scale(1); } }`}</style>
723
+ <div style={row(32)}>
724
+ <div style={{ textAlign: 'center' }}>
725
+ <div style={spinnerStyle} />
726
+ {label('spinner')}
727
+ </div>
728
+ <div style={{ textAlign: 'center' }}>
729
+ <div style={{ display: 'flex', gap: 4, alignItems: 'center', height: 32 }}>
730
+ {[0, 1, 2].map(i => (
731
+ <div key={i} style={{
732
+ width: 10, height: 10, borderRadius: '50%', background: c.primary,
733
+ animation: `pulse 1.4s ease-in-out ${i * 0.16}s infinite`,
734
+ }} />
735
+ ))}
736
+ </div>
737
+ {label('dots')}
738
+ </div>
739
+ <div style={{ textAlign: 'center' }}>
740
+ <div style={{
741
+ width: 32, height: 32, borderRadius: '50%', border: `3px solid ${c.borderColor}`,
742
+ borderRightColor: c.primary, borderBottomColor: c.primary,
743
+ animation: 'spin 1s linear infinite',
744
+ }} />
745
+ {label('half')}
746
+ </div>
747
+ <div style={{ textAlign: 'center' }}>
748
+ <div style={{
749
+ width: 32, height: 32, border: `3px solid ${c.primary}`, borderRadius: R,
750
+ animation: 'spin 1.2s ease-in-out infinite',
751
+ }} />
752
+ {label('square')}
753
+ </div>
754
+ </div>
755
+ </div>
756
+ );
757
+ }
758
+
759
+ function renderProgress(_comp: any, _tokens: any[], c: Colors) {
760
+ const percentages = [25, 50, 75];
761
+ return (
762
+ <div>
763
+ {sectionTitle('Progress')}
764
+ <div style={{ ...col(12), width: 360 }}>
765
+ {percentages.map(p => (
766
+ <div key={p}>
767
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
768
+ <span style={{ fontSize: 12, color: c.baseContent }}>{p}%</span>
769
+ </div>
770
+ <div style={{
771
+ height: 10, borderRadius: PILL, background: c.base200, overflow: 'hidden',
772
+ }}>
773
+ <div style={{
774
+ height: '100%', width: `${p}%`, borderRadius: PILL,
775
+ background: p >= 75 ? c.success : p >= 50 ? c.primary : c.warning,
776
+ transition: 'width 0.3s ease',
777
+ }} />
778
+ </div>
779
+ </div>
780
+ ))}
781
+ </div>
782
+ </div>
783
+ );
784
+ }
785
+
786
+ function renderTooltip(_comp: any, _tokens: any[], c: Colors) {
787
+ return (
788
+ <div>
789
+ {sectionTitle('Tooltip')}
790
+ <div style={row(48)}>
791
+ {['top', 'bottom'].map(pos => (
792
+ <div key={pos} style={{ position: 'relative', display: 'inline-flex', flexDirection: 'column', alignItems: 'center' }}>
793
+ {pos === 'top' && (
794
+ <div style={{ marginBottom: 8, position: 'relative' }}>
795
+ <div style={{
796
+ padding: '6px 12px', borderRadius: 6, background: c.neutral, color: '#fff',
797
+ fontSize: 12, fontWeight: 500, whiteSpace: 'nowrap',
798
+ }}>Tooltip text</div>
799
+ <div style={{
800
+ position: 'absolute', bottom: -4, left: '50%', transform: 'translateX(-50%)',
801
+ width: 0, height: 0, borderLeft: '5px solid transparent', borderRight: '5px solid transparent',
802
+ borderTop: `5px solid ${c.neutral}`,
803
+ }} />
804
+ </div>
805
+ )}
806
+ <button style={{
807
+ padding: '8px 16px', borderRadius: R, border: `1px solid ${c.borderColor}`,
808
+ background: c.base100, color: c.baseContent, cursor: 'pointer', fontSize: 13,
809
+ }}>Hover me ({pos})</button>
810
+ {pos === 'bottom' && (
811
+ <div style={{ marginTop: 8, position: 'relative' }}>
812
+ <div style={{
813
+ position: 'absolute', top: -4, left: '50%', transform: 'translateX(-50%)',
814
+ width: 0, height: 0, borderLeft: '5px solid transparent', borderRight: '5px solid transparent',
815
+ borderBottom: `5px solid ${c.neutral}`,
816
+ }} />
817
+ <div style={{
818
+ padding: '6px 12px', borderRadius: 6, background: c.neutral, color: '#fff',
819
+ fontSize: 12, fontWeight: 500, whiteSpace: 'nowrap',
820
+ }}>Tooltip text</div>
821
+ </div>
822
+ )}
823
+ </div>
824
+ ))}
825
+ </div>
826
+ </div>
827
+ );
828
+ }
829
+
830
+ /* ── Forms ── */
831
+
832
+ function renderCheckbox(_comp: any, _tokens: any[], c: Colors) {
833
+ const sizes: Record<string, number> = { sm: 16, md: 20, lg: 26 };
834
+ return (
835
+ <div>
836
+ {sectionTitle('Checkbox')}
837
+ <div style={row(20)}>
838
+ {Object.entries(sizes).map(([s, px]) => (
839
+ <div key={s} style={row(12)}>
840
+ {[true, false].map(checked => (
841
+ <div key={String(checked)} style={{ textAlign: 'center' }}>
842
+ <div style={{
843
+ width: px, height: px, borderRadius: 4, cursor: 'pointer',
844
+ border: checked ? 'none' : `2px solid ${c.borderColor}`,
845
+ background: checked ? c.primary : 'transparent',
846
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
847
+ color: c.primaryContent, fontSize: px * 0.6, fontWeight: 700,
848
+ transition: TRANSITION,
849
+ }}>{checked ? '\u2713' : ''}</div>
850
+ {label(`${s} ${checked ? 'on' : 'off'}`)}
851
+ </div>
852
+ ))}
853
+ </div>
854
+ ))}
855
+ </div>
856
+ </div>
857
+ );
858
+ }
859
+
860
+ function renderInput(comp: any, tokens: any[], c: Colors) {
861
+ const variants: Record<string, React.CSSProperties> = {
862
+ bordered: { border: `1.5px solid ${c.borderColor}`, background: c.base100 },
863
+ filled: { border: '1.5px solid transparent', background: c.base200 },
864
+ ghost: { border: '1.5px solid transparent', background: 'transparent', borderBottom: `1.5px solid ${c.borderColor}` },
865
+ };
866
+ return (
867
+ <div>
868
+ {sectionTitle('Input')}
869
+ <div style={row(16)}>
870
+ {Object.entries(variants).map(([v, vs], i) => (
871
+ <div key={v} style={{ textAlign: 'center' }}>
872
+ <input
873
+ readOnly
874
+ value={i === 0 ? '' : 'Sample text'}
875
+ placeholder="Placeholder..."
876
+ style={{
877
+ padding: '10px 14px', borderRadius: R, fontSize: 14, outline: 'none',
878
+ color: c.baseContent, width: 180, transition: TRANSITION,
879
+ boxShadow: i === 0 ? `0 0 0 2px ${c.primary}40` : 'none', ...vs,
880
+ }}
881
+ />
882
+ {label(v + (i === 0 ? ' (focus)' : ''))}
883
+ </div>
884
+ ))}
885
+ </div>
886
+ </div>
887
+ );
888
+ }
889
+
890
+ function renderToggle(_comp: any, _tokens: any[], c: Colors) {
891
+ const sizes: Record<string, { w: number; h: number; thumb: number }> = {
892
+ sm: { w: 36, h: 20, thumb: 14 },
893
+ md: { w: 48, h: 26, thumb: 20 },
894
+ lg: { w: 60, h: 32, thumb: 26 },
895
+ };
896
+ return (
897
+ <div>
898
+ {sectionTitle('Toggle')}
899
+ <div style={row(20)}>
900
+ {Object.entries(sizes).map(([s, dim]) => (
901
+ <div key={s} style={row(12)}>
902
+ {[true, false].map(on => (
903
+ <div key={String(on)} style={{ textAlign: 'center' }}>
904
+ <div style={{
905
+ width: dim.w, height: dim.h, borderRadius: PILL, cursor: 'pointer',
906
+ background: on ? c.primary : c.base300, position: 'relative',
907
+ boxShadow: 'inset 0 1px 3px rgba(0,0,0,0.15)', transition: TRANSITION,
908
+ }}>
909
+ <div style={{
910
+ width: dim.thumb, height: dim.thumb, borderRadius: '50%', background: '#fff',
911
+ position: 'absolute', top: (dim.h - dim.thumb) / 2,
912
+ left: on ? dim.w - dim.thumb - (dim.h - dim.thumb) / 2 : (dim.h - dim.thumb) / 2,
913
+ boxShadow: SHADOW_SM, transition: TRANSITION,
914
+ }} />
915
+ </div>
916
+ {label(`${s} ${on ? 'on' : 'off'}`)}
917
+ </div>
918
+ ))}
919
+ </div>
920
+ ))}
921
+ </div>
922
+ </div>
923
+ );
924
+ }
925
+
926
+ /* ════════════════════════════════════════════════════════════════
927
+ GENERIC FALLBACK
928
+ ════════════════════════════════════════════════════════════════ */
929
+
930
+ function renderGenericFallback(comp: any, tokens: any[], c: Colors) {
29
931
  const def = comp.definition ?? comp;
30
932
  const name = def.name;
31
933
  const variants = def.variants?.variant?.values ?? ['default'];
@@ -34,83 +936,116 @@ function renderComponentPreview(comp: any, tokens: any[]) {
34
936
 
35
937
  return (
36
938
  <div>
37
- <h3 style={{ fontSize: 16, marginBottom: 16 }}>{name}</h3>
939
+ {sectionTitle(name)}
38
940
  <div className="variant-grid">
39
941
  {variants.map((variant: string) =>
40
942
  sizes.map((size: string) => {
41
- // Try to get tokens from variantTokens
42
943
  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
-
944
+ const bg = getVT(vt, 'background', c.primary);
945
+ const fg = getVT(vt, 'textColor', c.primaryContent);
946
+ const fs = getVT(vt, 'fontSize', size === 'sm' ? '13px' : size === 'lg' ? '17px' : '15px');
947
+ const px = getVT(vt, 'paddingX', size === 'sm' ? '12px' : size === 'lg' ? '24px' : '16px');
948
+ const py = getVT(vt, 'paddingY', size === 'sm' ? '6px' : size === 'lg' ? '14px' : '10px');
949
+ const isTransparent = variant === 'outline' || variant === 'ghost';
73
950
  return (
74
951
  <div className="variant-cell" key={`${variant}-${size}`}>
75
- <button style={style}>{name}</button>
76
- <span>{variant}/{size}</span>
952
+ <div style={{
953
+ padding: `${py} ${px}`, borderRadius: R, fontSize: fs, fontWeight: 500,
954
+ background: isTransparent ? 'transparent' : bg,
955
+ color: isTransparent ? bg : fg,
956
+ border: variant === 'outline' ? `2px solid ${bg}` : '2px solid transparent',
957
+ display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
958
+ boxShadow: isTransparent ? 'none' : SHADOW_SM,
959
+ }}>{name}</div>
960
+ {label(`${variant} / ${size}`)}
77
961
  </div>
78
962
  );
79
963
  })
80
964
  )}
81
965
  </div>
966
+ </div>
967
+ );
968
+ }
82
969
 
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
- )}
970
+ /* ════════════════════════════════════════════════════════════════
971
+ DISPATCHER
972
+ ════════════════════════════════════════════════════════════════ */
973
+
974
+ function renderComponentPreview(comp: any, tokens: any[]) {
975
+ const def = comp.definition ?? comp;
976
+ const name = def.name;
977
+ const colors = getThemeColors(tokens);
978
+
979
+ switch (name) {
980
+ // Actions
981
+ case 'Button': return renderButton(comp, tokens, colors);
982
+ case 'Dropdown': return renderDropdown(comp, tokens, colors);
983
+ case 'Modal': return renderModal(comp, tokens, colors);
984
+ case 'Swap': return renderSwap(comp, tokens, colors);
985
+ case 'ThemeController': return renderThemeController(comp, tokens, colors);
986
+ // Data Display
987
+ case 'Accordion': return renderAccordion(comp, tokens, colors);
988
+ case 'Avatar': return renderAvatar(comp, tokens, colors);
989
+ case 'Badge': return renderBadge(comp, tokens, colors);
990
+ case 'Card': return renderCard(comp, tokens, colors);
991
+ case 'Carousel': return renderCarousel(comp, tokens, colors);
992
+ case 'ChatBubble': return renderChatBubble(comp, tokens, colors);
993
+ case 'Countdown': return renderCountdown(comp, tokens, colors);
994
+ case 'Kbd': return renderKbd(comp, tokens, colors);
995
+ case 'Stat': return renderStat(comp, tokens, colors);
996
+ case 'Table': return renderTable(comp, tokens, colors);
997
+ // Navigation
998
+ case 'Breadcrumbs': return renderBreadcrumbs(comp, tokens, colors);
999
+ case 'Menu': return renderMenu(comp, tokens, colors);
1000
+ case 'Navbar': return renderNavbar(comp, tokens, colors);
1001
+ case 'Pagination': return renderPagination(comp, tokens, colors);
1002
+ case 'Steps': return renderSteps(comp, tokens, colors);
1003
+ case 'Tab': return renderTab(comp, tokens, colors);
1004
+ // Feedback
1005
+ case 'Alert': return renderAlert(comp, tokens, colors);
1006
+ case 'Loading': return renderLoading(comp, tokens, colors);
1007
+ case 'Progress': return renderProgress(comp, tokens, colors);
1008
+ case 'Tooltip': return renderTooltip(comp, tokens, colors);
1009
+ // Forms
1010
+ case 'Checkbox': return renderCheckbox(comp, tokens, colors);
1011
+ case 'Input': return renderInput(comp, tokens, colors);
1012
+ case 'Toggle': return renderToggle(comp, tokens, colors);
1013
+ // Fallback
1014
+ default: return renderGenericFallback(comp, tokens, colors);
1015
+ }
1016
+ }
1017
+
1018
+ /* ════════════════════════════════════════════════════════════════
1019
+ FALLBACK PREVIEW (no components loaded)
1020
+ ════════════════════════════════════════════════════════════════ */
1021
+
1022
+ function renderFallbackPreview(tokens: any[]) {
1023
+ const c = getThemeColors(tokens);
1024
+ return (
1025
+ <div className="component-card">
1026
+ <h4 style={{ fontSize: 15, fontWeight: 600, marginBottom: 12 }}>Button</h4>
1027
+ <div style={row(8)}>
1028
+ <button style={{
1029
+ padding: '10px 20px', borderRadius: PILL, border: 'none',
1030
+ background: c.primary, color: c.primaryContent, cursor: 'pointer', fontWeight: 600, boxShadow: SHADOW_SM,
1031
+ }}>Primary</button>
1032
+ <button style={{
1033
+ padding: '10px 20px', borderRadius: PILL,
1034
+ border: `2px solid ${c.primary}`, background: 'transparent', color: c.primary, cursor: 'pointer', fontWeight: 600,
1035
+ }}>Outline</button>
1036
+ <button style={{
1037
+ padding: '10px 20px', borderRadius: PILL, border: '2px solid transparent',
1038
+ background: 'transparent', color: c.primary, cursor: 'pointer', fontWeight: 600,
1039
+ }}>Ghost</button>
1040
+ </div>
110
1041
  </div>
111
1042
  );
112
1043
  }
113
1044
 
1045
+ /* ════════════════════════════════════════════════════════════════
1046
+ MAIN EXPORT
1047
+ ════════════════════════════════════════════════════════════════ */
1048
+
114
1049
  export function PreviewPanel({ tokenSet, components, selectedComponent, splitView, currentTheme }: Props) {
115
1050
  const tokens = tokenSet?.tokens ?? [];
116
1051
 
@@ -163,26 +1098,3 @@ export function PreviewPanel({ tokenSet, components, selectedComponent, splitVie
163
1098
  </div>
164
1099
  );
165
1100
  }
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
- }