@fragments-sdk/cli 0.2.2

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 (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +4783 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-4FDQSGKX.js +786 -0
  7. package/dist/chunk-4FDQSGKX.js.map +1 -0
  8. package/dist/chunk-7H2MMGYG.js +369 -0
  9. package/dist/chunk-7H2MMGYG.js.map +1 -0
  10. package/dist/chunk-BSCG3IP7.js +619 -0
  11. package/dist/chunk-BSCG3IP7.js.map +1 -0
  12. package/dist/chunk-LY2CFFPY.js +898 -0
  13. package/dist/chunk-LY2CFFPY.js.map +1 -0
  14. package/dist/chunk-MUZ6CM66.js +6636 -0
  15. package/dist/chunk-MUZ6CM66.js.map +1 -0
  16. package/dist/chunk-OAENNG3G.js +1489 -0
  17. package/dist/chunk-OAENNG3G.js.map +1 -0
  18. package/dist/chunk-XHNKNI6J.js +235 -0
  19. package/dist/chunk-XHNKNI6J.js.map +1 -0
  20. package/dist/core-DWKLGY4N.js +68 -0
  21. package/dist/core-DWKLGY4N.js.map +1 -0
  22. package/dist/generate-4LQNJ7SX.js +249 -0
  23. package/dist/generate-4LQNJ7SX.js.map +1 -0
  24. package/dist/index.d.ts +775 -0
  25. package/dist/index.js +41 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/init-EMVI47QG.js +416 -0
  28. package/dist/init-EMVI47QG.js.map +1 -0
  29. package/dist/mcp-bin.d.ts +1 -0
  30. package/dist/mcp-bin.js +1117 -0
  31. package/dist/mcp-bin.js.map +1 -0
  32. package/dist/scan-4YPRF7FV.js +12 -0
  33. package/dist/scan-4YPRF7FV.js.map +1 -0
  34. package/dist/service-QSZMZJBJ.js +208 -0
  35. package/dist/service-QSZMZJBJ.js.map +1 -0
  36. package/dist/static-viewer-MIPGZ4Z7.js +12 -0
  37. package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
  38. package/dist/test-SQ5ZHXWU.js +1067 -0
  39. package/dist/test-SQ5ZHXWU.js.map +1 -0
  40. package/dist/tokens-HSGMYK64.js +173 -0
  41. package/dist/tokens-HSGMYK64.js.map +1 -0
  42. package/dist/viewer-YRF4SQE4.js +11101 -0
  43. package/dist/viewer-YRF4SQE4.js.map +1 -0
  44. package/package.json +107 -0
  45. package/src/ai.ts +266 -0
  46. package/src/analyze.ts +265 -0
  47. package/src/bin.ts +916 -0
  48. package/src/build.ts +248 -0
  49. package/src/commands/a11y.ts +302 -0
  50. package/src/commands/add.ts +313 -0
  51. package/src/commands/audit.ts +195 -0
  52. package/src/commands/baseline.ts +221 -0
  53. package/src/commands/build.ts +144 -0
  54. package/src/commands/compare.ts +337 -0
  55. package/src/commands/context.ts +107 -0
  56. package/src/commands/dev.ts +107 -0
  57. package/src/commands/enhance.ts +858 -0
  58. package/src/commands/generate.ts +391 -0
  59. package/src/commands/init.ts +531 -0
  60. package/src/commands/link/figma.ts +645 -0
  61. package/src/commands/link/index.ts +10 -0
  62. package/src/commands/link/storybook.ts +267 -0
  63. package/src/commands/list.ts +49 -0
  64. package/src/commands/metrics.ts +114 -0
  65. package/src/commands/reset.ts +242 -0
  66. package/src/commands/scan.ts +537 -0
  67. package/src/commands/storygen.ts +207 -0
  68. package/src/commands/tokens.ts +251 -0
  69. package/src/commands/validate.ts +93 -0
  70. package/src/commands/verify.ts +215 -0
  71. package/src/core/composition.test.ts +262 -0
  72. package/src/core/composition.ts +255 -0
  73. package/src/core/config.ts +84 -0
  74. package/src/core/constants.ts +111 -0
  75. package/src/core/context.ts +380 -0
  76. package/src/core/defineSegment.ts +137 -0
  77. package/src/core/discovery.ts +337 -0
  78. package/src/core/figma.ts +263 -0
  79. package/src/core/fragment-types.ts +214 -0
  80. package/src/core/generators/context.ts +389 -0
  81. package/src/core/generators/index.ts +23 -0
  82. package/src/core/generators/registry.ts +364 -0
  83. package/src/core/generators/typescript-extractor.ts +374 -0
  84. package/src/core/importAnalyzer.ts +217 -0
  85. package/src/core/index.ts +149 -0
  86. package/src/core/loader.ts +155 -0
  87. package/src/core/node.ts +63 -0
  88. package/src/core/parser.ts +551 -0
  89. package/src/core/previewLoader.ts +172 -0
  90. package/src/core/schema/fragment.schema.json +189 -0
  91. package/src/core/schema/registry.schema.json +137 -0
  92. package/src/core/schema.ts +182 -0
  93. package/src/core/storyAdapter.test.ts +571 -0
  94. package/src/core/storyAdapter.ts +761 -0
  95. package/src/core/token-types.ts +287 -0
  96. package/src/core/types.ts +754 -0
  97. package/src/diff.ts +323 -0
  98. package/src/index.ts +43 -0
  99. package/src/mcp/__tests__/projectFields.test.ts +130 -0
  100. package/src/mcp/bin.ts +36 -0
  101. package/src/mcp/index.ts +8 -0
  102. package/src/mcp/server.ts +1310 -0
  103. package/src/mcp/utils.ts +54 -0
  104. package/src/mcp-bin.ts +36 -0
  105. package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
  106. package/src/migrate/__tests__/args/args.test.ts +452 -0
  107. package/src/migrate/__tests__/meta/meta.test.ts +198 -0
  108. package/src/migrate/__tests__/stories/stories.test.ts +278 -0
  109. package/src/migrate/__tests__/utils/utils.test.ts +371 -0
  110. package/src/migrate/__tests__/values/values.test.ts +303 -0
  111. package/src/migrate/bin.ts +108 -0
  112. package/src/migrate/converter.ts +658 -0
  113. package/src/migrate/detect.ts +196 -0
  114. package/src/migrate/index.ts +45 -0
  115. package/src/migrate/migrate.ts +163 -0
  116. package/src/migrate/parser.ts +1136 -0
  117. package/src/migrate/report.ts +624 -0
  118. package/src/migrate/types.ts +169 -0
  119. package/src/screenshot.ts +249 -0
  120. package/src/service/__tests__/ast-utils.test.ts +426 -0
  121. package/src/service/__tests__/enhance-scanner.test.ts +200 -0
  122. package/src/service/__tests__/figma/figma.test.ts +652 -0
  123. package/src/service/__tests__/metrics-store.test.ts +409 -0
  124. package/src/service/__tests__/patch-generator.test.ts +186 -0
  125. package/src/service/__tests__/props-extractor.test.ts +365 -0
  126. package/src/service/__tests__/token-registry.test.ts +267 -0
  127. package/src/service/analytics.ts +659 -0
  128. package/src/service/ast-utils.ts +444 -0
  129. package/src/service/browser-pool.ts +339 -0
  130. package/src/service/capture.ts +267 -0
  131. package/src/service/diff.ts +279 -0
  132. package/src/service/enhance/aggregator.ts +489 -0
  133. package/src/service/enhance/cache.ts +275 -0
  134. package/src/service/enhance/codebase-scanner.ts +357 -0
  135. package/src/service/enhance/context-generator.ts +529 -0
  136. package/src/service/enhance/doc-extractor.ts +523 -0
  137. package/src/service/enhance/index.ts +131 -0
  138. package/src/service/enhance/props-extractor.ts +665 -0
  139. package/src/service/enhance/scanner.ts +445 -0
  140. package/src/service/enhance/storybook-parser.ts +552 -0
  141. package/src/service/enhance/types.ts +346 -0
  142. package/src/service/enhance/variant-renderer.ts +479 -0
  143. package/src/service/figma.ts +1008 -0
  144. package/src/service/index.ts +249 -0
  145. package/src/service/metrics-store.ts +333 -0
  146. package/src/service/patch-generator.ts +349 -0
  147. package/src/service/report.ts +854 -0
  148. package/src/service/storage.ts +401 -0
  149. package/src/service/token-fixes.ts +281 -0
  150. package/src/service/token-parser.ts +504 -0
  151. package/src/service/token-registry.ts +721 -0
  152. package/src/service/utils.ts +172 -0
  153. package/src/setup.ts +241 -0
  154. package/src/shared/command-wrapper.ts +81 -0
  155. package/src/shared/dev-server-client.ts +199 -0
  156. package/src/shared/index.ts +8 -0
  157. package/src/shared/segment-loader.ts +59 -0
  158. package/src/shared/types.ts +147 -0
  159. package/src/static-viewer.ts +715 -0
  160. package/src/test/discovery.ts +172 -0
  161. package/src/test/index.ts +281 -0
  162. package/src/test/reporters/console.ts +194 -0
  163. package/src/test/reporters/json.ts +190 -0
  164. package/src/test/reporters/junit.ts +186 -0
  165. package/src/test/runner.ts +598 -0
  166. package/src/test/types.ts +245 -0
  167. package/src/test/watch.ts +200 -0
  168. package/src/validators.ts +152 -0
  169. package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
  170. package/src/viewer/__tests__/render-utils.test.ts +232 -0
  171. package/src/viewer/__tests__/style-utils.test.ts +404 -0
  172. package/src/viewer/bin.ts +86 -0
  173. package/src/viewer/cli/health.ts +256 -0
  174. package/src/viewer/cli/index.ts +33 -0
  175. package/src/viewer/cli/scan.ts +124 -0
  176. package/src/viewer/cli/utils.ts +174 -0
  177. package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
  178. package/src/viewer/components/ActionCapture.tsx +172 -0
  179. package/src/viewer/components/ActionsPanel.tsx +371 -0
  180. package/src/viewer/components/App.tsx +638 -0
  181. package/src/viewer/components/BottomPanel.tsx +224 -0
  182. package/src/viewer/components/CodePanel.tsx +589 -0
  183. package/src/viewer/components/CommandPalette.tsx +336 -0
  184. package/src/viewer/components/ComponentGraph.tsx +394 -0
  185. package/src/viewer/components/ComponentHeader.tsx +85 -0
  186. package/src/viewer/components/ContractPanel.tsx +234 -0
  187. package/src/viewer/components/ErrorBoundary.tsx +85 -0
  188. package/src/viewer/components/FigmaEmbed.tsx +231 -0
  189. package/src/viewer/components/FragmentEditor.tsx +485 -0
  190. package/src/viewer/components/HealthDashboard.tsx +452 -0
  191. package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
  192. package/src/viewer/components/Icons.tsx +417 -0
  193. package/src/viewer/components/InteractionsPanel.tsx +720 -0
  194. package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
  195. package/src/viewer/components/IsolatedRender.tsx +111 -0
  196. package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
  197. package/src/viewer/components/LandingPage.tsx +441 -0
  198. package/src/viewer/components/Layout.tsx +22 -0
  199. package/src/viewer/components/LeftSidebar.tsx +391 -0
  200. package/src/viewer/components/MultiViewportPreview.tsx +429 -0
  201. package/src/viewer/components/PreviewArea.tsx +404 -0
  202. package/src/viewer/components/PreviewFrameHost.tsx +310 -0
  203. package/src/viewer/components/PreviewPane.tsx +150 -0
  204. package/src/viewer/components/PreviewToolbar.tsx +176 -0
  205. package/src/viewer/components/PropsEditor.tsx +512 -0
  206. package/src/viewer/components/PropsTable.tsx +98 -0
  207. package/src/viewer/components/RelationsSection.tsx +57 -0
  208. package/src/viewer/components/ResizablePanel.tsx +328 -0
  209. package/src/viewer/components/RightSidebar.tsx +118 -0
  210. package/src/viewer/components/ScreenshotButton.tsx +90 -0
  211. package/src/viewer/components/Sidebar.tsx +169 -0
  212. package/src/viewer/components/SkeletonLoader.tsx +156 -0
  213. package/src/viewer/components/StoryRenderer.tsx +128 -0
  214. package/src/viewer/components/ThemeProvider.tsx +96 -0
  215. package/src/viewer/components/Toast.tsx +67 -0
  216. package/src/viewer/components/TokenStylePanel.tsx +708 -0
  217. package/src/viewer/components/UsageSection.tsx +95 -0
  218. package/src/viewer/components/VariantMatrix.tsx +350 -0
  219. package/src/viewer/components/VariantRenderer.tsx +131 -0
  220. package/src/viewer/components/VariantTabs.tsx +84 -0
  221. package/src/viewer/components/ViewportSelector.tsx +165 -0
  222. package/src/viewer/components/_future/CreatePage.tsx +836 -0
  223. package/src/viewer/composition-renderer.ts +381 -0
  224. package/src/viewer/constants/index.ts +1 -0
  225. package/src/viewer/constants/ui.ts +185 -0
  226. package/src/viewer/entry.tsx +299 -0
  227. package/src/viewer/hooks/index.ts +2 -0
  228. package/src/viewer/hooks/useA11yCache.ts +383 -0
  229. package/src/viewer/hooks/useA11yService.ts +498 -0
  230. package/src/viewer/hooks/useActions.ts +138 -0
  231. package/src/viewer/hooks/useAppState.ts +124 -0
  232. package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
  233. package/src/viewer/hooks/useHmrStatus.ts +109 -0
  234. package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
  235. package/src/viewer/hooks/usePreviewBridge.ts +347 -0
  236. package/src/viewer/hooks/useScrollSpy.ts +78 -0
  237. package/src/viewer/hooks/useUrlState.ts +330 -0
  238. package/src/viewer/hooks/useViewSettings.ts +125 -0
  239. package/src/viewer/index.html +28 -0
  240. package/src/viewer/index.ts +14 -0
  241. package/src/viewer/intelligence/healthReport.ts +505 -0
  242. package/src/viewer/intelligence/styleDrift.ts +340 -0
  243. package/src/viewer/intelligence/usageScanner.ts +309 -0
  244. package/src/viewer/jsx-parser.ts +485 -0
  245. package/src/viewer/postcss.config.js +6 -0
  246. package/src/viewer/preview-frame-entry.tsx +25 -0
  247. package/src/viewer/preview-frame.html +109 -0
  248. package/src/viewer/render-template.html +68 -0
  249. package/src/viewer/render-utils.ts +170 -0
  250. package/src/viewer/server.ts +276 -0
  251. package/src/viewer/style-utils.ts +414 -0
  252. package/src/viewer/styles/globals.css +355 -0
  253. package/src/viewer/tailwind.config.js +37 -0
  254. package/src/viewer/types/a11y.ts +197 -0
  255. package/src/viewer/utils/a11y-fixes.ts +471 -0
  256. package/src/viewer/utils/actionExport.ts +372 -0
  257. package/src/viewer/utils/colorSchemes.ts +201 -0
  258. package/src/viewer/utils/detectRelationships.ts +256 -0
  259. package/src/viewer/vite-plugin.ts +2143 -0
@@ -0,0 +1,589 @@
1
+ import { useState, useCallback, useMemo, useEffect, isValidElement, type ReactNode } from 'react';
2
+ import type { SegmentVariant, PropDefinition } from '../../core/index.js';
3
+ import { codeToHtml } from 'shiki';
4
+ import clsx from 'clsx';
5
+ import { CopyIcon, CheckIcon } from './Icons.js';
6
+
7
+ interface CodePanelProps {
8
+ variant: SegmentVariant;
9
+ componentName: string;
10
+ compact?: boolean;
11
+ propDefs?: Record<string, PropDefinition>;
12
+ }
13
+
14
+ // Extract props from rendered element by calling render() and introspecting
15
+ function extractPropsFromRender(variant: SegmentVariant, componentName: string): Record<string, unknown> | null {
16
+ try {
17
+ const rendered = variant.render();
18
+ if (!isValidElement(rendered)) return null;
19
+
20
+ // Check if this is the expected component (by displayName or name)
21
+ const elementType = rendered.type;
22
+ const typeName = typeof elementType === 'function'
23
+ ? (elementType as { displayName?: string; name?: string }).displayName || (elementType as { name?: string }).name
24
+ : typeof elementType === 'string' ? elementType : null;
25
+
26
+ // Only extract if it's a single element of the expected component type
27
+ if (typeName && typeName.toLowerCase() === componentName.toLowerCase()) {
28
+ return rendered.props as Record<string, unknown>;
29
+ }
30
+
31
+ // If it's a wrapper element, try to find the component in children
32
+ const props = rendered.props as { children?: ReactNode };
33
+ if (props.children && isValidElement(props.children)) {
34
+ const childType = props.children.type;
35
+ const childTypeName = typeof childType === 'function'
36
+ ? (childType as { displayName?: string; name?: string }).displayName || (childType as { name?: string }).name
37
+ : null;
38
+ if (childTypeName && childTypeName.toLowerCase() === componentName.toLowerCase()) {
39
+ return props.children.props as Record<string, unknown>;
40
+ }
41
+ }
42
+
43
+ return null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ // Check if the component likely needs state management
50
+ function needsStatefulExample(componentName: string, propDefs?: Record<string, PropDefinition>): boolean {
51
+ const statefulComponents = ['toggle', 'checkbox', 'input', 'select', 'switch', 'radio'];
52
+ const lowerName = componentName.toLowerCase();
53
+
54
+ if (statefulComponents.some(c => lowerName.includes(c))) {
55
+ return true;
56
+ }
57
+
58
+ // Check if component has onChange/onChecked props
59
+ if (propDefs) {
60
+ const hasChangeHandler = Object.keys(propDefs).some(
61
+ key => key === 'onChange' || key === 'onChecked' || key === 'onValueChange'
62
+ );
63
+ const hasValueProp = Object.keys(propDefs).some(
64
+ key => key === 'value' || key === 'checked'
65
+ );
66
+ return hasChangeHandler && hasValueProp;
67
+ }
68
+
69
+ return false;
70
+ }
71
+
72
+ /**
73
+ * Convert compiled jsxDEV/jsx calls back to clean JSX syntax.
74
+ * Handles: jsxDEV(Component, { props, children }, ...) -> <Component props>children</Component>
75
+ */
76
+ function decompileJsxDev(code: string, indent = 0): string {
77
+ const indentStr = ' '.repeat(indent);
78
+
79
+ // Match jsxDEV or jsx call: jsxDEV(Component, {props}, ...)
80
+ // Also handle _jsxDEV, /* @__PURE__ */ prefix
81
+ const jsxMatch = code.match(/(?:\/\*\s*@__PURE__\s*\*\/\s*)?_?jsxs?(?:DEV)?\s*\(\s*([^,]+)\s*,\s*(\{[\s\S]*\})\s*(?:,[\s\S]*)?\)$/);
82
+
83
+ if (!jsxMatch) {
84
+ // Not a jsxDEV call - might be a plain string or primitive
85
+ const trimmed = code.trim();
86
+ // If it's a quoted string, return the content
87
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
88
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
89
+ return trimmed.slice(1, -1);
90
+ }
91
+ return trimmed;
92
+ }
93
+
94
+ let componentName = jsxMatch[1].trim();
95
+ const propsStr = jsxMatch[2];
96
+
97
+ // Clean up component name (remove quotes for HTML elements)
98
+ if (componentName.startsWith('"') || componentName.startsWith("'")) {
99
+ componentName = componentName.slice(1, -1);
100
+ }
101
+
102
+ // Parse props object - extract key-value pairs
103
+ const props: Record<string, string> = {};
104
+ let children: string | null = null;
105
+
106
+ // Debug props that should be excluded (added by jsxDEV in development)
107
+ const debugProps = new Set(['fileName', 'lineNumber', 'columnNumber', '__source', '__self']);
108
+
109
+ // Simple prop extraction (handles most common cases)
110
+ // Match: key: value, key: "string", key: number, key: boolean
111
+ const propMatches = propsStr.matchAll(/(\w+)\s*:\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[\w.]+|\{[^}]*\}|(?:\/\*[\s\S]*?\*\/\s*)?_?jsxs?(?:DEV)?\s*\([^)]*(?:\([^)]*\)[^)]*)*\))/g);
112
+
113
+ for (const match of propMatches) {
114
+ const key = match[1];
115
+ let value = match[2];
116
+
117
+ // Skip debug props
118
+ if (debugProps.has(key)) continue;
119
+
120
+ if (key === 'children') {
121
+ // Handle children specially
122
+ if (value.includes('jsxDEV') || value.includes('jsx(') || value.includes('_jsx')) {
123
+ // Nested JSX - recursively decompile
124
+ children = decompileJsxDev(value, indent + 1);
125
+ } else if (value.startsWith('"') || value.startsWith("'")) {
126
+ // String children
127
+ children = value.slice(1, -1);
128
+ } else {
129
+ children = `{${value}}`;
130
+ }
131
+ } else {
132
+ // Regular props
133
+ if (value.startsWith('"') || value.startsWith("'")) {
134
+ props[key] = value; // Keep as quoted string
135
+ } else if (value === 'true' || value === 'false') {
136
+ props[key] = value === 'true' ? `{true}` : `{false}`;
137
+ } else if (!isNaN(Number(value))) {
138
+ props[key] = `{${value}}`;
139
+ } else {
140
+ props[key] = `{${value}}`;
141
+ }
142
+ }
143
+ }
144
+
145
+ // Build JSX string
146
+ const propParts = Object.entries(props)
147
+ .filter(([k]) => k !== 'children')
148
+ .map(([k, v]) => {
149
+ // Convert camelCase event handlers back
150
+ if (v.startsWith('"') || v.startsWith("'")) {
151
+ return `${k}=${v}`;
152
+ }
153
+ return `${k}=${v}`;
154
+ });
155
+
156
+ const propsJsx = propParts.length > 0 ? ' ' + propParts.join(' ') : '';
157
+
158
+ if (children) {
159
+ if (children.includes('\n')) {
160
+ return `${indentStr}<${componentName}${propsJsx}>\n${children}\n${indentStr}</${componentName}>`;
161
+ }
162
+ return `${indentStr}<${componentName}${propsJsx}>${children}</${componentName}>`;
163
+ }
164
+
165
+ return `${indentStr}<${componentName}${propsJsx} />`;
166
+ }
167
+
168
+ /**
169
+ * Extract the JSX body from a render function's source code.
170
+ * Works with arrow functions like: () => <Component /> or () => (<div>...</div>)
171
+ * Also handles compiled jsxDEV calls and converts them back to JSX.
172
+ */
173
+ function extractRenderBody(renderFn: () => ReactNode): string | null {
174
+ try {
175
+ const source = renderFn.toString();
176
+
177
+ // Match arrow function body: () => <...> or () => (...)
178
+ // Handle both: () => <X /> and () => (\n <X />\n)
179
+ const arrowMatch = source.match(/^\s*\(\s*\)\s*=>\s*(.+)$/s);
180
+ if (arrowMatch) {
181
+ let body = arrowMatch[1].trim();
182
+
183
+ // Remove outer parentheses if present
184
+ if (body.startsWith('(') && body.endsWith(')')) {
185
+ body = body.slice(1, -1).trim();
186
+ }
187
+
188
+ // Check if this is compiled JSX (jsxDEV calls)
189
+ if (body.includes('jsxDEV') || body.includes('jsx(') || body.includes('_jsx')) {
190
+ return decompileJsxDev(body);
191
+ }
192
+
193
+ return body;
194
+ }
195
+
196
+ // Match function body with return statement
197
+ const returnMatch = source.match(/return\s*\(\s*([\s\S]*?)\s*\)\s*;?\s*\}?\s*$/);
198
+ if (returnMatch) {
199
+ const body = returnMatch[1].trim();
200
+
201
+ // Check if this is compiled JSX
202
+ if (body.includes('jsxDEV') || body.includes('jsx(') || body.includes('_jsx')) {
203
+ return decompileJsxDev(body);
204
+ }
205
+
206
+ return body;
207
+ }
208
+
209
+ return null;
210
+ } catch {
211
+ return null;
212
+ }
213
+ }
214
+
215
+ export function CodePanel({ variant, componentName, compact = false, propDefs }: CodePanelProps) {
216
+ const [copied, setCopied] = useState(false);
217
+ const [highlightedHtml, setHighlightedHtml] = useState<string>('');
218
+
219
+ // Generate code - extract from render function source for accuracy
220
+ const generatedCode = useMemo(() => {
221
+ // Priority 1: Use variant.code if available (from compiled JSON/AST)
222
+ if (variant.code) {
223
+ return generateFullExample(componentName, variant.code);
224
+ }
225
+
226
+ // Priority 2: Extract source from render function (runtime)
227
+ if (variant.render) {
228
+ const renderBody = extractRenderBody(variant.render);
229
+ if (renderBody) {
230
+ return generateFullExample(componentName, renderBody);
231
+ }
232
+ }
233
+
234
+ // Priority 3: Generate from args (fallback for edge cases)
235
+ const needsState = needsStatefulExample(componentName, propDefs);
236
+ let effectiveArgs = variant.args;
237
+
238
+ // If no args on variant, try to extract from rendered element
239
+ if (!effectiveArgs || Object.keys(effectiveArgs).length === 0) {
240
+ effectiveArgs = extractPropsFromRender(variant, componentName) ?? undefined;
241
+ }
242
+
243
+ return generateCombinedCode(componentName, propDefs, effectiveArgs, needsState);
244
+ }, [componentName, variant, propDefs]);
245
+
246
+ // Apply syntax highlighting
247
+ useEffect(() => {
248
+ let cancelled = false;
249
+
250
+ codeToHtml(generatedCode, {
251
+ lang: 'tsx',
252
+ theme: 'one-dark-pro',
253
+ }).then(html => {
254
+ if (!cancelled) {
255
+ setHighlightedHtml(html);
256
+ }
257
+ }).catch(err => {
258
+ console.error('Syntax highlighting failed:', err);
259
+ if (!cancelled) {
260
+ // Fallback to plain text
261
+ setHighlightedHtml(`<pre><code>${escapeHtml(generatedCode)}</code></pre>`);
262
+ }
263
+ });
264
+
265
+ return () => { cancelled = true; };
266
+ }, [generatedCode]);
267
+
268
+ const handleCopy = useCallback(async () => {
269
+ try {
270
+ await navigator.clipboard.writeText(generatedCode);
271
+ setCopied(true);
272
+ setTimeout(() => setCopied(false), 2000);
273
+ } catch (err) {
274
+ console.error('Failed to copy:', err);
275
+ }
276
+ }, [generatedCode]);
277
+
278
+ return (
279
+ <div className="relative">
280
+ {/* Syntax highlighted code */}
281
+ <div
282
+ className={clsx(
283
+ 'rounded-lg overflow-auto border border-[--border] bg-[#282c34]',
284
+ '[&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:p-4',
285
+ '[&_code]:!bg-transparent [&_code]:text-[13px] [&_code]:leading-relaxed',
286
+ '[&_.shiki]:!bg-transparent',
287
+ // Custom selection highlight for better visibility
288
+ '[&_*::selection]:bg-blue-500/40 [&_*::selection]:text-white',
289
+ compact && '[&_code]:text-xs'
290
+ )}
291
+ style={{ maxHeight: 400 }}
292
+ dangerouslySetInnerHTML={{ __html: highlightedHtml || '<div class="p-4 text-gray-500 text-sm">Loading...</div>' }}
293
+ />
294
+
295
+ {/* Copy button - fixed to top right of editor */}
296
+ <button
297
+ onClick={handleCopy}
298
+ className={clsx(
299
+ 'absolute top-2 right-2 flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-md transition-all duration-200',
300
+ copied
301
+ ? 'bg-green-500/20 text-green-400 border border-green-500/30'
302
+ : 'bg-white/10 text-gray-300 border border-white/10 hover:bg-white/15 hover:text-white'
303
+ )}
304
+ >
305
+ {copied ? (
306
+ <>
307
+ <CheckIcon className="w-3.5 h-3.5" />
308
+ <span>Copied!</span>
309
+ </>
310
+ ) : (
311
+ <>
312
+ <CopyIcon className="w-3.5 h-3.5" />
313
+ <span>Copy</span>
314
+ </>
315
+ )}
316
+ </button>
317
+ </div>
318
+ );
319
+ }
320
+
321
+ function escapeHtml(str: string): string {
322
+ return str
323
+ .replace(/&/g, '&amp;')
324
+ .replace(/</g, '&lt;')
325
+ .replace(/>/g, '&gt;')
326
+ .replace(/"/g, '&quot;')
327
+ .replace(/'/g, '&#039;');
328
+ }
329
+
330
+ /**
331
+ * Normalize indentation by removing common leading whitespace from all lines.
332
+ * Handles JSX where first line may be at column 0 but inner content is indented.
333
+ */
334
+ function normalizeIndentation(code: string): string {
335
+ const lines = code.split('\n');
336
+ if (lines.length <= 1) return code;
337
+
338
+ // Find minimum indentation (ignoring empty lines and first line)
339
+ // First line often has 0 indent, but inner content is over-indented
340
+ let minIndent = Infinity;
341
+ const firstLineIndent = lines[0].match(/^(\s*)/)?.[1].length ?? 0;
342
+
343
+ for (let i = 1; i < lines.length; i++) {
344
+ const line = lines[i];
345
+ if (line.trim().length === 0) continue;
346
+ const indent = line.match(/^(\s*)/)?.[1].length ?? 0;
347
+ minIndent = Math.min(minIndent, indent);
348
+ }
349
+
350
+ // If first line has indent, include it in calculation
351
+ if (firstLineIndent > 0) {
352
+ minIndent = Math.min(minIndent, firstLineIndent);
353
+ }
354
+
355
+ if (minIndent === Infinity || minIndent === 0) return code;
356
+
357
+ // Remove the common indentation from all lines
358
+ return lines
359
+ .map(line => line.slice(Math.min(minIndent, line.match(/^(\s*)/)?.[1].length ?? 0)))
360
+ .join('\n');
361
+ }
362
+
363
+ // Generate a full example from the variant's code string
364
+ // Shows the ACTUAL code from the fragment file - no transformations
365
+ function generateFullExample(
366
+ componentName: string,
367
+ variantCode: string
368
+ ): string {
369
+ // Normalize indentation first
370
+ const normalizedCode = normalizeIndentation(variantCode);
371
+
372
+ // Add component import
373
+ const imports = [`import { ${componentName} } from '@/components/${componentName}';`];
374
+
375
+ // Check if the code uses StatefulXxx wrapper - if so, include that helper
376
+ const statefulPattern = new RegExp(`<Stateful${componentName}`, 'i');
377
+ const usesStatefulWrapper = statefulPattern.test(normalizedCode);
378
+
379
+ if (usesStatefulWrapper) {
380
+ imports.unshift(`import { useState } from 'react';`);
381
+
382
+ // Determine the state type based on component
383
+ const lowerName = componentName.toLowerCase();
384
+ const isToggleLike = lowerName.includes('toggle') || lowerName.includes('checkbox') || lowerName.includes('switch');
385
+
386
+ const stateType = isToggleLike ? 'boolean' : 'string';
387
+ const defaultValue = isToggleLike ? 'false' : "''";
388
+ const stateName = isToggleLike ? 'checked' : 'value';
389
+ const setterName = isToggleLike ? 'setChecked' : 'setValue';
390
+ const valueProp = isToggleLike ? 'checked' : 'value';
391
+ const handlerProp = 'onChange';
392
+
393
+ // Generate the stateful wrapper helper that the code uses
394
+ const helperCode = `
395
+ // Stateful wrapper for interactive demos
396
+ function Stateful${componentName}(props: React.ComponentProps<typeof ${componentName}>) {
397
+ const [${stateName}, ${setterName}] = useState(props.${valueProp} ?? ${defaultValue});
398
+ return <${componentName} {...props} ${valueProp}={${stateName}} ${handlerProp}={${setterName}} />;
399
+ }`;
400
+
401
+ return `${imports.join('\n')}
402
+ ${helperCode}
403
+
404
+ // Example usage:
405
+ ${normalizedCode}`;
406
+ }
407
+
408
+ // For non-stateful code, just show the raw code with import
409
+ return `${imports.join('\n')}
410
+
411
+ ${normalizedCode}`;
412
+ }
413
+
414
+ // Generate combined import statement + JSX usage
415
+ function generateCombinedCode(
416
+ componentName: string,
417
+ propDefs?: Record<string, PropDefinition>,
418
+ variantArgs?: Record<string, unknown>,
419
+ needsState?: boolean
420
+ ): string {
421
+ const imports: string[] = [];
422
+ const hooks: string[] = [];
423
+
424
+ if (needsState) {
425
+ imports.push(`import { useState } from 'react';`);
426
+ }
427
+ imports.push(`import { ${componentName} } from '@/components/${componentName}';`);
428
+
429
+ // Determine state props that will be managed by hooks (to avoid duplicates)
430
+ const lowerName = componentName.toLowerCase();
431
+ const isToggleLike = lowerName.includes('toggle') || lowerName.includes('checkbox') || lowerName.includes('switch');
432
+ const statePropsToSkip = needsState
433
+ ? isToggleLike
434
+ ? ['checked', 'onChange', 'onCheckedChange']
435
+ : ['value', 'onChange', 'onValueChange']
436
+ : [];
437
+
438
+ // Build props string - prefer actual variant args over prop definitions
439
+ const propsEntries: string[] = [];
440
+ let childrenValue: unknown = undefined;
441
+
442
+ if (variantArgs && Object.keys(variantArgs).length > 0) {
443
+ // Use actual variant args for accurate code generation
444
+ for (const [name, value] of Object.entries(variantArgs)) {
445
+ if (value === undefined) continue;
446
+
447
+ // Handle children specially - will be rendered as element content
448
+ if (name === 'children') {
449
+ childrenValue = value;
450
+ continue;
451
+ }
452
+
453
+ // Skip function props (callbacks) - they're runtime handlers
454
+ if (typeof value === 'function') continue;
455
+
456
+ // Skip props that will be managed by state hooks
457
+ if (statePropsToSkip.includes(name)) continue;
458
+
459
+ const propType = propDefs?.[name]?.type || typeof value;
460
+ const formattedValue = formatPropValue(value, propType);
461
+ propsEntries.push(`${name}=${formattedValue}`);
462
+ }
463
+ } else if (propDefs) {
464
+ // Fallback to prop definitions if no variant args
465
+ for (const [name, def] of Object.entries(propDefs)) {
466
+ if (name === 'children') {
467
+ if (def.default !== undefined) {
468
+ childrenValue = def.default;
469
+ }
470
+ continue;
471
+ }
472
+
473
+ // Skip props that will be managed by state hooks
474
+ if (statePropsToSkip.includes(name)) continue;
475
+
476
+ if (def.required && def.default !== undefined) {
477
+ const value = formatPropValue(def.default, def.type);
478
+ propsEntries.push(`${name}=${value}`);
479
+ } else if (def.required) {
480
+ const placeholder = getTypePlaceholder(name, def);
481
+ propsEntries.push(`${name}=${placeholder}`);
482
+ }
483
+ }
484
+ }
485
+
486
+ // Build JSX code
487
+ const hasChildren = childrenValue !== undefined && childrenValue !== null;
488
+ const childrenStr = hasChildren ? formatChildrenValue(childrenValue) : null;
489
+
490
+ // For stateful components, wrap in a function component
491
+ if (needsState) {
492
+ let stateName = 'value';
493
+ let setterName = 'setValue';
494
+ let handlerProp = 'onChange';
495
+ let valueProp = 'value';
496
+ let defaultValue = "''";
497
+ let stateType = 'string';
498
+
499
+ if (isToggleLike) {
500
+ stateType = 'boolean';
501
+ defaultValue = 'false';
502
+ stateName = 'checked';
503
+ setterName = 'setChecked';
504
+ handlerProp = propDefs?.['onCheckedChange'] ? 'onCheckedChange' : 'onChange';
505
+ valueProp = 'checked';
506
+ }
507
+
508
+ hooks.push(`const [${stateName}, ${setterName}] = useState<${stateType}>(${defaultValue});`);
509
+
510
+ // Add state props
511
+ propsEntries.push(`${valueProp}={${stateName}}`);
512
+ propsEntries.push(`${handlerProp}={${setterName}}`);
513
+
514
+ const propsStr = propsEntries.length > 0
515
+ ? `\n ${propsEntries.join('\n ')}\n `
516
+ : ' ';
517
+
518
+ const jsxCode = hasChildren
519
+ ? `<${componentName}${propsStr}>${childrenStr}</${componentName}>`
520
+ : `<${componentName}${propsStr}/>`;
521
+
522
+ return `${imports.join('\n')}
523
+
524
+ function Example() {
525
+ ${hooks.join('\n ')}
526
+
527
+ return (
528
+ ${jsxCode}
529
+ );
530
+ }`;
531
+ }
532
+
533
+ // Simple non-stateful code
534
+ let jsxCode: string;
535
+
536
+ if (propsEntries.length === 0 && !hasChildren) {
537
+ jsxCode = `<${componentName} />`;
538
+ } else if (propsEntries.length === 0 && hasChildren) {
539
+ jsxCode = `<${componentName}>${childrenStr}</${componentName}>`;
540
+ } else if (propsEntries.length === 1 && !hasChildren) {
541
+ jsxCode = `<${componentName} ${propsEntries[0]} />`;
542
+ } else if (propsEntries.length === 1 && hasChildren) {
543
+ jsxCode = `<${componentName} ${propsEntries[0]}>\n ${childrenStr}\n</${componentName}>`;
544
+ } else if (!hasChildren) {
545
+ jsxCode = `<${componentName}\n ${propsEntries.join('\n ')}\n/>`;
546
+ } else {
547
+ jsxCode = `<${componentName}\n ${propsEntries.join('\n ')}\n>\n ${childrenStr}\n</${componentName}>`;
548
+ }
549
+
550
+ return `${imports.join('\n')}
551
+
552
+ ${jsxCode}`;
553
+ }
554
+
555
+ function formatChildrenValue(value: unknown): string {
556
+ if (typeof value === 'string') {
557
+ return value;
558
+ }
559
+ if (typeof value === 'number' || typeof value === 'boolean') {
560
+ return `{${value}}`;
561
+ }
562
+ return '{/* children */}';
563
+ }
564
+
565
+ function formatPropValue(value: unknown, type: string): string {
566
+ if (type === 'string' || typeof value === 'string') {
567
+ return `"${value}"`;
568
+ }
569
+ if (type === 'boolean' || typeof value === 'boolean') {
570
+ return `{${value}}`;
571
+ }
572
+ if (type === 'number' || typeof value === 'number') {
573
+ return `{${value}}`;
574
+ }
575
+ return `{${JSON.stringify(value)}}`;
576
+ }
577
+
578
+ function getTypePlaceholder(name: string, def: PropDefinition): string {
579
+ if (def.values && def.values.length > 0) {
580
+ return `"${def.values[0]}"`;
581
+ }
582
+ switch (def.type) {
583
+ case 'string': return `"${name}"`;
584
+ case 'boolean': return '{true}';
585
+ case 'number': return '{0}';
586
+ case 'function': return '{() => {}}';
587
+ default: return '{undefined}';
588
+ }
589
+ }