@fragments-sdk/viewer 0.2.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.
Files changed (141) hide show
  1. package/LICENSE +84 -0
  2. package/index.html +28 -0
  3. package/package.json +71 -0
  4. package/src/__tests__/a11y-fixes.test.ts +358 -0
  5. package/src/__tests__/jsx-parser.test.ts +502 -0
  6. package/src/__tests__/render-utils.test.ts +232 -0
  7. package/src/__tests__/style-utils.test.ts +404 -0
  8. package/src/app/index.ts +1 -0
  9. package/src/assets/fragments-logo.ts +4 -0
  10. package/src/assets/fragments_logo.png +0 -0
  11. package/src/components/AccessibilityPanel.tsx +1457 -0
  12. package/src/components/ActionCapture.tsx +172 -0
  13. package/src/components/ActionsPanel.tsx +332 -0
  14. package/src/components/AllVariantsPreview.tsx +78 -0
  15. package/src/components/App.tsx +604 -0
  16. package/src/components/BottomPanel.tsx +288 -0
  17. package/src/components/CodePanel.naming.test.tsx +59 -0
  18. package/src/components/CodePanel.tsx +118 -0
  19. package/src/components/CommandPalette.tsx +392 -0
  20. package/src/components/ComponentDocView.tsx +164 -0
  21. package/src/components/ComponentGraph.tsx +380 -0
  22. package/src/components/ComponentHeader.tsx +88 -0
  23. package/src/components/ContractPanel.tsx +241 -0
  24. package/src/components/DeviceMockup.tsx +156 -0
  25. package/src/components/EmptyVariantMessage.tsx +54 -0
  26. package/src/components/ErrorBoundary.tsx +97 -0
  27. package/src/components/FigmaEmbed.tsx +238 -0
  28. package/src/components/FragmentEditor.tsx +525 -0
  29. package/src/components/FragmentRenderer.tsx +61 -0
  30. package/src/components/HeaderSearch.tsx +24 -0
  31. package/src/components/HealthDashboard.tsx +441 -0
  32. package/src/components/HmrStatusIndicator.tsx +61 -0
  33. package/src/components/Icons.tsx +479 -0
  34. package/src/components/InteractionsPanel.tsx +757 -0
  35. package/src/components/IsolatedPreviewFrame.tsx +390 -0
  36. package/src/components/IsolatedRender.tsx +113 -0
  37. package/src/components/KeyboardShortcutsHelp.tsx +53 -0
  38. package/src/components/LandingPage.tsx +420 -0
  39. package/src/components/Layout.tsx +27 -0
  40. package/src/components/LeftSidebar.tsx +472 -0
  41. package/src/components/LoadErrorMessage.tsx +102 -0
  42. package/src/components/MultiViewportPreview.tsx +527 -0
  43. package/src/components/NoVariantsMessage.tsx +59 -0
  44. package/src/components/PanelShell.tsx +161 -0
  45. package/src/components/PerformancePanel.tsx +304 -0
  46. package/src/components/PreviewArea.tsx +254 -0
  47. package/src/components/PreviewAside.tsx +168 -0
  48. package/src/components/PreviewFrameHost.tsx +304 -0
  49. package/src/components/PreviewToolbar.tsx +80 -0
  50. package/src/components/PropsEditor.tsx +506 -0
  51. package/src/components/PropsTable.tsx +111 -0
  52. package/src/components/RelationsSection.tsx +88 -0
  53. package/src/components/ResizablePanel.tsx +271 -0
  54. package/src/components/RightSidebar.tsx +102 -0
  55. package/src/components/RuntimeToolsRegistrar.tsx +17 -0
  56. package/src/components/ScreenshotButton.tsx +90 -0
  57. package/src/components/ShadowPreview.tsx +204 -0
  58. package/src/components/Sidebar.tsx +169 -0
  59. package/src/components/SkeletonLoader.tsx +161 -0
  60. package/src/components/ThemeProvider.tsx +42 -0
  61. package/src/components/Toast.tsx +3 -0
  62. package/src/components/TokenStylePanel.tsx +699 -0
  63. package/src/components/TopToolbar.tsx +159 -0
  64. package/src/components/Untitled +1 -0
  65. package/src/components/UsageSection.tsx +95 -0
  66. package/src/components/VariantMatrix.tsx +391 -0
  67. package/src/components/VariantRenderer.tsx +131 -0
  68. package/src/components/VariantTabs.tsx +40 -0
  69. package/src/components/ViewerHeader.tsx +69 -0
  70. package/src/components/ViewerStateSync.tsx +52 -0
  71. package/src/components/ViewportSelector.tsx +172 -0
  72. package/src/components/WebMCPDevTools.tsx +503 -0
  73. package/src/components/WebMCPIntegration.tsx +47 -0
  74. package/src/components/WebMCPStatusIndicator.tsx +60 -0
  75. package/src/components/_future/CreatePage.tsx +835 -0
  76. package/src/components/viewer-utils.ts +16 -0
  77. package/src/composition-renderer.ts +381 -0
  78. package/src/constants/index.ts +1 -0
  79. package/src/constants/ui.ts +166 -0
  80. package/src/entry.tsx +335 -0
  81. package/src/hooks/index.ts +2 -0
  82. package/src/hooks/useA11yCache.ts +383 -0
  83. package/src/hooks/useA11yService.ts +364 -0
  84. package/src/hooks/useActions.ts +138 -0
  85. package/src/hooks/useAppState.ts +147 -0
  86. package/src/hooks/useCompiledFragments.ts +42 -0
  87. package/src/hooks/useFigmaIntegration.ts +132 -0
  88. package/src/hooks/useHmrStatus.ts +109 -0
  89. package/src/hooks/useKeyboardShortcuts.ts +270 -0
  90. package/src/hooks/usePreviewBridge.ts +347 -0
  91. package/src/hooks/useScrollSpy.ts +78 -0
  92. package/src/hooks/useShadowStyles.ts +221 -0
  93. package/src/hooks/useUrlState.ts +318 -0
  94. package/src/hooks/useViewSettings.ts +111 -0
  95. package/src/intelligence/healthReport.ts +505 -0
  96. package/src/intelligence/styleDrift.ts +340 -0
  97. package/src/intelligence/usageScanner.ts +309 -0
  98. package/src/jsx-parser.ts +486 -0
  99. package/src/preview-frame-entry.tsx +25 -0
  100. package/src/preview-frame.html +148 -0
  101. package/src/render-template.html +68 -0
  102. package/src/render-utils.ts +311 -0
  103. package/src/shared/ComponentDocContent.module.scss +10 -0
  104. package/src/shared/ComponentDocContent.module.scss.d.ts +2 -0
  105. package/src/shared/ComponentDocContent.tsx +274 -0
  106. package/src/shared/DocsHeaderBar.tsx +129 -0
  107. package/src/shared/DocsPageAsideHost.tsx +89 -0
  108. package/src/shared/DocsPageShell.tsx +124 -0
  109. package/src/shared/DocsSearchCommand.tsx +99 -0
  110. package/src/shared/DocsSidebarNav.tsx +66 -0
  111. package/src/shared/PropsTable.module.scss +68 -0
  112. package/src/shared/PropsTable.module.scss.d.ts +2 -0
  113. package/src/shared/PropsTable.tsx +76 -0
  114. package/src/shared/VariantPreviewCard.module.scss +114 -0
  115. package/src/shared/VariantPreviewCard.module.scss.d.ts +2 -0
  116. package/src/shared/VariantPreviewCard.tsx +137 -0
  117. package/src/shared/docs-data/index.ts +32 -0
  118. package/src/shared/docs-data/mcp-configs.ts +72 -0
  119. package/src/shared/docs-data/palettes.ts +75 -0
  120. package/src/shared/docs-data/setup-examples.ts +55 -0
  121. package/src/shared/docs-layout.scss +28 -0
  122. package/src/shared/docs-layout.scss.d.ts +2 -0
  123. package/src/shared/index.ts +34 -0
  124. package/src/shared/types.ts +53 -0
  125. package/src/style-utils.ts +414 -0
  126. package/src/styles/globals.css +278 -0
  127. package/src/types/a11y.ts +197 -0
  128. package/src/utils/a11y-fixes.ts +509 -0
  129. package/src/utils/actionExport.ts +372 -0
  130. package/src/utils/colorSchemes.ts +201 -0
  131. package/src/utils/contrast.ts +246 -0
  132. package/src/utils/detectRelationships.ts +256 -0
  133. package/src/webmcp/__tests__/analytics.test.ts +108 -0
  134. package/src/webmcp/analytics.ts +165 -0
  135. package/src/webmcp/index.ts +3 -0
  136. package/src/webmcp/posthog-bridge.ts +39 -0
  137. package/src/webmcp/runtime-tools.ts +152 -0
  138. package/src/webmcp/scan-utils.ts +135 -0
  139. package/src/webmcp/use-tool-analytics.ts +69 -0
  140. package/src/webmcp/viewer-state.ts +45 -0
  141. package/tsconfig.json +20 -0
