@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.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +4783 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-4FDQSGKX.js +786 -0
- package/dist/chunk-4FDQSGKX.js.map +1 -0
- package/dist/chunk-7H2MMGYG.js +369 -0
- package/dist/chunk-7H2MMGYG.js.map +1 -0
- package/dist/chunk-BSCG3IP7.js +619 -0
- package/dist/chunk-BSCG3IP7.js.map +1 -0
- package/dist/chunk-LY2CFFPY.js +898 -0
- package/dist/chunk-LY2CFFPY.js.map +1 -0
- package/dist/chunk-MUZ6CM66.js +6636 -0
- package/dist/chunk-MUZ6CM66.js.map +1 -0
- package/dist/chunk-OAENNG3G.js +1489 -0
- package/dist/chunk-OAENNG3G.js.map +1 -0
- package/dist/chunk-XHNKNI6J.js +235 -0
- package/dist/chunk-XHNKNI6J.js.map +1 -0
- package/dist/core-DWKLGY4N.js +68 -0
- package/dist/core-DWKLGY4N.js.map +1 -0
- package/dist/generate-4LQNJ7SX.js +249 -0
- package/dist/generate-4LQNJ7SX.js.map +1 -0
- package/dist/index.d.ts +775 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/init-EMVI47QG.js +416 -0
- package/dist/init-EMVI47QG.js.map +1 -0
- package/dist/mcp-bin.d.ts +1 -0
- package/dist/mcp-bin.js +1117 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/scan-4YPRF7FV.js +12 -0
- package/dist/scan-4YPRF7FV.js.map +1 -0
- package/dist/service-QSZMZJBJ.js +208 -0
- package/dist/service-QSZMZJBJ.js.map +1 -0
- package/dist/static-viewer-MIPGZ4Z7.js +12 -0
- package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
- package/dist/test-SQ5ZHXWU.js +1067 -0
- package/dist/test-SQ5ZHXWU.js.map +1 -0
- package/dist/tokens-HSGMYK64.js +173 -0
- package/dist/tokens-HSGMYK64.js.map +1 -0
- package/dist/viewer-YRF4SQE4.js +11101 -0
- package/dist/viewer-YRF4SQE4.js.map +1 -0
- package/package.json +107 -0
- package/src/ai.ts +266 -0
- package/src/analyze.ts +265 -0
- package/src/bin.ts +916 -0
- package/src/build.ts +248 -0
- package/src/commands/a11y.ts +302 -0
- package/src/commands/add.ts +313 -0
- package/src/commands/audit.ts +195 -0
- package/src/commands/baseline.ts +221 -0
- package/src/commands/build.ts +144 -0
- package/src/commands/compare.ts +337 -0
- package/src/commands/context.ts +107 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/enhance.ts +858 -0
- package/src/commands/generate.ts +391 -0
- package/src/commands/init.ts +531 -0
- package/src/commands/link/figma.ts +645 -0
- package/src/commands/link/index.ts +10 -0
- package/src/commands/link/storybook.ts +267 -0
- package/src/commands/list.ts +49 -0
- package/src/commands/metrics.ts +114 -0
- package/src/commands/reset.ts +242 -0
- package/src/commands/scan.ts +537 -0
- package/src/commands/storygen.ts +207 -0
- package/src/commands/tokens.ts +251 -0
- package/src/commands/validate.ts +93 -0
- package/src/commands/verify.ts +215 -0
- package/src/core/composition.test.ts +262 -0
- package/src/core/composition.ts +255 -0
- package/src/core/config.ts +84 -0
- package/src/core/constants.ts +111 -0
- package/src/core/context.ts +380 -0
- package/src/core/defineSegment.ts +137 -0
- package/src/core/discovery.ts +337 -0
- package/src/core/figma.ts +263 -0
- package/src/core/fragment-types.ts +214 -0
- package/src/core/generators/context.ts +389 -0
- package/src/core/generators/index.ts +23 -0
- package/src/core/generators/registry.ts +364 -0
- package/src/core/generators/typescript-extractor.ts +374 -0
- package/src/core/importAnalyzer.ts +217 -0
- package/src/core/index.ts +149 -0
- package/src/core/loader.ts +155 -0
- package/src/core/node.ts +63 -0
- package/src/core/parser.ts +551 -0
- package/src/core/previewLoader.ts +172 -0
- package/src/core/schema/fragment.schema.json +189 -0
- package/src/core/schema/registry.schema.json +137 -0
- package/src/core/schema.ts +182 -0
- package/src/core/storyAdapter.test.ts +571 -0
- package/src/core/storyAdapter.ts +761 -0
- package/src/core/token-types.ts +287 -0
- package/src/core/types.ts +754 -0
- package/src/diff.ts +323 -0
- package/src/index.ts +43 -0
- package/src/mcp/__tests__/projectFields.test.ts +130 -0
- package/src/mcp/bin.ts +36 -0
- package/src/mcp/index.ts +8 -0
- package/src/mcp/server.ts +1310 -0
- package/src/mcp/utils.ts +54 -0
- package/src/mcp-bin.ts +36 -0
- package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
- package/src/migrate/__tests__/args/args.test.ts +452 -0
- package/src/migrate/__tests__/meta/meta.test.ts +198 -0
- package/src/migrate/__tests__/stories/stories.test.ts +278 -0
- package/src/migrate/__tests__/utils/utils.test.ts +371 -0
- package/src/migrate/__tests__/values/values.test.ts +303 -0
- package/src/migrate/bin.ts +108 -0
- package/src/migrate/converter.ts +658 -0
- package/src/migrate/detect.ts +196 -0
- package/src/migrate/index.ts +45 -0
- package/src/migrate/migrate.ts +163 -0
- package/src/migrate/parser.ts +1136 -0
- package/src/migrate/report.ts +624 -0
- package/src/migrate/types.ts +169 -0
- package/src/screenshot.ts +249 -0
- package/src/service/__tests__/ast-utils.test.ts +426 -0
- package/src/service/__tests__/enhance-scanner.test.ts +200 -0
- package/src/service/__tests__/figma/figma.test.ts +652 -0
- package/src/service/__tests__/metrics-store.test.ts +409 -0
- package/src/service/__tests__/patch-generator.test.ts +186 -0
- package/src/service/__tests__/props-extractor.test.ts +365 -0
- package/src/service/__tests__/token-registry.test.ts +267 -0
- package/src/service/analytics.ts +659 -0
- package/src/service/ast-utils.ts +444 -0
- package/src/service/browser-pool.ts +339 -0
- package/src/service/capture.ts +267 -0
- package/src/service/diff.ts +279 -0
- package/src/service/enhance/aggregator.ts +489 -0
- package/src/service/enhance/cache.ts +275 -0
- package/src/service/enhance/codebase-scanner.ts +357 -0
- package/src/service/enhance/context-generator.ts +529 -0
- package/src/service/enhance/doc-extractor.ts +523 -0
- package/src/service/enhance/index.ts +131 -0
- package/src/service/enhance/props-extractor.ts +665 -0
- package/src/service/enhance/scanner.ts +445 -0
- package/src/service/enhance/storybook-parser.ts +552 -0
- package/src/service/enhance/types.ts +346 -0
- package/src/service/enhance/variant-renderer.ts +479 -0
- package/src/service/figma.ts +1008 -0
- package/src/service/index.ts +249 -0
- package/src/service/metrics-store.ts +333 -0
- package/src/service/patch-generator.ts +349 -0
- package/src/service/report.ts +854 -0
- package/src/service/storage.ts +401 -0
- package/src/service/token-fixes.ts +281 -0
- package/src/service/token-parser.ts +504 -0
- package/src/service/token-registry.ts +721 -0
- package/src/service/utils.ts +172 -0
- package/src/setup.ts +241 -0
- package/src/shared/command-wrapper.ts +81 -0
- package/src/shared/dev-server-client.ts +199 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/segment-loader.ts +59 -0
- package/src/shared/types.ts +147 -0
- package/src/static-viewer.ts +715 -0
- package/src/test/discovery.ts +172 -0
- package/src/test/index.ts +281 -0
- package/src/test/reporters/console.ts +194 -0
- package/src/test/reporters/json.ts +190 -0
- package/src/test/reporters/junit.ts +186 -0
- package/src/test/runner.ts +598 -0
- package/src/test/types.ts +245 -0
- package/src/test/watch.ts +200 -0
- package/src/validators.ts +152 -0
- package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
- package/src/viewer/__tests__/render-utils.test.ts +232 -0
- package/src/viewer/__tests__/style-utils.test.ts +404 -0
- package/src/viewer/bin.ts +86 -0
- package/src/viewer/cli/health.ts +256 -0
- package/src/viewer/cli/index.ts +33 -0
- package/src/viewer/cli/scan.ts +124 -0
- package/src/viewer/cli/utils.ts +174 -0
- package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
- package/src/viewer/components/ActionCapture.tsx +172 -0
- package/src/viewer/components/ActionsPanel.tsx +371 -0
- package/src/viewer/components/App.tsx +638 -0
- package/src/viewer/components/BottomPanel.tsx +224 -0
- package/src/viewer/components/CodePanel.tsx +589 -0
- package/src/viewer/components/CommandPalette.tsx +336 -0
- package/src/viewer/components/ComponentGraph.tsx +394 -0
- package/src/viewer/components/ComponentHeader.tsx +85 -0
- package/src/viewer/components/ContractPanel.tsx +234 -0
- package/src/viewer/components/ErrorBoundary.tsx +85 -0
- package/src/viewer/components/FigmaEmbed.tsx +231 -0
- package/src/viewer/components/FragmentEditor.tsx +485 -0
- package/src/viewer/components/HealthDashboard.tsx +452 -0
- package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
- package/src/viewer/components/Icons.tsx +417 -0
- package/src/viewer/components/InteractionsPanel.tsx +720 -0
- package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
- package/src/viewer/components/IsolatedRender.tsx +111 -0
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
- package/src/viewer/components/LandingPage.tsx +441 -0
- package/src/viewer/components/Layout.tsx +22 -0
- package/src/viewer/components/LeftSidebar.tsx +391 -0
- package/src/viewer/components/MultiViewportPreview.tsx +429 -0
- package/src/viewer/components/PreviewArea.tsx +404 -0
- package/src/viewer/components/PreviewFrameHost.tsx +310 -0
- package/src/viewer/components/PreviewPane.tsx +150 -0
- package/src/viewer/components/PreviewToolbar.tsx +176 -0
- package/src/viewer/components/PropsEditor.tsx +512 -0
- package/src/viewer/components/PropsTable.tsx +98 -0
- package/src/viewer/components/RelationsSection.tsx +57 -0
- package/src/viewer/components/ResizablePanel.tsx +328 -0
- package/src/viewer/components/RightSidebar.tsx +118 -0
- package/src/viewer/components/ScreenshotButton.tsx +90 -0
- package/src/viewer/components/Sidebar.tsx +169 -0
- package/src/viewer/components/SkeletonLoader.tsx +156 -0
- package/src/viewer/components/StoryRenderer.tsx +128 -0
- package/src/viewer/components/ThemeProvider.tsx +96 -0
- package/src/viewer/components/Toast.tsx +67 -0
- package/src/viewer/components/TokenStylePanel.tsx +708 -0
- package/src/viewer/components/UsageSection.tsx +95 -0
- package/src/viewer/components/VariantMatrix.tsx +350 -0
- package/src/viewer/components/VariantRenderer.tsx +131 -0
- package/src/viewer/components/VariantTabs.tsx +84 -0
- package/src/viewer/components/ViewportSelector.tsx +165 -0
- package/src/viewer/components/_future/CreatePage.tsx +836 -0
- package/src/viewer/composition-renderer.ts +381 -0
- package/src/viewer/constants/index.ts +1 -0
- package/src/viewer/constants/ui.ts +185 -0
- package/src/viewer/entry.tsx +299 -0
- package/src/viewer/hooks/index.ts +2 -0
- package/src/viewer/hooks/useA11yCache.ts +383 -0
- package/src/viewer/hooks/useA11yService.ts +498 -0
- package/src/viewer/hooks/useActions.ts +138 -0
- package/src/viewer/hooks/useAppState.ts +124 -0
- package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
- package/src/viewer/hooks/useHmrStatus.ts +109 -0
- package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
- package/src/viewer/hooks/usePreviewBridge.ts +347 -0
- package/src/viewer/hooks/useScrollSpy.ts +78 -0
- package/src/viewer/hooks/useUrlState.ts +330 -0
- package/src/viewer/hooks/useViewSettings.ts +125 -0
- package/src/viewer/index.html +28 -0
- package/src/viewer/index.ts +14 -0
- package/src/viewer/intelligence/healthReport.ts +505 -0
- package/src/viewer/intelligence/styleDrift.ts +340 -0
- package/src/viewer/intelligence/usageScanner.ts +309 -0
- package/src/viewer/jsx-parser.ts +485 -0
- package/src/viewer/postcss.config.js +6 -0
- package/src/viewer/preview-frame-entry.tsx +25 -0
- package/src/viewer/preview-frame.html +109 -0
- package/src/viewer/render-template.html +68 -0
- package/src/viewer/render-utils.ts +170 -0
- package/src/viewer/server.ts +276 -0
- package/src/viewer/style-utils.ts +414 -0
- package/src/viewer/styles/globals.css +355 -0
- package/src/viewer/tailwind.config.js +37 -0
- package/src/viewer/types/a11y.ts +197 -0
- package/src/viewer/utils/a11y-fixes.ts +471 -0
- package/src/viewer/utils/actionExport.ts +372 -0
- package/src/viewer/utils/colorSchemes.ts +201 -0
- package/src/viewer/utils/detectRelationships.ts +256 -0
- 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, '&')
|
|
324
|
+
.replace(/</g, '<')
|
|
325
|
+
.replace(/>/g, '>')
|
|
326
|
+
.replace(/"/g, '"')
|
|
327
|
+
.replace(/'/g, ''');
|
|
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
|
+
}
|