specweave 1.0.274 → 1.0.276

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 (38) hide show
  1. package/.claude-plugin/README.md +1 -2
  2. package/.claude-plugin/marketplace.json +0 -11
  3. package/dist/src/adapters/agents-md-generator.d.ts.map +1 -1
  4. package/dist/src/adapters/agents-md-generator.js +1 -4
  5. package/dist/src/adapters/agents-md-generator.js.map +1 -1
  6. package/dist/src/adapters/claude-md-generator.js +1 -1
  7. package/dist/src/adapters/claude-md-generator.js.map +1 -1
  8. package/dist/src/core/lazy-loading/llm-plugin-detector.d.ts +2 -2
  9. package/dist/src/core/lazy-loading/llm-plugin-detector.d.ts.map +1 -1
  10. package/dist/src/core/lazy-loading/llm-plugin-detector.js +0 -1
  11. package/dist/src/core/lazy-loading/llm-plugin-detector.js.map +1 -1
  12. package/dist/src/sync/github-reconciler.d.ts +9 -0
  13. package/dist/src/sync/github-reconciler.d.ts.map +1 -1
  14. package/dist/src/sync/github-reconciler.js +41 -9
  15. package/dist/src/sync/github-reconciler.js.map +1 -1
  16. package/dist/src/utils/auto-install.js +1 -1
  17. package/dist/src/utils/auto-install.js.map +1 -1
  18. package/dist/src/utils/generate-skills-index.d.ts.map +1 -1
  19. package/dist/src/utils/generate-skills-index.js +1 -3
  20. package/dist/src/utils/generate-skills-index.js.map +1 -1
  21. package/package.json +1 -1
  22. package/plugins/PLUGINS-INDEX.md +0 -1
  23. package/plugins/specweave/lib/vendor/sync/github-reconciler.d.ts +9 -0
  24. package/plugins/specweave/lib/vendor/sync/github-reconciler.js +41 -9
  25. package/plugins/specweave/lib/vendor/sync/github-reconciler.js.map +1 -1
  26. package/plugins/specweave/skills/npm/SKILL.md +155 -34
  27. package/plugins/specweave-frontend/PLUGIN.md +2 -1
  28. package/plugins/specweave-frontend/skills/design-system-architect/SKILL.md +1 -6
  29. package/plugins/specweave-frontend/skills/figma/SKILL.md +287 -0
  30. package/plugins/specweave-frontend/skills/frontend/SKILL.md +4 -0
  31. package/plugins/specweave-frontend/skills/frontend-architect/SKILL.md +1 -0
  32. package/plugins/specweave-frontend/skills/frontend-design/SKILL.md +1 -1
  33. package/plugins/specweave-release/commands/npm.md +158 -38
  34. package/plugins/specweave-figma/.claude-plugin/plugin.json +0 -23
  35. package/plugins/specweave-figma/PLUGIN.md +0 -34
  36. package/plugins/specweave-figma/commands/figma-import.md +0 -694
  37. package/plugins/specweave-figma/commands/figma-to-react.md +0 -838
  38. package/plugins/specweave-figma/commands/figma-tokens.md +0 -819