@@ -0,0 +1,835 @@
1
+ import { useForm, Controller } from 'react-hook-form';
2
+ import { zodResolver } from '@hookform/resolvers/zod';
3
+ import { z } from 'zod';
4
+ import { useState, useRef, useEffect, useMemo } from 'react';
5
+ import clsx from 'clsx';
6
+ import { HexColorPicker } from 'react-colorful';
7
+ import { generateColorScheme, type ColorSchemeType, getContrastColor } from '../../utils/colorSchemes.js';
8
+
9
+ const formSchema = z.object({
10
+ projectName: z.string().min(2, 'Project name must be at least 2 characters'),
11
+ headlessLibrary: z.enum(['radix', 'base-ui']),
12
+ colorSchemeType: z.enum(['monochromatic', 'complementary', 'analogous', 'triadic', 'split-complementary', 'tetradic']),
13
+ primaryColor: z.string().regex(/^#[0-9A-Fa-f]{6}$/, 'Must be a valid hex color'),
14
+ darkMode: z.boolean(),
15
+ borderRadius: z.enum(['none', 'sm', 'md', 'lg', 'full']),
16
+ });
17
+
18
+ export type CreateFormData = z.infer<typeof formSchema>;
19
+
20
+ // Scheme options with color counts
21
+ const SCHEME_OPTIONS: Array<{
22
+ value: ColorSchemeType;
23
+ label: string;
24
+ description: string;
25
+ colorCount: number;
26
+ }> = [
27
+ { value: 'monochromatic', label: 'Monochromatic', description: 'Variations of a single hue', colorCount: 5 },
28
+ { value: 'complementary', label: 'Complementary', description: 'Two opposite colors', colorCount: 2 },
29
+ { value: 'analogous', label: 'Analogous', description: 'Three adjacent colors', colorCount: 3 },
30
+ { value: 'triadic', label: 'Triadic', description: 'Three evenly spaced colors', colorCount: 3 },
31
+ { value: 'split-complementary', label: 'Split Complementary', description: 'Base + two adjacent to complement', colorCount: 3 },
32
+ { value: 'tetradic', label: 'Tetradic (Square)', description: 'Four evenly spaced colors', colorCount: 4 },
33
+ ];
34
+
35
+ // Border radius values
36
+ const RADIUS_VALUES: Record<string, string> = {
37
+ none: '0px',
38
+ sm: '4px',
39
+ md: '8px',
40
+ lg: '12px',
41
+ full: '9999px',
42
+ };
43
+
44
+ // Get the key colors for a scheme
45
+ function getSchemeKeyColors(baseColor: string, type: ColorSchemeType): string[] {
46
+ const fullPalette = generateColorScheme(baseColor, type);
47
+
48
+ switch (type) {
49
+ case 'monochromatic':
50
+ return fullPalette;
51
+ case 'complementary':
52
+ return [fullPalette[1], fullPalette[3]];
53
+ case 'analogous':
54
+ return [fullPalette[0], fullPalette[2], fullPalette[4]];
55
+ case 'triadic':
56
+ return [fullPalette[0], fullPalette[2], fullPalette[4]];
57
+ case 'split-complementary':
58
+ return [fullPalette[0], fullPalette[2], fullPalette[4]];
59
+ case 'tetradic':
60
+ return [fullPalette[0], fullPalette[1], fullPalette[2], fullPalette[3]];
61
+ default:
62
+ return fullPalette;
63
+ }
64
+ }
65
+
66
+ export function CreatePage() {
67
+ const [showColorPicker, setShowColorPicker] = useState(false);
68
+ const [showSchemeDropdown, setShowSchemeDropdown] = useState(false);
69
+ const [showExportModal, setShowExportModal] = useState(false);
70
+ const [previewDarkMode, setPreviewDarkMode] = useState(false);
71
+ const colorPickerRef = useRef<HTMLDivElement>(null);
72
+ const schemeDropdownRef = useRef<HTMLDivElement>(null);
73
+
74
+ const navigateBack = () => {
75
+ window.location.hash = '/';
76
+ };
77
+
78
+ const {
79
+ register,
80
+ handleSubmit,
81
+ watch,
82
+ control,
83
+ setValue,
84
+ formState: { errors },
85
+ } = useForm<CreateFormData>({
86
+ resolver: zodResolver(formSchema),
87
+ defaultValues: {
88
+ projectName: 'my-design-system',
89
+ headlessLibrary: 'radix',
90
+ colorSchemeType: 'monochromatic',
91
+ primaryColor: '#10a37f',
92
+ darkMode: true,
93
+ borderRadius: 'md',
94
+ },
95
+ });
96
+
97
+ const formValues = watch();
98
+ const { primaryColor, colorSchemeType, borderRadius, darkMode } = formValues;
99
+
100
+ // Generate current palette
101
+ const currentPalette = useMemo(
102
+ () => getSchemeKeyColors(primaryColor, colorSchemeType),
103
+ [primaryColor, colorSchemeType]
104
+ );
105
+ const currentScheme = SCHEME_OPTIONS.find(s => s.value === colorSchemeType);
106
+
107
+ // Generate CSS variables based on form values
108
+ const cssVariables = useMemo(() => {
109
+ const palette = generateColorScheme(primaryColor, colorSchemeType);
110
+ return {
111
+ '--color-primary': palette[0] || primaryColor,
112
+ '--color-primary-hover': palette[1] || primaryColor,
113
+ '--color-secondary': palette[2] || '#6b7280',
114
+ '--color-accent': primaryColor,
115
+ '--color-accent-hover': palette[1] || primaryColor,
116
+ '--radius': RADIUS_VALUES[borderRadius],
117
+ '--radius-sm': borderRadius === 'none' ? '0px' : '4px',
118
+ '--radius-md': borderRadius === 'none' ? '0px' : '8px',
119
+ '--radius-lg': borderRadius === 'none' ? '0px' : '12px',
120
+ };
121
+ }, [primaryColor, colorSchemeType, borderRadius]);
122
+
123
+ // Close dropdowns when clicking outside
124
+ useEffect(() => {
125
+ const handleClickOutside = (event: MouseEvent) => {
126
+ if (colorPickerRef.current && !colorPickerRef.current.contains(event.target as Node)) {
127
+ setShowColorPicker(false);
128
+ }
129
+ if (schemeDropdownRef.current && !schemeDropdownRef.current.contains(event.target as Node)) {
130
+ setShowSchemeDropdown(false);
131
+ }
132
+ };
133
+ document.addEventListener('mousedown', handleClickOutside);
134
+ return () => document.removeEventListener('mousedown', handleClickOutside);
135
+ }, []);
136
+
137
+ const handleFormSubmit = (_data: CreateFormData) => {
138
+ setShowExportModal(true);
139
+ };
140
+
141
+ const generateExportConfig = () => {
142
+ const config = {
143
+ name: formValues.projectName,
144
+ headlessLibrary: formValues.headlessLibrary,
145
+ theme: {
146
+ colors: {
147
+ primary: primaryColor,
148
+ scheme: colorSchemeType,
149
+ palette: currentPalette,
150
+ },
151
+ borderRadius: borderRadius,
152
+ darkMode: darkMode,
153
+ },
154
+ };
155
+ return JSON.stringify(config, null, 2);
156
+ };
157
+
158
+ const generateCssVariables = () => {
159
+ const palette = generateColorScheme(primaryColor, colorSchemeType);
160
+ let css = `/* ${formValues.projectName} - Generated by Fragments */
161
+ :root {
162
+ /* Primary Colors */
163
+ --color-primary: ${palette[0]};
164
+ --color-primary-foreground: ${getContrastColor(palette[0])};
165
+ --color-secondary: ${palette[2] || palette[1]};
166
+ --color-secondary-foreground: ${getContrastColor(palette[2] || palette[1])};
167
+ --color-accent: ${primaryColor};
168
+ --color-accent-foreground: ${getContrastColor(primaryColor)};
169
+
170
+ /* Palette */
171
+ ${currentPalette.map((c, i) => ` --palette-${i + 1}: ${c};`).join('\n')}
172
+
173
+ /* Border Radius */
174
+ --radius: ${RADIUS_VALUES[borderRadius]};
175
+ --radius-sm: ${borderRadius === 'none' ? '0' : '0.25rem'};
176
+ --radius-md: ${borderRadius === 'none' ? '0' : '0.5rem'};
177
+ --radius-lg: ${borderRadius === 'none' ? '0' : '0.75rem'};
178
+ --radius-full: 9999px;
179
+
180
+ /* Light Mode Surfaces */
181
+ --bg-primary: #ffffff;
182
+ --bg-secondary: #f9fafb;
183
+ --bg-tertiary: #f3f4f6;
184
+ --text-primary: #111827;
185
+ --text-secondary: #4b5563;
186
+ --text-tertiary: #9ca3af;
187
+ --border: #e5e7eb;
188
+ --border-strong: #d1d5db;
189
+ }`;
190
+
191
+ if (darkMode) {
192
+ css += `
193
+
194
+ .dark {
195
+ /* Dark Mode Surfaces */
196
+ --bg-primary: #111827;
197
+ --bg-secondary: #1f2937;
198
+ --bg-tertiary: #374151;
199
+ --text-primary: #f9fafb;
200
+ --text-secondary: #d1d5db;
201
+ --text-tertiary: #9ca3af;
202
+ --border: #374151;
203
+ --border-strong: #4b5563;
204
+ }`;
205
+ }
206
+
207
+ return css;
208
+ };
209
+
210
+ const generateTailwindConfig = () => {
211
+ return `// tailwind.config.js - Generated by Fragments
212
+ /** @type {import('tailwindcss').Config} */
213
+ export default {
214
+ darkMode: 'class',
215
+ content: ['./src/**/*.{js,ts,jsx,tsx}'],
216
+ theme: {
217
+ extend: {
218
+ colors: {
219
+ primary: {
220
+ DEFAULT: '${primaryColor}',
221
+ foreground: '${getContrastColor(primaryColor)}',
222
+ },
223
+ secondary: {
224
+ DEFAULT: '${currentPalette[1] || primaryColor}',
225
+ foreground: '${getContrastColor(currentPalette[1] || primaryColor)}',
226
+ },
227
+ accent: {
228
+ DEFAULT: '${currentPalette[2] || primaryColor}',
229
+ foreground: '${getContrastColor(currentPalette[2] || primaryColor)}',
230
+ },
231
+ // Full palette
232
+ ${currentPalette.map((c, i) => ` palette${i + 1}: '${c}',`).join('\n')}
233
+ },
234
+ borderRadius: {
235
+ DEFAULT: '${RADIUS_VALUES[borderRadius]}',
236
+ },
237
+ },
238
+ },
239
+ plugins: [],
240
+ };`;
241
+ };
242
+
243
+ return (
244
+ <div className="min-h-screen bg-[--bg-primary] flex">
245
+ {/* Main Form Section */}
246
+ <div className="flex-1 overflow-y-auto">
247
+ <div className="max-w-2xl mx-auto px-6 py-16">
248
+ {/* Header */}
249
+ <div className="mb-12">
250
+ <button
251
+ onClick={navigateBack}
252
+ className="flex items-center gap-1.5 text-sm text-tertiary hover:text-primary mb-4 -ml-1 transition-colors"
253
+ >
254
+ <BackIcon className="w-4 h-4" />
255
+ Back to viewer
256
+ </button>
257
+ <h1 className="text-2xl font-semibold text-primary mb-2">
258
+ Create Your Design System
259
+ </h1>
260
+ <p className="text-secondary">
261
+ Configure your components and export to use in your project.
262
+ </p>
263
+ </div>
264
+
265
+ <form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-8">
266
+ {/* Project Name */}
267
+ <div>
268
+ <label className="block text-sm text-primary mb-2">
269
+ Project Name
270
+ </label>
271
+ <input
272
+ {...register('projectName')}
273
+ placeholder="my-design-system"
274
+ className={clsx(
275
+ 'w-full px-4 py-3 rounded-xl text-sm',
276
+ 'bg-[--bg-tertiary] text-primary placeholder:text-tertiary',
277
+ 'border border-transparent',
278
+ 'focus:outline-none focus:ring-1 focus:ring-[--border-strong]',
279
+ 'transition-all',
280
+ errors.projectName && 'ring-1 ring-red-500'
281
+ )}
282
+ />
283
+ </div>
284
+
285
+ <div className="h-px bg-[--border]" />
286
+
287
+ {/* Headless Library */}
288
+ <div>
289
+ <label className="block text-sm text-primary mb-4">
290
+ Headless Library
291
+ </label>
292
+ <div className="grid grid-cols-2 gap-3">
293
+ {[
294
+ { value: 'radix', label: 'Radix UI', desc: 'Accessible primitives' },
295
+ { value: 'base-ui', label: 'Base UI', desc: 'Hooks-first approach' },
296
+ ].map((lib) => (
297
+ <label
298
+ key={lib.value}
299
+ className={clsx(
300
+ 'flex flex-col p-4 rounded-xl cursor-pointer transition-all',
301
+ 'border',
302
+ watch('headlessLibrary') === lib.value
303
+ ? 'border-[--color-accent] bg-[--color-accent]/5'
304
+ : 'border-[--border] hover:border-[--border-strong]'
305
+ )}
306
+ >
307
+ <input
308
+ type="radio"
309
+ {...register('headlessLibrary')}
310
+ value={lib.value}
311
+ className="sr-only"
312
+ />
313
+ <span className="text-sm font-medium text-primary">{lib.label}</span>
314
+ <span className="text-xs text-tertiary mt-0.5">{lib.desc}</span>
315
+ </label>
316
+ ))}
317
+ </div>
318
+ </div>
319
+
320
+ <div className="h-px bg-[--border]" />
321
+
322
+ {/* Color Scheme */}
323
+ <div>
324
+ <label className="block text-sm text-primary mb-4">
325
+ Color Scheme
326
+ </label>
327
+
328
+ <div className="space-y-4">
329
+ {/* Primary Color */}
330
+ <div className="flex items-center gap-4">
331
+ <div className="relative" ref={colorPickerRef}>
332
+ <button
333
+ type="button"
334
+ onClick={() => setShowColorPicker(!showColorPicker)}
335
+ className="w-12 h-12 rounded-lg border border-[--border] hover:border-[--border-strong] transition-colors"
336
+ style={{ backgroundColor: primaryColor }}
337
+ />
338
+ {showColorPicker && (
339
+ <div className="absolute top-14 left-0 z-50 p-3 rounded-xl bg-[--bg-elevated] border border-[--border] shadow-xl">
340
+ <Controller
341
+ name="primaryColor"
342
+ control={control}
343
+ render={({ field }) => (
344
+ <HexColorPicker color={field.value} onChange={field.onChange} />
345
+ )}
346
+ />
347
+ <input
348
+ type="text"
349
+ value={primaryColor}
350
+ onChange={(e) => setValue('primaryColor', e.target.value)}
351
+ className="w-full mt-3 px-3 py-2 rounded-lg text-xs font-mono bg-[--bg-tertiary] text-primary border border-transparent focus:outline-none focus:ring-1 focus:ring-[--border-strong]"
352
+ />
353
+ </div>
354
+ )}
355
+ </div>
356
+ <div className="flex-1">
357
+ <input
358
+ type="text"
359
+ value={primaryColor}
360
+ onChange={(e) => setValue('primaryColor', e.target.value)}
361
+ className="w-full px-4 py-3 rounded-xl text-sm font-mono bg-[--bg-tertiary] text-primary border border-transparent focus:outline-none focus:ring-1 focus:ring-[--border-strong]"
362
+ />
363
+ </div>
364
+ </div>
365
+
366
+ {/* Custom Scheme Type Dropdown */}
367
+ <div className="relative" ref={schemeDropdownRef}>
368
+ <button
369
+ type="button"
370
+ onClick={() => setShowSchemeDropdown(!showSchemeDropdown)}
371
+ className={clsx(
372
+ 'w-full px-4 py-3 rounded-xl text-sm text-left',
373
+ 'bg-[--bg-tertiary] text-primary',
374
+ 'border border-transparent',
375
+ 'focus:outline-none focus:ring-1 focus:ring-[--border-strong]',
376
+ 'flex items-center justify-between',
377
+ showSchemeDropdown && 'ring-1 ring-[--border-strong]'
378
+ )}
379
+ >
380
+ <div className="flex items-center gap-3">
381
+ <div className="flex gap-0.5">
382
+ {getSchemeKeyColors(primaryColor, colorSchemeType).map((color, i) => (
383
+ <div
384
+ key={i}
385
+ className="w-4 h-4 rounded-sm first:rounded-l last:rounded-r"
386
+ style={{ backgroundColor: color }}
387
+ />
388
+ ))}
389
+ </div>
390
+ <span>{currentScheme?.label}</span>
391
+ <span className="text-tertiary">— {currentScheme?.description}</span>
392
+ </div>
393
+ <ChevronIcon className={clsx('w-4 h-4 text-tertiary transition-transform', showSchemeDropdown && 'rotate-180')} />
394
+ </button>
395
+
396
+ {showSchemeDropdown && (
397
+ <div className="absolute top-full left-0 right-0 mt-2 z-40 py-2 rounded-xl bg-[--bg-elevated] border border-[--border] shadow-xl max-h-80 overflow-y-auto">
398
+ {SCHEME_OPTIONS.map((scheme) => {
399
+ const schemeColors = getSchemeKeyColors(primaryColor, scheme.value);
400
+ const isSelected = colorSchemeType === scheme.value;
401
+
402
+ return (
403
+ <button
404
+ key={scheme.value}
405
+ type="button"
406
+ onClick={() => {
407
+ setValue('colorSchemeType', scheme.value);
408
+ setShowSchemeDropdown(false);
409
+ }}
410
+ className={clsx(
411
+ 'w-full px-4 py-3 text-left flex items-center gap-3',
412
+ 'hover:bg-[--bg-hover] transition-colors',
413
+ isSelected && 'bg-[--bg-tertiary]'
414
+ )}
415
+ >
416
+ <div className="flex gap-0.5 shrink-0">
417
+ {schemeColors.map((color, i) => (
418
+ <div
419
+ key={i}
420
+ className="w-5 h-5 rounded-sm first:rounded-l last:rounded-r"
421
+ style={{ backgroundColor: color }}
422
+ />
423
+ ))}
424
+ </div>
425
+ <div className="flex-1 min-w-0">
426
+ <div className="flex items-center gap-2">
427
+ {isSelected && <span className="text-[--color-accent]">✓</span>}
428
+ <span className={clsx('text-sm', isSelected ? 'text-primary font-medium' : 'text-primary')}>
429
+ {scheme.label}
430
+ </span>
431
+ </div>
432
+ <span className="text-xs text-tertiary">{scheme.description}</span>
433
+ </div>
434
+ </button>
435
+ );
436
+ })}
437
+ </div>
438
+ )}
439
+ </div>
440
+
441
+ {/* Current Palette Preview */}
442
+ <div>
443
+ <div className="flex gap-1.5">
444
+ {currentPalette.map((color, i) => (
445
+ <div
446
+ key={i}
447
+ className="flex-1 h-12 rounded-lg first:rounded-l-xl last:rounded-r-xl transition-colors"
448
+ style={{ backgroundColor: color }}
449
+ />
450
+ ))}
451
+ </div>
452
+ <p className="text-xs text-tertiary mt-2">
453
+ {currentScheme?.colorCount} color {currentScheme?.label.toLowerCase()} palette
454
+ </p>
455
+ </div>
456
+ </div>
457
+ </div>
458
+
459
+ <div className="h-px bg-[--border]" />
460
+
461
+ {/* Style Options */}
462
+ <div className="space-y-6">
463
+ <div className="flex items-center justify-between">
464
+ <div>
465
+ <span className="text-sm text-primary">Border Radius</span>
466
+ <p className="text-xs text-tertiary mt-0.5">Component corner style</p>
467
+ </div>
468
+ <div className="flex gap-1 bg-[--bg-tertiary] p-1 rounded-lg">
469
+ {(['none', 'sm', 'md', 'lg', 'full'] as const).map((radius) => (
470
+ <label
471
+ key={radius}
472
+ className={clsx(
473
+ 'px-3 py-1.5 text-xs font-medium rounded-md cursor-pointer transition-all',
474
+ watch('borderRadius') === radius
475
+ ? 'bg-[--bg-primary] text-primary shadow-sm'
476
+ : 'text-tertiary hover:text-secondary'
477
+ )}
478
+ >
479
+ <input
480
+ type="radio"
481
+ {...register('borderRadius')}
482
+ value={radius}
483
+ className="sr-only"
484
+ />
485
+ {radius}
486
+ </label>
487
+ ))}
488
+ </div>
489
+ </div>
490
+
491
+ <div className="flex items-center justify-between">
492
+ <div>
493
+ <span className="text-sm text-primary">Dark Mode</span>
494
+ <p className="text-xs text-tertiary mt-0.5">Include dark theme styles</p>
495
+ </div>
496
+ <label className="relative cursor-pointer">
497
+ <input
498
+ type="checkbox"
499
+ {...register('darkMode')}
500
+ className="sr-only peer"
501
+ />
502
+ <div className="w-11 h-6 bg-[--bg-tertiary] rounded-full peer-checked:bg-[--color-accent] transition-colors" />
503
+ <div className="absolute left-0.5 top-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform peer-checked:translate-x-5" />
504
+ </label>
505
+ </div>
506
+ </div>
507
+
508
+ <div className="h-px bg-[--border]" />
509
+
510
+ {/* Submit */}
511
+ <button
512
+ type="submit"
513
+ className={clsx(
514
+ 'w-full py-3 px-6 rounded-xl text-sm font-medium transition-all',
515
+ 'bg-[--color-accent] text-white',
516
+ 'hover:brightness-110',
517
+ 'focus:outline-none focus:ring-2 focus:ring-[--color-accent] focus:ring-offset-2 focus:ring-offset-[--bg-primary]'
518
+ )}
519
+ >
520
+ Export Design System
521
+ </button>
522
+ </form>
523
+ </div>
524
+ </div>
525
+
526
+ {/* Preview Sidebar */}
527
+ <div className="hidden lg:block w-96 border-l border-[--border] bg-[--bg-secondary] overflow-y-auto">
528
+ <div className="sticky top-0 p-6">
529
+ <div className="flex items-center justify-between mb-6">
530
+ <h2 className="text-sm font-medium text-primary">Live Preview</h2>
531
+ <button
532
+ onClick={() => setPreviewDarkMode(!previewDarkMode)}
533
+ className={clsx(
534
+ 'flex items-center gap-2 px-3 py-1.5 text-xs font-medium rounded-lg transition-all',
535
+ previewDarkMode
536
+ ? 'bg-gray-800 text-white'
537
+ : 'bg-white text-gray-800 border border-gray-200'
538
+ )}
539
+ >
540
+ {previewDarkMode ? <MoonIcon className="w-3.5 h-3.5" /> : <SunIcon className="w-3.5 h-3.5" />}
541
+ {previewDarkMode ? 'Dark' : 'Light'}
542
+ </button>
543
+ </div>
544
+
545
+ {/* Preview Container with custom CSS vars */}
546
+ <div
547
+ className={clsx(
548
+ 'space-y-8 p-4 rounded-xl transition-colors',
549
+ previewDarkMode ? 'bg-gray-900' : 'bg-white border border-gray-200'
550
+ )}
551
+ style={cssVariables as React.CSSProperties}
552
+ >
553
+ {/* Button Variants */}
554
+ <div>
555
+ <h3 className={clsx('text-xs font-medium uppercase tracking-wider mb-3', previewDarkMode ? 'text-gray-400' : 'text-gray-500')}>Buttons</h3>
556
+ <div className="space-y-3">
557
+ <button
558
+ className="w-full px-4 py-2.5 text-sm font-medium transition-all"
559
+ style={{
560
+ backgroundColor: primaryColor,
561
+ color: getContrastColor(primaryColor),
562
+ borderRadius: RADIUS_VALUES[borderRadius],
563
+ }}
564
+ >
565
+ Primary Button
566
+ </button>
567
+ <button
568
+ className="w-full px-4 py-2.5 text-sm font-medium border transition-all"
569
+ style={{
570
+ backgroundColor: 'transparent',
571
+ borderColor: primaryColor,
572
+ color: primaryColor,
573
+ borderRadius: RADIUS_VALUES[borderRadius],
574
+ }}
575
+ >
576
+ Secondary Button
577
+ </button>
578
+ <button
579
+ className="w-full px-4 py-2.5 text-sm font-medium transition-all"
580
+ style={{
581
+ backgroundColor: 'transparent',
582
+ color: primaryColor,
583
+ borderRadius: RADIUS_VALUES[borderRadius],
584
+ }}
585
+ >
586
+ Ghost Button
587
+ </button>
588
+ <button
589
+ className="w-full px-4 py-2.5 text-sm font-medium transition-all"
590
+ style={{
591
+ backgroundColor: '#ef4444',
592
+ color: '#ffffff',
593
+ borderRadius: RADIUS_VALUES[borderRadius],
594
+ }}
595
+ >
596
+ Danger Button
597
+ </button>
598
+ </div>
599
+ </div>
600
+
601
+ {/* Input */}
602
+ <div>
603
+ <h3 className={clsx('text-xs font-medium uppercase tracking-wider mb-3', previewDarkMode ? 'text-gray-400' : 'text-gray-500')}>Input</h3>
604
+ <input
605
+ type="text"
606
+ placeholder="Enter your email"
607
+ className={clsx(
608
+ 'w-full px-4 py-2.5 text-sm border focus:outline-none transition-all',
609
+ previewDarkMode
610
+ ? 'bg-gray-800 text-white placeholder:text-gray-500 border-gray-700'
611
+ : 'bg-gray-50 text-gray-900 placeholder:text-gray-400 border-gray-200'
612
+ )}
613
+ style={{
614
+ borderRadius: RADIUS_VALUES[borderRadius],
615
+ }}
616
+ onFocus={(e) => {
617
+ e.target.style.borderColor = primaryColor;
618
+ }}
619
+ onBlur={(e) => {
620
+ e.target.style.borderColor = previewDarkMode ? '#374151' : '#e5e7eb';
621
+ }}
622
+ />
623
+ </div>
624
+
625
+ {/* Card */}
626
+ <div>
627
+ <h3 className={clsx('text-xs font-medium uppercase tracking-wider mb-3', previewDarkMode ? 'text-gray-400' : 'text-gray-500')}>Card</h3>
628
+ <div
629
+ className={clsx(
630
+ 'p-4 border',
631
+ previewDarkMode ? 'bg-gray-800 border-gray-700' : 'bg-gray-50 border-gray-200'
632
+ )}
633
+ style={{ borderRadius: RADIUS_VALUES[borderRadius] }}
634
+ >
635
+ <h4 className={clsx('text-sm font-medium mb-1', previewDarkMode ? 'text-white' : 'text-gray-900')}>Card Title</h4>
636
+ <p className={clsx('text-xs mb-3', previewDarkMode ? 'text-gray-400' : 'text-gray-500')}>This is a sample card component with your selected styles.</p>
637
+ <button
638
+ className="px-3 py-1.5 text-xs font-medium transition-all"
639
+ style={{
640
+ backgroundColor: primaryColor,
641
+ color: getContrastColor(primaryColor),
642
+ borderRadius: RADIUS_VALUES[borderRadius],
643
+ }}
644
+ >
645
+ Action
646
+ </button>
647
+ </div>
648
+ </div>
649
+
650
+ {/* Badge */}
651
+ <div>
652
+ <h3 className={clsx('text-xs font-medium uppercase tracking-wider mb-3', previewDarkMode ? 'text-gray-400' : 'text-gray-500')}>Badges</h3>
653
+ <div className="flex flex-wrap gap-2">
654
+ {currentPalette.slice(0, 4).map((color, i) => (
655
+ <span
656
+ key={i}
657
+ className="px-2.5 py-1 text-xs font-medium"
658
+ style={{
659
+ backgroundColor: color + '20',
660
+ color: color,
661
+ borderRadius: borderRadius === 'full' ? '9999px' : RADIUS_VALUES['sm'],
662
+ }}
663
+ >
664
+ Badge {i + 1}
665
+ </span>
666
+ ))}
667
+ </div>
668
+ </div>
669
+
670
+ {/* Color Palette */}
671
+ <div>
672
+ <h3 className={clsx('text-xs font-medium uppercase tracking-wider mb-3', previewDarkMode ? 'text-gray-400' : 'text-gray-500')}>Your Palette</h3>
673
+ <div className="flex gap-2">
674
+ {currentPalette.map((color, i) => (
675
+ <div key={i} className="flex-1 text-center">
676
+ <div
677
+ className="aspect-square rounded-lg mb-1"
678
+ style={{ backgroundColor: color }}
679
+ />
680
+ <span className={clsx('text-[10px] font-mono', previewDarkMode ? 'text-gray-500' : 'text-gray-400')}>{color.slice(1, 7)}</span>
681
+ </div>
682
+ ))}
683
+ </div>
684
+ </div>
685
+ </div>
686
+ </div>
687
+ </div>
688
+
689
+ {/* Export Modal */}
690
+ {showExportModal && (
691
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
692
+ <div className="w-full max-w-2xl max-h-[80vh] overflow-hidden rounded-2xl bg-[--bg-elevated] border border-[--border] shadow-2xl">
693
+ <div className="flex items-center justify-between p-4 border-b border-[--border]">
694
+ <h2 className="text-lg font-semibold text-primary">Export Design System</h2>
695
+ <button
696
+ onClick={() => setShowExportModal(false)}
697
+ className="p-2 rounded-lg hover:bg-[--bg-hover] text-tertiary hover:text-primary transition-colors"
698
+ >
699
+ <CloseIcon className="w-5 h-5" />
700
+ </button>
701
+ </div>
702
+
703
+ <div className="p-4 overflow-y-auto max-h-[60vh] space-y-6">
704
+ {/* CLI Command */}
705
+ <div>
706
+ <h3 className="text-sm font-medium text-primary mb-2">Quick Start</h3>
707
+ <div className="flex items-center gap-2 p-3 rounded-lg bg-[--bg-tertiary] font-mono text-sm">
708
+ <span className="text-tertiary">$</span>
709
+ <code className="text-primary flex-1">npx fragment-ui init {formValues.projectName}</code>
710
+ <button
711
+ onClick={() => navigator.clipboard.writeText(`npx fragment-ui init ${formValues.projectName}`)}
712
+ className="px-2 py-1 text-xs rounded bg-[--bg-hover] text-secondary hover:text-primary transition-colors"
713
+ >
714
+ Copy
715
+ </button>
716
+ </div>
717
+ </div>
718
+
719
+ {/* CSS Variables */}
720
+ <div>
721
+ <div className="flex items-center justify-between mb-2">
722
+ <h3 className="text-sm font-medium text-primary">CSS Variables</h3>
723
+ <button
724
+ onClick={() => navigator.clipboard.writeText(generateCssVariables())}
725
+ className="px-2 py-1 text-xs rounded bg-[--bg-hover] text-secondary hover:text-primary transition-colors"
726
+ >
727
+ Copy
728
+ </button>
729
+ </div>
730
+ <pre className="p-4 rounded-lg bg-[--bg-tertiary] text-xs font-mono text-secondary overflow-x-auto whitespace-pre">
731
+ {generateCssVariables()}
732
+ </pre>
733
+ </div>
734
+
735
+ {/* Tailwind Config */}
736
+ <div>
737
+ <div className="flex items-center justify-between mb-2">
738
+ <h3 className="text-sm font-medium text-primary">tailwind.config.js</h3>
739
+ <button
740
+ onClick={() => navigator.clipboard.writeText(generateTailwindConfig())}
741
+ className="px-2 py-1 text-xs rounded bg-[--bg-hover] text-secondary hover:text-primary transition-colors"
742
+ >
743
+ Copy
744
+ </button>
745
+ </div>
746
+ <pre className="p-4 rounded-lg bg-[--bg-tertiary] text-xs font-mono text-secondary overflow-x-auto whitespace-pre">
747
+ {generateTailwindConfig()}
748
+ </pre>
749
+ </div>
750
+
751
+ {/* Config JSON */}
752
+ <div>
753
+ <div className="flex items-center justify-between mb-2">
754
+ <h3 className="text-sm font-medium text-primary">fragment.config.json</h3>
755
+ <button
756
+ onClick={() => navigator.clipboard.writeText(generateExportConfig())}
757
+ className="px-2 py-1 text-xs rounded bg-[--bg-hover] text-secondary hover:text-primary transition-colors"
758
+ >
759
+ Copy
760
+ </button>
761
+ </div>
762
+ <pre className="p-4 rounded-lg bg-[--bg-tertiary] text-xs font-mono text-secondary overflow-x-auto whitespace-pre">
763
+ {generateExportConfig()}
764
+ </pre>
765
+ </div>
766
+ </div>
767
+
768
+ <div className="p-4 border-t border-[--border] flex justify-end gap-3">
769
+ <button
770
+ onClick={() => setShowExportModal(false)}
771
+ className="px-4 py-2 text-sm font-medium rounded-lg border border-[--border] text-secondary hover:text-primary transition-colors"
772
+ >
773
+ Close
774
+ </button>
775
+ <button
776
+ onClick={() => {
777
+ const blob = new Blob([generateExportConfig()], { type: 'application/json' });
778
+ const url = URL.createObjectURL(blob);
779
+ const a = document.createElement('a');
780
+ a.href = url;
781
+ a.download = 'fragment.config.json';
782
+ a.click();
783
+ }}
784
+ className="px-4 py-2 text-sm font-medium rounded-lg text-white transition-colors"
785
+ style={{ backgroundColor: primaryColor }}
786
+ >
787
+ Download Config
788
+ </button>
789
+ </div>
790
+ </div>
791
+ </div>
792
+ )}
793
+ </div>
794
+ );
795
+ }
796
+
797
+ function ChevronIcon({ className }: { className?: string }) {
798
+ return (
799
+ <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
800
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
801
+ </svg>
802
+ );
803
+ }
804
+
805
+ function CloseIcon({ className }: { className?: string }) {
806
+ return (
807
+ <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
808
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
809
+ </svg>
810
+ );
811
+ }
812
+
813
+ function BackIcon({ className }: { className?: string }) {
814
+ return (
815
+ <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
816
+ <path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
817
+ </svg>
818
+ );
819
+ }
820
+
821
+ function SunIcon({ className }: { className?: string }) {
822
+ return (
823
+ <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
824
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
825
+ </svg>
826
+ );
827
+ }
828
+
829
+ function MoonIcon({ className }: { className?: string }) {
830
+ return (
831
+ <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
832
+ <path strokeLinecap="round" strokeLinejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
833
+ </svg>
834
+ );
835
+ }