@fragments-sdk/cli 0.7.3 → 0.7.5
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 +1 -4
- package/README.md +2 -0
- package/dist/bin.js +39 -16
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-D34Q6A7S.js → chunk-AWYCDRPG.js} +8 -2
- package/dist/chunk-AWYCDRPG.js.map +1 -0
- package/dist/{chunk-R2YH7NLN.js → chunk-CR3XHBGM.js} +3 -3
- package/dist/{chunk-QPY4DUFB.js → chunk-EFQ7SIBX.js} +583 -108
- package/dist/chunk-EFQ7SIBX.js.map +1 -0
- package/dist/{chunk-UXLGIGSX.js → chunk-GIC3I2KZ.js} +2 -2
- package/dist/{chunk-R6IZZSE7.js → chunk-JZNATKQA.js} +9 -3
- package/dist/chunk-JZNATKQA.js.map +1 -0
- package/dist/{chunk-P33AKQJW.js → chunk-SFWZ4K7C.js} +8 -2
- package/dist/{chunk-P33AKQJW.js.map → chunk-SFWZ4K7C.js.map} +1 -1
- package/dist/{core-3NMNCLFW.js → core-T7BDYEGO.js} +3 -3
- package/dist/{discovery-AKGA6CJD.js → discovery-Z4RDDFVR.js} +2 -2
- package/dist/{generate-JAUEHKK7.js → generate-C2DKFCFJ.js} +5 -5
- package/dist/index.d.ts +28 -2
- package/dist/index.js +9 -7
- package/dist/index.js.map +1 -1
- package/dist/{init-DZQOT54X.js → init-O3FCHEPN.js} +26 -8
- package/dist/init-O3FCHEPN.js.map +1 -0
- package/dist/mcp-bin.js +3 -3
- package/dist/{scan-OJRCVKK2.js → scan-IYTZDUKG.js} +6 -6
- package/dist/{service-CFFBHW4X.js → service-VA6XKADO.js} +3 -3
- package/dist/{static-viewer-VA2JXSCX.js → static-viewer-5N42MBDR.js} +3 -3
- package/dist/{test-O7DZNKDC.js → test-OMMDWL2W.js} +4 -4
- package/dist/{tokens-N7THFD6J.js → tokens-6VJAHFIG.js} +5 -5
- package/dist/{viewer-QTR7QJMM.js → viewer-IVP5XC7U.js} +37 -17
- package/dist/viewer-IVP5XC7U.js.map +1 -0
- package/package.json +8 -2
- package/src/bin.ts +4 -0
- package/src/commands/add.ts +6 -0
- package/src/commands/init.ts +24 -4
- package/src/commands/validate.ts +24 -2
- package/src/core/config.ts +6 -0
- package/src/core/discovery.ts +7 -1
- package/src/core/index.ts +1 -0
- package/src/core/schema.ts +6 -0
- package/src/core/types.ts +21 -0
- package/src/index.ts +2 -1
- package/src/migrate/detect.ts +4 -0
- package/src/service/snippet-validation.test.ts +209 -0
- package/src/service/snippet-validation.ts +635 -0
- package/src/validators.ts +53 -5
- package/src/viewer/__tests__/viewer-integration.test.ts +8 -0
- package/src/viewer/components/App.tsx +63 -2
- package/src/viewer/components/CodePanel.naming.test.tsx +60 -0
- package/src/viewer/components/CodePanel.tsx +76 -468
- package/src/viewer/components/Layout.tsx +2 -2
- package/src/viewer/components/LeftSidebar.tsx +35 -77
- package/src/viewer/preview-frame.html +1 -1
- package/src/viewer/styles/globals.css +2 -1
- package/src/viewer/utils/a11y-fixes.ts +24 -9
- package/src/viewer/vite-plugin.ts +27 -2
- package/dist/chunk-D34Q6A7S.js.map +0 -1
- package/dist/chunk-QPY4DUFB.js.map +0 -1
- package/dist/chunk-R6IZZSE7.js.map +0 -1
- package/dist/init-DZQOT54X.js.map +0 -1
- package/dist/viewer-QTR7QJMM.js.map +0 -1
- /package/dist/{chunk-R2YH7NLN.js.map → chunk-CR3XHBGM.js.map} +0 -0
- /package/dist/{chunk-UXLGIGSX.js.map → chunk-GIC3I2KZ.js.map} +0 -0
- /package/dist/{core-3NMNCLFW.js.map → core-T7BDYEGO.js.map} +0 -0
- /package/dist/{discovery-AKGA6CJD.js.map → discovery-Z4RDDFVR.js.map} +0 -0
- /package/dist/{generate-JAUEHKK7.js.map → generate-C2DKFCFJ.js.map} +0 -0
- /package/dist/{scan-OJRCVKK2.js.map → scan-IYTZDUKG.js.map} +0 -0
- /package/dist/{service-CFFBHW4X.js.map → service-VA6XKADO.js.map} +0 -0
- /package/dist/{static-viewer-VA2JXSCX.js.map → static-viewer-5N42MBDR.js.map} +0 -0
- /package/dist/{test-O7DZNKDC.js.map → test-OMMDWL2W.js.map} +0 -0
- /package/dist/{tokens-N7THFD6J.js.map → tokens-6VJAHFIG.js.map} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useMemo
|
|
2
|
-
import type { FragmentVariant, PropDefinition } from '../../core/index.js';
|
|
1
|
+
import { useMemo } from 'react';
|
|
3
2
|
import { CodeBlock } from '@fragments/ui';
|
|
3
|
+
import type { FragmentVariant, PropDefinition } from '../../core/index.js';
|
|
4
4
|
|
|
5
5
|
interface CodePanelProps {
|
|
6
6
|
variant: FragmentVariant;
|
|
@@ -9,238 +9,107 @@ interface CodePanelProps {
|
|
|
9
9
|
propDefs?: Record<string, PropDefinition>;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// Check if this is the expected component (by displayName or name)
|
|
19
|
-
const elementType = rendered.type;
|
|
20
|
-
const typeName = typeof elementType === 'function'
|
|
21
|
-
? (elementType as { displayName?: string; name?: string }).displayName || (elementType as { name?: string }).name
|
|
22
|
-
: typeof elementType === 'string' ? elementType : null;
|
|
23
|
-
|
|
24
|
-
// Only extract if it's a single element of the expected component type
|
|
25
|
-
if (typeName && typeName.toLowerCase() === componentName.toLowerCase()) {
|
|
26
|
-
return rendered.props as Record<string, unknown>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// If it's a wrapper element, try to find the component in children
|
|
30
|
-
const props = rendered.props as { children?: ReactNode };
|
|
31
|
-
if (props.children && isValidElement(props.children)) {
|
|
32
|
-
const childType = props.children.type;
|
|
33
|
-
const childTypeName = typeof childType === 'function'
|
|
34
|
-
? (childType as { displayName?: string; name?: string }).displayName || (childType as { name?: string }).name
|
|
35
|
-
: null;
|
|
36
|
-
if (childTypeName && childTypeName.toLowerCase() === componentName.toLowerCase()) {
|
|
37
|
-
return props.children.props as Record<string, unknown>;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return null;
|
|
42
|
-
} catch {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
12
|
+
function quoteString(value: string): string {
|
|
13
|
+
return `'${value
|
|
14
|
+
.replace(/\\/g, '\\\\')
|
|
15
|
+
.replace(/'/g, "\\'")
|
|
16
|
+
.replace(/\n/g, '\\n')
|
|
17
|
+
}'`;
|
|
45
18
|
}
|
|
46
19
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const lowerName = componentName.toLowerCase();
|
|
20
|
+
function serializeValue(value: unknown): string | null {
|
|
21
|
+
if (value === null) return '{null}';
|
|
22
|
+
if (value === undefined) return null;
|
|
51
23
|
|
|
52
|
-
if (
|
|
53
|
-
return
|
|
24
|
+
if (typeof value === 'string') {
|
|
25
|
+
return quoteString(value);
|
|
54
26
|
}
|
|
55
27
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const hasChangeHandler = Object.keys(propDefs).some(
|
|
59
|
-
key => key === 'onChange' || key === 'onChecked' || key === 'onValueChange'
|
|
60
|
-
);
|
|
61
|
-
const hasValueProp = Object.keys(propDefs).some(
|
|
62
|
-
key => key === 'value' || key === 'checked'
|
|
63
|
-
);
|
|
64
|
-
return hasChangeHandler && hasValueProp;
|
|
28
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
29
|
+
return `{${String(value)}}`;
|
|
65
30
|
}
|
|
66
31
|
|
|
67
|
-
|
|
68
|
-
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
const items = value
|
|
34
|
+
.map((item) => serializeValue(item))
|
|
35
|
+
.filter((item): item is string => item !== null)
|
|
36
|
+
.map((item) => (item.startsWith('{') && item.endsWith('}') ? item.slice(1, -1) : item));
|
|
69
37
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
* Handles: jsxDEV(Component, { props, children }, ...) -> <Component props>children</Component>
|
|
73
|
-
*/
|
|
74
|
-
function decompileJsxDev(code: string, indent = 0): string {
|
|
75
|
-
const indentStr = ' '.repeat(indent);
|
|
76
|
-
|
|
77
|
-
// Match jsxDEV or jsx call: jsxDEV(Component, {props}, ...)
|
|
78
|
-
// Also handle _jsxDEV, /* @__PURE__ */ prefix
|
|
79
|
-
const jsxMatch = code.match(/(?:\/\*\s*@__PURE__\s*\*\/\s*)?_?jsxs?(?:DEV)?\s*\(\s*([^,]+)\s*,\s*(\{[\s\S]*\})\s*(?:,[\s\S]*)?\)$/);
|
|
80
|
-
|
|
81
|
-
if (!jsxMatch) {
|
|
82
|
-
// Not a jsxDEV call - might be a plain string or primitive
|
|
83
|
-
const trimmed = code.trim();
|
|
84
|
-
// If it's a quoted string, return the content
|
|
85
|
-
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
86
|
-
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
87
|
-
return trimmed.slice(1, -1);
|
|
88
|
-
}
|
|
89
|
-
return trimmed;
|
|
38
|
+
if (items.length === 0) return '{[]}';
|
|
39
|
+
return `{[${items.join(', ')}]}`;
|
|
90
40
|
}
|
|
91
41
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// Clean up component name (remove quotes for HTML elements)
|
|
96
|
-
if (componentName.startsWith('"') || componentName.startsWith("'")) {
|
|
97
|
-
componentName = componentName.slice(1, -1);
|
|
42
|
+
if (typeof value === 'object') {
|
|
43
|
+
return '{/* complex value */}';
|
|
98
44
|
}
|
|
99
45
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
let children: string | null = null;
|
|
103
|
-
|
|
104
|
-
// Debug props that should be excluded (added by jsxDEV in development)
|
|
105
|
-
const debugProps = new Set(['fileName', 'lineNumber', 'columnNumber', '__source', '__self']);
|
|
106
|
-
|
|
107
|
-
// Simple prop extraction (handles most common cases)
|
|
108
|
-
// Match: key: value, key: "string", key: number, key: boolean
|
|
109
|
-
const propMatches = propsStr.matchAll(/(\w+)\s*:\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[\w.]+|\{[^}]*\}|(?:\/\*[\s\S]*?\*\/\s*)?_?jsxs?(?:DEV)?\s*\([^)]*(?:\([^)]*\)[^)]*)*\))/g);
|
|
110
|
-
|
|
111
|
-
for (const match of propMatches) {
|
|
112
|
-
const key = match[1];
|
|
113
|
-
const value = match[2];
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
114
48
|
|
|
115
|
-
|
|
116
|
-
|
|
49
|
+
function toSingleLine(code: string): string {
|
|
50
|
+
return code
|
|
51
|
+
.split('\n')
|
|
52
|
+
.map((line) => line.trim())
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.join(' ')
|
|
55
|
+
.trim();
|
|
56
|
+
}
|
|
117
57
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
58
|
+
function buildFallbackSnippet(componentName: string, variant: FragmentVariant): string {
|
|
59
|
+
const args = variant.args ?? {};
|
|
60
|
+
const propEntries = Object.entries(args)
|
|
61
|
+
.filter(([name, value]) => name !== 'children' && typeof value !== 'function')
|
|
62
|
+
.map(([name, value]) => {
|
|
63
|
+
const serialized = serializeValue(value);
|
|
64
|
+
return serialized ? `${name}=${serialized}` : null;
|
|
65
|
+
})
|
|
66
|
+
.filter((entry): entry is string => entry !== null);
|
|
67
|
+
|
|
68
|
+
const children = typeof args.children === 'string' ? args.children : null;
|
|
69
|
+
const openTag = propEntries.length > 0
|
|
70
|
+
? `<${componentName} ${propEntries.join(' ')}>`
|
|
71
|
+
: `<${componentName}>`;
|
|
72
|
+
|
|
73
|
+
const usage = children
|
|
74
|
+
? `${openTag}${children}</${componentName}>`
|
|
75
|
+
: toSingleLine(`${openTag}</${componentName}>`);
|
|
76
|
+
|
|
77
|
+
return `import { ${componentName} } from '@/components/${componentName}';
|
|
78
|
+
|
|
79
|
+
// TODO: Add explicit \`code\` for variant "${variant.name}" in this fragment file.
|
|
80
|
+
${usage}`;
|
|
81
|
+
}
|
|
142
82
|
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// Convert camelCase event handlers back
|
|
148
|
-
if (v.startsWith('"') || v.startsWith("'")) {
|
|
149
|
-
return `${k}=${v}`;
|
|
150
|
-
}
|
|
151
|
-
return `${k}=${v}`;
|
|
152
|
-
});
|
|
83
|
+
function normalizeAuthoredSnippet(code: string): string {
|
|
84
|
+
const normalized = code.replace(/\r\n/g, '\n').trim();
|
|
85
|
+
return normalized.length > 0 ? normalized : '';
|
|
86
|
+
}
|
|
153
87
|
|
|
154
|
-
|
|
88
|
+
export function __buildFallbackSnippetForTest(componentName: string, variant: FragmentVariant): string {
|
|
89
|
+
return buildFallbackSnippet(componentName, variant);
|
|
90
|
+
}
|
|
155
91
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
92
|
+
function resolveDisplayedCode(componentName: string, variant: FragmentVariant): string {
|
|
93
|
+
if (typeof variant.code === 'string') {
|
|
94
|
+
const normalized = normalizeAuthoredSnippet(variant.code);
|
|
95
|
+
if (normalized.length > 0) {
|
|
96
|
+
return normalized;
|
|
159
97
|
}
|
|
160
|
-
return `${indentStr}<${componentName}${propsJsx}>${children}</${componentName}>`;
|
|
161
98
|
}
|
|
162
99
|
|
|
163
|
-
return
|
|
100
|
+
return buildFallbackSnippet(componentName, variant);
|
|
164
101
|
}
|
|
165
102
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
* Works with arrow functions like: () => <Component /> or () => (<div>...</div>)
|
|
169
|
-
* Also handles compiled jsxDEV calls and converts them back to JSX.
|
|
170
|
-
*/
|
|
171
|
-
function extractRenderBody(renderFn: () => ReactNode): string | null {
|
|
172
|
-
try {
|
|
173
|
-
const source = renderFn.toString();
|
|
174
|
-
|
|
175
|
-
// Match arrow function body: () => <...> or () => (...)
|
|
176
|
-
// Handle both: () => <X /> and () => (\n <X />\n)
|
|
177
|
-
const arrowMatch = source.match(/^\s*\(\s*\)\s*=>\s*(.+)$/s);
|
|
178
|
-
if (arrowMatch) {
|
|
179
|
-
let body = arrowMatch[1].trim();
|
|
180
|
-
|
|
181
|
-
// Remove outer parentheses if present
|
|
182
|
-
if (body.startsWith('(') && body.endsWith(')')) {
|
|
183
|
-
body = body.slice(1, -1).trim();
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Check if this is compiled JSX (jsxDEV calls)
|
|
187
|
-
if (body.includes('jsxDEV') || body.includes('jsx(') || body.includes('_jsx')) {
|
|
188
|
-
return decompileJsxDev(body);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return body;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Match function body with return statement
|
|
195
|
-
const returnMatch = source.match(/return\s*\(\s*([\s\S]*?)\s*\)\s*;?\s*\}?\s*$/);
|
|
196
|
-
if (returnMatch) {
|
|
197
|
-
const body = returnMatch[1].trim();
|
|
198
|
-
|
|
199
|
-
// Check if this is compiled JSX
|
|
200
|
-
if (body.includes('jsxDEV') || body.includes('jsx(') || body.includes('_jsx')) {
|
|
201
|
-
return decompileJsxDev(body);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return body;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return null;
|
|
208
|
-
} catch {
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
103
|
+
export function __resolveDisplayedCodeForTest(componentName: string, variant: FragmentVariant): string {
|
|
104
|
+
return resolveDisplayedCode(componentName, variant);
|
|
211
105
|
}
|
|
212
106
|
|
|
213
|
-
export function CodePanel({ variant, componentName, compact = false
|
|
214
|
-
|
|
215
|
-
const generatedCode = useMemo(() => {
|
|
216
|
-
// Priority 1: Use variant.code if available (from compiled JSON/AST)
|
|
217
|
-
if (variant.code) {
|
|
218
|
-
return generateFullExample(componentName, variant.code);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Priority 2: Extract source from render function (runtime)
|
|
222
|
-
if (variant.render) {
|
|
223
|
-
const renderBody = extractRenderBody(variant.render);
|
|
224
|
-
if (renderBody) {
|
|
225
|
-
return generateFullExample(componentName, renderBody);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Priority 3: Generate from args (fallback for edge cases)
|
|
230
|
-
const needsState = needsStatefulExample(componentName, propDefs);
|
|
231
|
-
let effectiveArgs = variant.args;
|
|
232
|
-
|
|
233
|
-
// If no args on variant, try to extract from rendered element
|
|
234
|
-
if (!effectiveArgs || Object.keys(effectiveArgs).length === 0) {
|
|
235
|
-
effectiveArgs = extractPropsFromRender(variant, componentName) ?? undefined;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return generateCombinedCode(componentName, propDefs, effectiveArgs, needsState);
|
|
239
|
-
}, [componentName, variant, propDefs]);
|
|
107
|
+
export function CodePanel({ variant, componentName, compact = false }: CodePanelProps) {
|
|
108
|
+
const code = useMemo(() => resolveDisplayedCode(componentName, variant), [componentName, variant]);
|
|
240
109
|
|
|
241
110
|
return (
|
|
242
111
|
<CodeBlock
|
|
243
|
-
code={
|
|
112
|
+
code={code}
|
|
244
113
|
language="tsx"
|
|
245
114
|
showCopy
|
|
246
115
|
maxHeight={400}
|
|
@@ -248,264 +117,3 @@ export function CodePanel({ variant, componentName, compact = false, propDefs }:
|
|
|
248
117
|
/>
|
|
249
118
|
);
|
|
250
119
|
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Normalize indentation by removing common leading whitespace from all lines.
|
|
254
|
-
* Handles JSX where first line may be at column 0 but inner content is indented.
|
|
255
|
-
*/
|
|
256
|
-
function normalizeIndentation(code: string): string {
|
|
257
|
-
const lines = code.split('\n');
|
|
258
|
-
if (lines.length <= 1) return code;
|
|
259
|
-
|
|
260
|
-
// Find minimum indentation (ignoring empty lines and first line)
|
|
261
|
-
// First line often has 0 indent, but inner content is over-indented
|
|
262
|
-
let minIndent = Infinity;
|
|
263
|
-
const firstLineIndent = lines[0].match(/^(\s*)/)?.[1].length ?? 0;
|
|
264
|
-
|
|
265
|
-
for (let i = 1; i < lines.length; i++) {
|
|
266
|
-
const line = lines[i];
|
|
267
|
-
if (line.trim().length === 0) continue;
|
|
268
|
-
const indent = line.match(/^(\s*)/)?.[1].length ?? 0;
|
|
269
|
-
minIndent = Math.min(minIndent, indent);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// If first line has indent, include it in calculation
|
|
273
|
-
if (firstLineIndent > 0) {
|
|
274
|
-
minIndent = Math.min(minIndent, firstLineIndent);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (minIndent === Infinity || minIndent === 0) return code;
|
|
278
|
-
|
|
279
|
-
// Remove the common indentation from all lines
|
|
280
|
-
return lines
|
|
281
|
-
.map(line => line.slice(Math.min(minIndent, line.match(/^(\s*)/)?.[1].length ?? 0)))
|
|
282
|
-
.join('\n');
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Generate a full example from the variant's code string
|
|
286
|
-
// Shows the ACTUAL code from the fragment file - no transformations
|
|
287
|
-
function generateFullExample(
|
|
288
|
-
componentName: string,
|
|
289
|
-
variantCode: string
|
|
290
|
-
): string {
|
|
291
|
-
// Normalize indentation first
|
|
292
|
-
const normalizedCode = normalizeIndentation(variantCode);
|
|
293
|
-
|
|
294
|
-
// Add component import
|
|
295
|
-
const imports = [`import { ${componentName} } from '@/components/${componentName}';`];
|
|
296
|
-
|
|
297
|
-
// Check if the code uses StatefulXxx wrapper - if so, include that helper
|
|
298
|
-
const statefulPattern = new RegExp(`<Stateful${componentName}`, 'i');
|
|
299
|
-
const usesStatefulWrapper = statefulPattern.test(normalizedCode);
|
|
300
|
-
|
|
301
|
-
if (usesStatefulWrapper) {
|
|
302
|
-
imports.unshift(`import { useState } from 'react';`);
|
|
303
|
-
|
|
304
|
-
// Determine the state type based on component
|
|
305
|
-
const lowerName = componentName.toLowerCase();
|
|
306
|
-
const isToggleLike = lowerName.includes('toggle') || lowerName.includes('checkbox') || lowerName.includes('switch');
|
|
307
|
-
|
|
308
|
-
const stateType = isToggleLike ? 'boolean' : 'string';
|
|
309
|
-
const defaultValue = isToggleLike ? 'false' : "''";
|
|
310
|
-
const stateName = isToggleLike ? 'checked' : 'value';
|
|
311
|
-
const setterName = isToggleLike ? 'setChecked' : 'setValue';
|
|
312
|
-
const valueProp = isToggleLike ? 'checked' : 'value';
|
|
313
|
-
const handlerProp = 'onChange';
|
|
314
|
-
|
|
315
|
-
// Generate the stateful wrapper helper that the code uses
|
|
316
|
-
const helperCode = `
|
|
317
|
-
// Stateful wrapper for interactive demos
|
|
318
|
-
function Stateful${componentName}(props: React.ComponentProps<typeof ${componentName}>) {
|
|
319
|
-
const [${stateName}, ${setterName}] = useState(props.${valueProp} ?? ${defaultValue});
|
|
320
|
-
return <${componentName} {...props} ${valueProp}={${stateName}} ${handlerProp}={${setterName}} />;
|
|
321
|
-
}`;
|
|
322
|
-
|
|
323
|
-
return `${imports.join('\n')}
|
|
324
|
-
${helperCode}
|
|
325
|
-
|
|
326
|
-
// Example usage:
|
|
327
|
-
${normalizedCode}`;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// For non-stateful code, just show the raw code with import
|
|
331
|
-
return `${imports.join('\n')}
|
|
332
|
-
|
|
333
|
-
${normalizedCode}`;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Generate combined import statement + JSX usage
|
|
337
|
-
function generateCombinedCode(
|
|
338
|
-
componentName: string,
|
|
339
|
-
propDefs?: Record<string, PropDefinition>,
|
|
340
|
-
variantArgs?: Record<string, unknown>,
|
|
341
|
-
needsState?: boolean
|
|
342
|
-
): string {
|
|
343
|
-
const imports: string[] = [];
|
|
344
|
-
const hooks: string[] = [];
|
|
345
|
-
|
|
346
|
-
if (needsState) {
|
|
347
|
-
imports.push(`import { useState } from 'react';`);
|
|
348
|
-
}
|
|
349
|
-
imports.push(`import { ${componentName} } from '@/components/${componentName}';`);
|
|
350
|
-
|
|
351
|
-
// Determine state props that will be managed by hooks (to avoid duplicates)
|
|
352
|
-
const lowerName = componentName.toLowerCase();
|
|
353
|
-
const isToggleLike = lowerName.includes('toggle') || lowerName.includes('checkbox') || lowerName.includes('switch');
|
|
354
|
-
const statePropsToSkip = needsState
|
|
355
|
-
? isToggleLike
|
|
356
|
-
? ['checked', 'onChange', 'onCheckedChange']
|
|
357
|
-
: ['value', 'onChange', 'onValueChange']
|
|
358
|
-
: [];
|
|
359
|
-
|
|
360
|
-
// Build props string - prefer actual variant args over prop definitions
|
|
361
|
-
const propsEntries: string[] = [];
|
|
362
|
-
let childrenValue: unknown = undefined;
|
|
363
|
-
|
|
364
|
-
if (variantArgs && Object.keys(variantArgs).length > 0) {
|
|
365
|
-
// Use actual variant args for accurate code generation
|
|
366
|
-
for (const [name, value] of Object.entries(variantArgs)) {
|
|
367
|
-
if (value === undefined) continue;
|
|
368
|
-
|
|
369
|
-
// Handle children specially - will be rendered as element content
|
|
370
|
-
if (name === 'children') {
|
|
371
|
-
childrenValue = value;
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Skip function props (callbacks) - they're runtime handlers
|
|
376
|
-
if (typeof value === 'function') continue;
|
|
377
|
-
|
|
378
|
-
// Skip props that will be managed by state hooks
|
|
379
|
-
if (statePropsToSkip.includes(name)) continue;
|
|
380
|
-
|
|
381
|
-
const propType = propDefs?.[name]?.type || typeof value;
|
|
382
|
-
const formattedValue = formatPropValue(value, propType);
|
|
383
|
-
propsEntries.push(`${name}=${formattedValue}`);
|
|
384
|
-
}
|
|
385
|
-
} else if (propDefs) {
|
|
386
|
-
// Fallback to prop definitions if no variant args
|
|
387
|
-
for (const [name, def] of Object.entries(propDefs)) {
|
|
388
|
-
if (name === 'children') {
|
|
389
|
-
if (def.default !== undefined) {
|
|
390
|
-
childrenValue = def.default;
|
|
391
|
-
}
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Skip props that will be managed by state hooks
|
|
396
|
-
if (statePropsToSkip.includes(name)) continue;
|
|
397
|
-
|
|
398
|
-
if (def.required && def.default !== undefined) {
|
|
399
|
-
const value = formatPropValue(def.default, def.type);
|
|
400
|
-
propsEntries.push(`${name}=${value}`);
|
|
401
|
-
} else if (def.required) {
|
|
402
|
-
const placeholder = getTypePlaceholder(name, def);
|
|
403
|
-
propsEntries.push(`${name}=${placeholder}`);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Build JSX code
|
|
409
|
-
const hasChildren = childrenValue !== undefined && childrenValue !== null;
|
|
410
|
-
const childrenStr = hasChildren ? formatChildrenValue(childrenValue) : null;
|
|
411
|
-
|
|
412
|
-
// For stateful components, wrap in a function component
|
|
413
|
-
if (needsState) {
|
|
414
|
-
let stateName = 'value';
|
|
415
|
-
let setterName = 'setValue';
|
|
416
|
-
let handlerProp = 'onChange';
|
|
417
|
-
let valueProp = 'value';
|
|
418
|
-
let defaultValue = "''";
|
|
419
|
-
let stateType = 'string';
|
|
420
|
-
|
|
421
|
-
if (isToggleLike) {
|
|
422
|
-
stateType = 'boolean';
|
|
423
|
-
defaultValue = 'false';
|
|
424
|
-
stateName = 'checked';
|
|
425
|
-
setterName = 'setChecked';
|
|
426
|
-
handlerProp = propDefs?.['onCheckedChange'] ? 'onCheckedChange' : 'onChange';
|
|
427
|
-
valueProp = 'checked';
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
hooks.push(`const [${stateName}, ${setterName}] = useState<${stateType}>(${defaultValue});`);
|
|
431
|
-
|
|
432
|
-
// Add state props
|
|
433
|
-
propsEntries.push(`${valueProp}={${stateName}}`);
|
|
434
|
-
propsEntries.push(`${handlerProp}={${setterName}}`);
|
|
435
|
-
|
|
436
|
-
const propsStr = propsEntries.length > 0
|
|
437
|
-
? `\n ${propsEntries.join('\n ')}\n `
|
|
438
|
-
: ' ';
|
|
439
|
-
|
|
440
|
-
const jsxCode = hasChildren
|
|
441
|
-
? `<${componentName}${propsStr}>${childrenStr}</${componentName}>`
|
|
442
|
-
: `<${componentName}${propsStr}/>`;
|
|
443
|
-
|
|
444
|
-
return `${imports.join('\n')}
|
|
445
|
-
|
|
446
|
-
function Example() {
|
|
447
|
-
${hooks.join('\n ')}
|
|
448
|
-
|
|
449
|
-
return (
|
|
450
|
-
${jsxCode}
|
|
451
|
-
);
|
|
452
|
-
}`;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Simple non-stateful code
|
|
456
|
-
let jsxCode: string;
|
|
457
|
-
|
|
458
|
-
if (propsEntries.length === 0 && !hasChildren) {
|
|
459
|
-
jsxCode = `<${componentName} />`;
|
|
460
|
-
} else if (propsEntries.length === 0 && hasChildren) {
|
|
461
|
-
jsxCode = `<${componentName}>${childrenStr}</${componentName}>`;
|
|
462
|
-
} else if (propsEntries.length === 1 && !hasChildren) {
|
|
463
|
-
jsxCode = `<${componentName} ${propsEntries[0]} />`;
|
|
464
|
-
} else if (propsEntries.length === 1 && hasChildren) {
|
|
465
|
-
jsxCode = `<${componentName} ${propsEntries[0]}>\n ${childrenStr}\n</${componentName}>`;
|
|
466
|
-
} else if (!hasChildren) {
|
|
467
|
-
jsxCode = `<${componentName}\n ${propsEntries.join('\n ')}\n/>`;
|
|
468
|
-
} else {
|
|
469
|
-
jsxCode = `<${componentName}\n ${propsEntries.join('\n ')}\n>\n ${childrenStr}\n</${componentName}>`;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
return `${imports.join('\n')}
|
|
473
|
-
|
|
474
|
-
${jsxCode}`;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function formatChildrenValue(value: unknown): string {
|
|
478
|
-
if (typeof value === 'string') {
|
|
479
|
-
return value;
|
|
480
|
-
}
|
|
481
|
-
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
482
|
-
return `{${value}}`;
|
|
483
|
-
}
|
|
484
|
-
return '{/* children */}';
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function formatPropValue(value: unknown, type: string): string {
|
|
488
|
-
if (type === 'string' || typeof value === 'string') {
|
|
489
|
-
return `"${value}"`;
|
|
490
|
-
}
|
|
491
|
-
if (type === 'boolean' || typeof value === 'boolean') {
|
|
492
|
-
return `{${value}}`;
|
|
493
|
-
}
|
|
494
|
-
if (type === 'number' || typeof value === 'number') {
|
|
495
|
-
return `{${value}}`;
|
|
496
|
-
}
|
|
497
|
-
return `{${JSON.stringify(value)}}`;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function getTypePlaceholder(name: string, def: PropDefinition): string {
|
|
501
|
-
if (def.values && def.values.length > 0) {
|
|
502
|
-
return `"${def.values[0]}"`;
|
|
503
|
-
}
|
|
504
|
-
switch (def.type) {
|
|
505
|
-
case 'string': return `"${name}"`;
|
|
506
|
-
case 'boolean': return '{true}';
|
|
507
|
-
case 'number': return '{0}';
|
|
508
|
-
case 'function': return '{() => {}}';
|
|
509
|
-
default: return '{undefined}';
|
|
510
|
-
}
|
|
511
|
-
}
|
|
@@ -9,11 +9,11 @@ interface LayoutProps {
|
|
|
9
9
|
|
|
10
10
|
export function Layout({ leftSidebar, header, children }: LayoutProps) {
|
|
11
11
|
return (
|
|
12
|
-
<AppShell
|
|
12
|
+
<AppShell>
|
|
13
13
|
<AppShell.Header>
|
|
14
14
|
{header}
|
|
15
15
|
</AppShell.Header>
|
|
16
|
-
<AppShell.Sidebar width="
|
|
16
|
+
<AppShell.Sidebar width="260px" collapsible="icon">
|
|
17
17
|
{leftSidebar}
|
|
18
18
|
</AppShell.Sidebar>
|
|
19
19
|
<AppShell.Main padding="none">
|