@@ -1,838 +0,0 @@
1
- ---
2
- description: Convert Figma components to production-ready React components with TypeScript, styled-components, and responsive design.
3
- ---
4
-
5
- # /sw-figma:to-react
6
-
7
- Convert Figma components to production-ready React components with TypeScript, styled-components, and responsive design.
8
-
9
- You are a Figma-to-React conversion expert who generates pixel-perfect, type-safe React components from Figma designs.
10
-
11
- ## Your Task
12
-
13
- Transform Figma components into React components with proper TypeScript types, styling, accessibility, and responsive behavior.
14
-
15
- ### 1. Conversion Architecture
16
-
17
- **Design-to-Code Pipeline**:
18
- ```
19
- Figma Component → Analyze Structure → Extract Props → Generate TSX → Apply Styles → Add Interactivity
20
- ```
21
-
22
- **Supported Patterns**:
23
- - Functional components with hooks
24
- - TypeScript interfaces for props
25
- - Styled-components or CSS modules
26
- - Responsive design (mobile-first)
27
- - Accessibility attributes (ARIA)
28
- - Component variants (Figma properties)
29
- - Auto-layout → Flexbox/Grid
30
-
31
- ### 2. Component Analysis
32
-
33
- **Figma Node Structure**:
34
- ```typescript
35
- interface FigmaNode {
36
- id: string;
37
- name: string;
38
- type: 'COMPONENT' | 'FRAME' | 'TEXT' | 'RECTANGLE' | 'VECTOR';
39
- children?: FigmaNode[];
40
-
41
- // Layout
42
- absoluteBoundingBox: { x: number; y: number; width: number; height: number };
43
- layoutMode?: 'HORIZONTAL' | 'VERTICAL' | 'NONE';
44
- layoutAlign?: 'MIN' | 'CENTER' | 'MAX' | 'STRETCH';
45
- primaryAxisSizingMode?: 'FIXED' | 'AUTO';
46
- counterAxisSizingMode?: 'FIXED' | 'AUTO';
47
- paddingLeft?: number;
48
- paddingRight?: number;
49
- paddingTop?: number;
50
- paddingBottom?: number;
51
- itemSpacing?: number;
52
-
53
- // Styling
54
- fills?: Fill[];
55
- strokes?: Stroke[];
56
- effects?: Effect[];
57
- cornerRadius?: number;
58
-
59
- // Text
60
- characters?: string;
61
- style?: TextStyle;
62
-
63
- // Component properties (variants)
64
- componentPropertyDefinitions?: Record<string, ComponentProperty>;
65
- }
66
- ```
67
-
68
- **Extract Component Metadata**:
69
- ```typescript
70
- function analyzeComponent(node: FigmaNode) {
71
- return {
72
- name: node.name,
73
- type: inferComponentType(node),
74
- props: extractProps(node),
75
- children: node.children?.map(analyzeComponent) || [],
76
- layout: extractLayout(node),
77
- styling: extractStyling(node),
78
- text: node.type === 'TEXT' ? node.characters : null,
79
- variants: node.componentPropertyDefinitions || {},
80
- };
81
- }
82
-
83
- function inferComponentType(node: FigmaNode): string {
84
- // Button detection
85
- if (node.name.toLowerCase().includes('button')) return 'Button';
86
-
87
- // Input detection
88
- if (node.name.toLowerCase().includes('input')) return 'Input';
89
-
90
- // Card detection
91
- if (node.name.toLowerCase().includes('card')) return 'Card';
92
-
93
- // Icon detection
94
- if (node.type === 'VECTOR') return 'Icon';
95
-
96
- // Text detection
97
- if (node.type === 'TEXT') return 'Text';
98
-
99
- // Generic container
100
- return 'Container';
101
- }
102
- ```
103
-
104
- ### 3. React Component Generation
105
-
106
- **TypeScript Component Template**:
107
-
108
- ```typescript
109
- import { FC } from 'react';
110
- import styled from 'styled-components';
111
-
112
- // Generated interfaces from Figma component properties
113
- interface ${ComponentName}Props {
114
- variant?: 'primary' | 'secondary' | 'tertiary';
115
- size?: 'small' | 'medium' | 'large';
116
- disabled?: boolean;
117
- onClick?: () => void;
118
- children?: React.ReactNode;
119
- }
120
-
121
- const ${ComponentName}: FC<${ComponentName}Props> = ({
122
- variant = 'primary',
123
- size = 'medium',
124
- disabled = false,
125
- onClick,
126
- children,
127
- }) => {
128
- return (
129
- <StyledContainer
130
- variant={variant}
131
- size={size}
132
- disabled={disabled}
133
- onClick={onClick}
134
- >
135
- {children}
136
- </StyledContainer>
137
- );
138
- };
139
-
140
- const StyledContainer = styled.div<${ComponentName}Props>`
141
- /* Generated styles from Figma */
142
- `;
143
-
144
- export default ${ComponentName};
145
- ```
146
-
147
- **Complete Example - Button Component**:
148
-
149
- ```typescript
150
- import { FC, ButtonHTMLAttributes } from 'react';
151
- import styled from 'styled-components';
152
-
153
- interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
154
- variant?: 'primary' | 'secondary' | 'outline';
155
- size?: 'sm' | 'md' | 'lg';
156
- fullWidth?: boolean;
157
- leftIcon?: React.ReactNode;
158
- rightIcon?: React.ReactNode;
159
- loading?: boolean;
160
- }
161
-
162
- const Button: FC<ButtonProps> = ({
163
- variant = 'primary',
164
- size = 'md',
165
- fullWidth = false,
166
- leftIcon,
167
- rightIcon,
168
- loading = false,
169
- disabled,
170
- children,
171
- ...props
172
- }) => {
173
- return (
174
- <StyledButton
175
- variant={variant}
176
- size={size}
177
- fullWidth={fullWidth}
178
- disabled={disabled || loading}
179
- {...props}
180
- >
181
- {leftIcon && <IconWrapper>{leftIcon}</IconWrapper>}
182
- {loading ? <Spinner /> : children}
183
- {rightIcon && <IconWrapper>{rightIcon}</IconWrapper>}
184
- </StyledButton>
185
- );
186
- };
187
-
188
- const StyledButton = styled.button<ButtonProps>`
189
- /* Base styles */
190
- display: inline-flex;
191
- align-items: center;
192
- justify-content: center;
193
- gap: 8px;
194
- font-family: 'Inter', sans-serif;
195
- font-weight: 500;
196
- border: none;
197
- border-radius: 8px;
198
- cursor: pointer;
199
- transition: all 0.2s ease;
200
-
201
- /* Full width */
202
- width: ${({ fullWidth }) => fullWidth ? '100%' : 'auto'};
203
-
204
- /* Size variants */
205
- ${({ size }) => {
206
- switch (size) {
207
- case 'sm':
208
- return `
209
- padding: 8px 16px;
210
- font-size: 14px;
211
- line-height: 20px;
212
- `;
213
- case 'lg':
214
- return `
215
- padding: 16px 32px;
216
- font-size: 18px;
217
- line-height: 24px;
218
- `;
219
- default: // md
220
- return `
221
- padding: 12px 24px;
222
- font-size: 16px;
223
- line-height: 24px;
224
- `;
225
- }
226
- }}
227
-
228
- /* Color variants */
229
- ${({ variant }) => {
230
- switch (variant) {
231
- case 'primary':
232
- return `
233
- background: #0066FF;
234
- color: #FFFFFF;
235
-
236
- &:hover:not(:disabled) {
237
- background: #0052CC;
238
- }
239
-
240
- &:active:not(:disabled) {
241
- background: #003D99;
242
- }
243
- `;
244
- case 'secondary':
245
- return `
246
- background: #F0F0F0;
247
- color: #333333;
248
-
249
- &:hover:not(:disabled) {
250
- background: #E0E0E0;
251
- }
252
-
253
- &:active:not(:disabled) {
254
- background: #D0D0D0;
255
- }
256
- `;
257
- case 'outline':
258
- return `
259
- background: transparent;
260
- color: #0066FF;
261
- border: 2px solid #0066FF;
262
-
263
- &:hover:not(:disabled) {
264
- background: rgba(0, 102, 255, 0.1);
265
- }
266
-
267
- &:active:not(:disabled) {
268
- background: rgba(0, 102, 255, 0.2);
269
- }
270
- `;
271
- }
272
- }}
273
-
274
- /* Disabled state */
275
- &:disabled {
276
- opacity: 0.5;
277
- cursor: not-allowed;
278
- }
279
-
280
- /* Focus state (accessibility) */
281
- &:focus-visible {
282
- outline: 2px solid #0066FF;
283
- outline-offset: 2px;
284
- }
285
- `;
286
-
287
- const IconWrapper = styled.span`
288
- display: inline-flex;
289
- align-items: center;
290
- `;
291
-
292
- const Spinner = styled.div`
293
- width: 16px;
294
- height: 16px;
295
- border: 2px solid currentColor;
296
- border-right-color: transparent;
297
- border-radius: 50%;
298
- animation: spin 0.6s linear infinite;
299
-
300
- @keyframes spin {
301
- to { transform: rotate(360deg); }
302
- }
303
- `;
304
-
305
- export default Button;
306
- ```
307
-
308
- ### 4. Style Conversion
309
-
310
- **Figma → CSS Mapping**:
311
-
312
- ```typescript
313
- function convertFigmaStylesToCSS(node: FigmaNode): string {
314
- const styles: string[] = [];
315
-
316
- // Layout (Auto Layout → Flexbox)
317
- if (node.layoutMode) {
318
- styles.push('display: flex;');
319
- styles.push(`flex-direction: ${node.layoutMode === 'HORIZONTAL' ? 'row' : 'column'};`);
320
-
321
- if (node.layoutAlign) {
322
- const alignMap = {
323
- MIN: 'flex-start',
324
- CENTER: 'center',
325
- MAX: 'flex-end',
326
- STRETCH: 'stretch',
327
- };
328
- styles.push(`align-items: ${alignMap[node.layoutAlign]};`);
329
- }
330
-
331
- if (node.itemSpacing) {
332
- styles.push(`gap: ${node.itemSpacing}px;`);
333
- }
334
- }
335
-
336
- // Padding
337
- if (node.paddingLeft || node.paddingRight || node.paddingTop || node.paddingBottom) {
338
- const padding = [
339
- node.paddingTop || 0,
340
- node.paddingRight || 0,
341
- node.paddingBottom || 0,
342
- node.paddingLeft || 0,
343
- ].join('px ');
344
- styles.push(`padding: ${padding}px;`);
345
- }
346
-
347
- // Size
348
- const box = node.absoluteBoundingBox;
349
- if (node.primaryAxisSizingMode === 'FIXED') {
350
- const dim = node.layoutMode === 'HORIZONTAL' ? 'width' : 'height';
351
- styles.push(`${dim}: ${box.width}px;`);
352
- }
353
-
354
- // Background (fills)
355
- if (node.fills && node.fills.length > 0) {
356
- const fill = node.fills[0];
357
- if (fill.type === 'SOLID') {
358
- const color = rgbaToCSS(fill.color, fill.opacity);
359
- styles.push(`background: ${color};`);
360
- } else if (fill.type === 'GRADIENT_LINEAR') {
361
- const gradient = convertGradient(fill);
362
- styles.push(`background: ${gradient};`);
363
- }
364
- }
365
-
366
- // Border (strokes)
367
- if (node.strokes && node.strokes.length > 0) {
368
- const stroke = node.strokes[0];
369
- const color = rgbaToCSS(stroke.color, stroke.opacity);
370
- const width = node.strokeWeight || 1;
371
- styles.push(`border: ${width}px solid ${color};`);
372
- }
373
-
374
- // Border radius
375
- if (node.cornerRadius) {
376
- styles.push(`border-radius: ${node.cornerRadius}px;`);
377
- }
378
-
379
- // Shadows (effects)
380
- if (node.effects && node.effects.length > 0) {
381
- const shadows = node.effects
382
- .filter((e: any) => e.type === 'DROP_SHADOW')
383
- .map((e: any) => {
384
- const color = rgbaToCSS(e.color, e.color.a);
385
- return `${e.offset.x}px ${e.offset.y}px ${e.radius}px ${color}`;
386
- })
387
- .join(', ');
388
-
389
- if (shadows) {
390
- styles.push(`box-shadow: ${shadows};`);
391
- }
392
- }
393
-
394
- // Typography (text styles)
395
- if (node.style) {
396
- styles.push(`font-family: '${node.style.fontFamily}', sans-serif;`);
397
- styles.push(`font-size: ${node.style.fontSize}px;`);
398
- styles.push(`font-weight: ${node.style.fontWeight};`);
399
- styles.push(`line-height: ${node.style.lineHeightPx}px;`);
400
-
401
- if (node.style.letterSpacing) {
402
- styles.push(`letter-spacing: ${node.style.letterSpacing}px;`);
403
- }
404
-
405
- if (node.style.textAlignHorizontal) {
406
- const alignMap = { LEFT: 'left', CENTER: 'center', RIGHT: 'right', JUSTIFIED: 'justify' };
407
- styles.push(`text-align: ${alignMap[node.style.textAlignHorizontal]};`);
408
- }
409
- }
410
-
411
- return styles.join('\n ');
412
- }
413
-
414
- function rgbaToCSS(color: { r: number; g: number; b: number }, opacity = 1): string {
415
- const r = Math.round(color.r * 255);
416
- const g = Math.round(color.g * 255);
417
- const b = Math.round(color.b * 255);
418
-
419
- return opacity === 1
420
- ? `rgb(${r}, ${g}, ${b})`
421
- : `rgba(${r}, ${g}, ${b}, ${opacity})`;
422
- }
423
-
424
- function convertGradient(fill: any): string {
425
- const stops = fill.gradientStops
426
- .map((stop: any) => {
427
- const color = rgbaToCSS(stop.color, stop.color.a);
428
- return `${color} ${Math.round(stop.position * 100)}%`;
429
- })
430
- .join(', ');
431
-
432
- const angle = Math.atan2(
433
- fill.gradientHandlePositions[1].y - fill.gradientHandlePositions[0].y,
434
- fill.gradientHandlePositions[1].x - fill.gradientHandlePositions[0].x
435
- ) * (180 / Math.PI);
436
-
437
- return `linear-gradient(${angle}deg, ${stops})`;
438
- }
439
- ```
440
-
441
- ### 5. Responsive Design
442
-
443
- **Generate Media Queries from Figma Breakpoints**:
444
-
445
- ```typescript
446
- const breakpoints = {
447
- mobile: 375,
448
- tablet: 768,
449
- desktop: 1440,
450
- };
451
-
452
- const StyledCard = styled.div`
453
- /* Mobile-first base styles */
454
- padding: 16px;
455
- font-size: 14px;
456
-
457
- /* Tablet */
458
- @media (min-width: ${breakpoints.tablet}px) {
459
- padding: 24px;
460
- font-size: 16px;
461
- }
462
-
463
- /* Desktop */
464
- @media (min-width: ${breakpoints.desktop}px) {
465
- padding: 32px;
466
- font-size: 18px;
467
- }
468
- `;
469
- ```
470
-
471
- **Responsive Container**:
472
- ```typescript
473
- const Container = styled.div`
474
- width: 100%;
475
- max-width: 1200px;
476
- margin: 0 auto;
477
- padding: 0 16px;
478
-
479
- @media (min-width: 768px) {
480
- padding: 0 32px;
481
- }
482
-
483
- @media (min-width: 1440px) {
484
- padding: 0 64px;
485
- }
486
- `;
487
- ```
488
-
489
- ### 6. Component Variants (Figma Properties)
490
-
491
- **Figma Component Property → React Props**:
492
-
493
- ```typescript
494
- // Figma component with variants:
495
- // - State: Default, Hover, Active, Disabled
496
- // - Size: Small, Medium, Large
497
- // - Icon: None, Left, Right
498
-
499
- interface ChipProps {
500
- state?: 'default' | 'hover' | 'active' | 'disabled';
501
- size?: 'sm' | 'md' | 'lg';
502
- iconPosition?: 'none' | 'left' | 'right';
503
- label: string;
504
- icon?: React.ReactNode;
505
- onClick?: () => void;
506
- }
507
-
508
- const Chip: FC<ChipProps> = ({
509
- state = 'default',
510
- size = 'md',
511
- iconPosition = 'none',
512
- label,
513
- icon,
514
- onClick,
515
- }) => {
516
- return (
517
- <StyledChip
518
- state={state}
519
- size={size}
520
- onClick={state !== 'disabled' ? onClick : undefined}
521
- >
522
- {iconPosition === 'left' && icon && <IconWrapper>{icon}</IconWrapper>}
523
- <Label>{label}</Label>
524
- {iconPosition === 'right' && icon && <IconWrapper>{icon}</IconWrapper>}
525
- </StyledChip>
526
- );
527
- };
528
-
529
- const StyledChip = styled.div<ChipProps>`
530
- display: inline-flex;
531
- align-items: center;
532
- gap: 8px;
533
- border-radius: 100px;
534
- font-weight: 500;
535
- cursor: ${({ state }) => state === 'disabled' ? 'not-allowed' : 'pointer'};
536
- transition: all 0.2s;
537
-
538
- /* Size variants */
539
- ${({ size }) => {
540
- switch (size) {
541
- case 'sm': return `padding: 4px 12px; font-size: 12px;`;
542
- case 'lg': return `padding: 12px 20px; font-size: 16px;`;
543
- default: return `padding: 8px 16px; font-size: 14px;`;
544
- }
545
- }}
546
-
547
- /* State variants */
548
- ${({ state }) => {
549
- switch (state) {
550
- case 'hover':
551
- return `background: #E0F2FF; color: #0066FF;`;
552
- case 'active':
553
- return `background: #0066FF; color: #FFFFFF;`;
554
- case 'disabled':
555
- return `background: #F0F0F0; color: #A0A0A0; opacity: 0.6;`;
556
- default:
557
- return `background: #F0F0F0; color: #333333;`;
558
- }
559
- }}
560
- `;
561
- ```
562
-
563
- ### 7. Accessibility (a11y)
564
-
565
- **Add ARIA Attributes**:
566
-
567
- ```typescript
568
- const AccessibleButton: FC<ButtonProps> = ({
569
- children,
570
- disabled,
571
- loading,
572
- ariaLabel,
573
- ...props
574
- }) => {
575
- return (
576
- <button
577
- aria-label={ariaLabel || (typeof children === 'string' ? children : undefined)}
578
- aria-disabled={disabled || loading}
579
- aria-busy={loading}
580
- disabled={disabled || loading}
581
- {...props}
582
- >
583
- {children}
584
- </button>
585
- );
586
- };
587
-
588
- // Icon buttons MUST have aria-label
589
- const IconButton: FC<IconButtonProps> = ({ icon, onClick, ariaLabel }) => {
590
- return (
591
- <button
592
- aria-label={ariaLabel} // Required for screen readers
593
- onClick={onClick}
594
- >
595
- {icon}
596
- </button>
597
- );
598
- };
599
- ```
600
-
601
- **Semantic HTML**:
602
- ```typescript
603
- // ❌ Wrong: div soup
604
- <div onClick={onClick}>Submit</div>
605
-
606
- // ✅ Correct: semantic button
607
- <button onClick={onClick}>Submit</button>
608
-
609
- // ❌ Wrong: generic div
610
- <div>Card Title</div>
611
-
612
- // ✅ Correct: heading
613
- <h2>Card Title</h2>
614
- ```
615
-
616
- ### 8. Storybook Integration
617
-
618
- **Generate Storybook Stories**:
619
-
620
- ```typescript
621
- // Button.stories.tsx
622
- import type { Meta, StoryObj } from '@storybook/react';
623
- import Button from './Button';
624
-
625
- const meta: Meta<typeof Button> = {
626
- title: 'Components/Button',
627
- component: Button,
628
- tags: ['autodocs'],
629
- argTypes: {
630
- variant: {
631
- control: 'select',
632
- options: ['primary', 'secondary', 'outline'],
633
- },
634
- size: {
635
- control: 'select',
636
- options: ['sm', 'md', 'lg'],
637
- },
638
- disabled: {
639
- control: 'boolean',
640
- },
641
- loading: {
642
- control: 'boolean',
643
- },
644
- fullWidth: {
645
- control: 'boolean',
646
- },
647
- },
648
- };
649
-
650
- export default meta;
651
- type Story = StoryObj<typeof Button>;
652
-
653
- export const Primary: Story = {
654
- args: {
655
- variant: 'primary',
656
- size: 'md',
657
- children: 'Button',
658
- },
659
- };
660
-
661
- export const Secondary: Story = {
662
- args: {
663
- variant: 'secondary',
664
- size: 'md',
665
- children: 'Button',
666
- },
667
- };
668
-
669
- export const WithIcons: Story = {
670
- args: {
671
- variant: 'primary',
672
- size: 'md',
673
- leftIcon: <IconArrowLeft />,
674
- rightIcon: <IconArrowRight />,
675
- children: 'Button',
676
- },
677
- };
678
-
679
- export const Loading: Story = {
680
- args: {
681
- variant: 'primary',
682
- size: 'md',
683
- loading: true,
684
- children: 'Button',
685
- },
686
- };
687
-
688
- export const Disabled: Story = {
689
- args: {
690
- variant: 'primary',
691
- size: 'md',
692
- disabled: true,
693
- children: 'Button',
694
- },
695
- };
696
- ```
697
-
698
- ### 9. Testing
699
-
700
- **Generate Component Tests**:
701
-
702
- ```typescript
703
- import { render, screen, fireEvent } from '@testing-library/react';
704
- import Button from './Button';
705
-
706
- describe('Button', () => {
707
- it('renders children correctly', () => {
708
- render(<Button>Click me</Button>);
709
- expect(screen.getByText('Click me')).toBeInTheDocument();
710
- });
711
-
712
- it('calls onClick when clicked', () => {
713
- const handleClick = vi.fn();
714
- render(<Button onClick={handleClick}>Click me</Button>);
715
-
716
- fireEvent.click(screen.getByText('Click me'));
717
- expect(handleClick).toHaveBeenCalledTimes(1);
718
- });
719
-
720
- it('does not call onClick when disabled', () => {
721
- const handleClick = vi.fn();
722
- render(<Button disabled onClick={handleClick}>Click me</Button>);
723
-
724
- fireEvent.click(screen.getByText('Click me'));
725
- expect(handleClick).not.toHaveBeenCalled();
726
- });
727
-
728
- it('renders loading state correctly', () => {
729
- render(<Button loading>Click me</Button>);
730
- expect(screen.queryByText('Click me')).not.toBeInTheDocument();
731
- });
732
-
733
- it('renders icons correctly', () => {
734
- render(
735
- <Button leftIcon={<span data-testid="left-icon">←</span>}>
736
- Click me
737
- </Button>
738
- );
739
-
740
- expect(screen.getByTestId('left-icon')).toBeInTheDocument();
741
- });
742
- });
743
- ```
744
-
745
- ### 10. Code Generation Automation
746
-
747
- **Full Pipeline Script**:
748
-
749
- ```typescript
750
- import { FigmaImporter } from './figma-importer';
751
- import { generateReactComponent } from './react-generator';
752
- import fs from 'fs/promises';
753
-
754
- async function figmaToReact(fileKey: string, componentName: string) {
755
- // 1. Fetch component from Figma
756
- const importer = new FigmaImporter({
757
- accessToken: process.env.FIGMA_ACCESS_TOKEN!,
758
- fileKey,
759
- });
760
-
761
- const file = await importer.fetchFile();
762
- const component = findComponentByName(file.document, componentName);
763
-
764
- if (!component) {
765
- throw new Error(`Component "${componentName}" not found`);
766
- }
767
-
768
- // 2. Analyze component structure
769
- const analysis = analyzeComponent(component);
770
-
771
- // 3. Generate React component code
772
- const reactCode = generateReactComponent(analysis);
773
-
774
- // 4. Generate TypeScript types
775
- const types = generateTypeScriptTypes(analysis);
776
-
777
- // 5. Generate styled-components
778
- const styles = generateStyledComponents(analysis);
779
-
780
- // 6. Generate Storybook story
781
- const story = generateStorybook(analysis);
782
-
783
- // 7. Generate tests
784
- const tests = generateTests(analysis);
785
-
786
- // 8. Save files
787
- const componentDir = `./src/components/${analysis.name}`;
788
- await fs.mkdir(componentDir, { recursive: true });
789
-
790
- await fs.writeFile(`${componentDir}/${analysis.name}.tsx`, reactCode);
791
- await fs.writeFile(`${componentDir}/${analysis.name}.types.ts`, types);
792
- await fs.writeFile(`${componentDir}/${analysis.name}.styles.ts`, styles);
793
- await fs.writeFile(`${componentDir}/${analysis.name}.stories.tsx`, story);
794
- await fs.writeFile(`${componentDir}/${analysis.name}.test.tsx`, tests);
795
- await fs.writeFile(`${componentDir}/index.ts`, `export { default } from './${analysis.name}';`);
796
-
797
- console.log(`✅ Generated React component: ${analysis.name}`);
798
- console.log(`📁 Location: ${componentDir}`);
799
- }
800
-
801
- // Usage
802
- figmaToReact('ABC123XYZ456', 'Button').catch(console.error);
803
- ```
804
-
805
- ## Workflow
806
-
807
- 1. Ask about Figma file and component to convert
808
- 2. Fetch component metadata from Figma API
809
- 3. Analyze component structure and variants
810
- 4. Ask about styling approach (styled-components, CSS modules, Tailwind)
811
- 5. Generate TypeScript component with props interface
812
- 6. Convert Figma styles to CSS/styled-components
813
- 7. Add responsive breakpoints if needed
814
- 8. Generate Storybook stories for all variants
815
- 9. Generate unit tests
816
- 10. Save all generated files and provide usage examples
817
-
818
- ## When to Use
819
-
820
- - Converting Figma designs to React components
821
- - Building design systems from Figma
822
- - Automating component creation from mockups
823
- - Ensuring pixel-perfect implementation
824
- - Syncing Figma variants with React props
825
- - Generating Storybook documentation from designs
826
-
827
- ## Best Practices
828
-
829
- 1. **Type Safety**: Always generate TypeScript interfaces
830
- 2. **Variants**: Map Figma component properties to React props
831
- 3. **Accessibility**: Include ARIA attributes and semantic HTML
832
- 4. **Responsive**: Generate mobile-first responsive styles
833
- 5. **Testing**: Create comprehensive unit tests
834
- 6. **Documentation**: Generate Storybook stories automatically
835
- 7. **Naming**: Use PascalCase for components, match Figma names
836
- 8. **Optimization**: Extract common styles to theme tokens
837
-
838
- Transform Figma designs into production-ready React components!