inkhouse 0.1.0-beta.0

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 (62) hide show
  1. package/README.md +201 -0
  2. package/bin/inkhouse.mjs +171 -0
  3. package/code.js +11802 -0
  4. package/manifest.json +30 -0
  5. package/package.json +45 -0
  6. package/scanner/blob-placement-regression.ts +132 -0
  7. package/scanner/class-collector.ts +69 -0
  8. package/scanner/cli.ts +336 -0
  9. package/scanner/component-scanner.ts +2876 -0
  10. package/scanner/css-patch-regression.ts +112 -0
  11. package/scanner/css-token-reader-regression.ts +92 -0
  12. package/scanner/css-token-reader.ts +477 -0
  13. package/scanner/font-style-resolver-regression.ts +32 -0
  14. package/scanner/index.ts +9 -0
  15. package/scanner/radial-gradient-regression.ts +53 -0
  16. package/scanner/style-map.ts +145 -0
  17. package/scanner/tailwind-parser.ts +644 -0
  18. package/scanner/transform-math-regression.ts +42 -0
  19. package/scanner/types.ts +298 -0
  20. package/src/blob-placement.ts +111 -0
  21. package/src/change-detection.ts +204 -0
  22. package/src/class-utils.ts +105 -0
  23. package/src/clip-path-decorative.ts +194 -0
  24. package/src/color-resolver.ts +98 -0
  25. package/src/colors.ts +196 -0
  26. package/src/component-defs.ts +54 -0
  27. package/src/component-gen.ts +561 -0
  28. package/src/component-lookup.ts +82 -0
  29. package/src/config.ts +115 -0
  30. package/src/design-system.ts +59 -0
  31. package/src/dev-server.ts +173 -0
  32. package/src/figma-globals.d.ts +3 -0
  33. package/src/font-style-resolver.ts +171 -0
  34. package/src/github.ts +1465 -0
  35. package/src/icon-builder.ts +607 -0
  36. package/src/image-cache.ts +22 -0
  37. package/src/inline-text.ts +271 -0
  38. package/src/layout-parser.ts +667 -0
  39. package/src/layout-utils.ts +155 -0
  40. package/src/main.ts +687 -0
  41. package/src/node-ir.ts +595 -0
  42. package/src/pack-provider.ts +148 -0
  43. package/src/packs.ts +126 -0
  44. package/src/radial-gradient.ts +84 -0
  45. package/src/render-context.ts +138 -0
  46. package/src/responsive-analyzer.ts +139 -0
  47. package/src/state-analyzer.ts +143 -0
  48. package/src/story-builder.ts +1706 -0
  49. package/src/story-layout.ts +38 -0
  50. package/src/tailwind.ts +2379 -0
  51. package/src/text-builder.ts +116 -0
  52. package/src/text-line.ts +42 -0
  53. package/src/token-source.ts +43 -0
  54. package/src/tokens.ts +717 -0
  55. package/src/transform-math.ts +44 -0
  56. package/src/ui-builder.ts +1996 -0
  57. package/src/utility-resolver.ts +125 -0
  58. package/src/variables.ts +1042 -0
  59. package/src/width-solver.ts +466 -0
  60. package/templates/patch-tokens-route.ts +165 -0
  61. package/templates/scan-components-route.ts +57 -0
  62. package/ui.html +1222 -0
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Component Scanner Types
3
+ *
4
+ * Defines the data structures for analyzed components that the Figma plugin
5
+ * will use to generate design system components.
6
+ */
7
+
8
+ // ============================================================================
9
+ // Component Analysis Types
10
+ // ============================================================================
11
+
12
+ export type ComponentType = 'cva' | 'compound' | 'state' | 'simple';
13
+
14
+ /**
15
+ * Base interface for all component analyses
16
+ */
17
+ export interface BaseComponentAnalysis {
18
+ type: ComponentType;
19
+ name: string;
20
+ filePath: string;
21
+ /** Whether a co-located *.stories.tsx file exists */
22
+ hasStory?: boolean;
23
+ /** Parsed stories from the co-located story file */
24
+ stories?: StoryInfo[];
25
+ }
26
+
27
+ /**
28
+ * A single Storybook story extracted from *.stories.tsx
29
+ */
30
+ export interface StoryInfo {
31
+ /** Story export name (e.g., "Default", "Variants", "Sizes") */
32
+ name: string;
33
+ /** Args passed to the component (for args-based stories) */
34
+ args?: Record<string, string>;
35
+ /** Component instances found in the story's render function or args (flat) */
36
+ instances: ComponentInstance[];
37
+ /** Container layout classes (e.g., ["flex", "flex-wrap", "gap-3"]) */
38
+ layoutClasses?: string[];
39
+ /** Full JSX tree for recursive rendering */
40
+ jsxTree?: JsxNode;
41
+ }
42
+
43
+ /**
44
+ * A single component usage found in a story (flat extraction)
45
+ */
46
+ export interface ComponentInstance {
47
+ /** Component tag name (e.g., "Button", "Card") */
48
+ componentName: string;
49
+ /** Props on the component (e.g., { variant: "default", size: "sm" }) */
50
+ props: Record<string, any>;
51
+ /** Text content children */
52
+ children?: string;
53
+ }
54
+
55
+ // ============================================================================
56
+ // JSX Tree Types (for recursive content extraction)
57
+ // ============================================================================
58
+
59
+ /**
60
+ * A node in the JSX tree - either an element or text
61
+ */
62
+ export type JsxNode = JsxElement | JsxText;
63
+
64
+ /**
65
+ * An element node (HTML tag or React component)
66
+ */
67
+ export interface JsxElement {
68
+ type: 'element';
69
+ /** Tag name (e.g., "div", "Button", "h1") */
70
+ tagName: string;
71
+ /** Whether this is a React component (starts with uppercase) */
72
+ isComponent: boolean;
73
+ /** Props/attributes on the element */
74
+ props: Record<string, string>;
75
+ /** Child nodes */
76
+ children: JsxNode[];
77
+ }
78
+
79
+ /**
80
+ * A text node
81
+ */
82
+ export interface JsxText {
83
+ type: 'text';
84
+ /** The text content */
85
+ content: string;
86
+ }
87
+
88
+ /**
89
+ * CVA-based components (Button, Badge, Alert)
90
+ * These have explicit variants defined via class-variance-authority
91
+ */
92
+ export interface CVAComponentAnalysis extends BaseComponentAnalysis {
93
+ type: 'cva';
94
+ baseClasses: string[];
95
+ variants: Record<string, string[]>; // { variant: ["default", "destructive", ...] }
96
+ defaultVariants: Record<string, string>;
97
+ variantClasses: Record<string, Record<string, string[]>>; // { variant: { default: [...classes], destructive: [...] } }
98
+ }
99
+
100
+ /**
101
+ * Compound components (Card, Dialog)
102
+ * These consist of multiple sub-components with their own classes
103
+ */
104
+ export interface CompoundComponentAnalysis extends BaseComponentAnalysis {
105
+ type: 'compound';
106
+ subComponents: SubComponentInfo[];
107
+ }
108
+
109
+ export interface SubComponentInfo {
110
+ name: string;
111
+ classes: string[];
112
+ slot: 'container' | 'header' | 'title' | 'description' | 'content' | 'footer' | 'trigger' | 'item';
113
+ }
114
+
115
+ /**
116
+ * State-based components (Input, Checkbox)
117
+ * These have states defined via Tailwind modifiers
118
+ */
119
+ export interface StateComponentAnalysis extends BaseComponentAnalysis {
120
+ type: 'state';
121
+ baseClasses: string[];
122
+ states: Record<string, StateInfo>;
123
+ }
124
+
125
+ export interface StateInfo {
126
+ classes: string[];
127
+ trigger: string; // e.g., "focus-visible:", "aria-invalid:", "disabled:"
128
+ }
129
+
130
+ /**
131
+ * Simple components with static classes
132
+ */
133
+ export interface SimpleComponentAnalysis extends BaseComponentAnalysis {
134
+ type: 'simple';
135
+ classes: string[];
136
+ /** JSX tree structure for components with decorative elements */
137
+ jsxTree?: JsxNode;
138
+ }
139
+
140
+ /**
141
+ * Layout information inferred from Tailwind classes
142
+ */
143
+ export interface LayoutInfo {
144
+ display: 'flex' | 'grid' | 'block' | 'inline' | 'hidden' | 'table' | 'contents' | null;
145
+ direction: 'row' | 'col' | null;
146
+ gap?: number; // pixels
147
+ padding?: { x?: number; y?: number; all?: number };
148
+ alignment?: string; // e.g., 'center', 'start', 'end'
149
+ justification?: string;
150
+ wrap?: boolean;
151
+ }
152
+
153
+ /**
154
+ * Responsive variant information for a component
155
+ */
156
+ export interface ResponsiveInfo {
157
+ /** Classes grouped by breakpoint */
158
+ breakpoints: Record<string, string[]>;
159
+ /** Whether the component has responsive visibility changes */
160
+ hasResponsiveVisibility: boolean;
161
+ }
162
+
163
+ /**
164
+ * Color scheme variant information
165
+ */
166
+ export interface ColorSchemeInfo {
167
+ light: string[];
168
+ dark: string[];
169
+ }
170
+
171
+ export type ComponentAnalysis =
172
+ | CVAComponentAnalysis
173
+ | CompoundComponentAnalysis
174
+ | StateComponentAnalysis
175
+ | SimpleComponentAnalysis;
176
+
177
+ // ============================================================================
178
+ // Parsed Tailwind Class Types
179
+ // ============================================================================
180
+
181
+ export interface ParsedTailwindClass {
182
+ original: string; // The full class string
183
+ modifier?: string; // Combined modifier string (all stacked modifiers joined)
184
+ modifiers: string[]; // Individual modifier layers (e.g. ['sm:', 'hover:'])
185
+ utility: string; // The actual utility class (after stripping modifiers)
186
+ value?: string; // Value if present (e.g., "4" in "p-4")
187
+ responsive?: string; // Responsive breakpoint modifier if present (e.g., "sm:")
188
+ pseudo?: string; // Pseudo-element/class modifier if present (e.g., "placeholder:")
189
+ state?: string; // State modifier if present (e.g., "hover:")
190
+ isDark?: boolean; // Whether this class has a dark: modifier
191
+ opacity?: number; // Opacity modifier if present (e.g., 50 from "bg-muted/50")
192
+ arbitrary?: string; // Arbitrary value if present (e.g., "13px" from "text-[13px]")
193
+ lineHeight?: string; // Line-height slash notation (e.g., "6" from "text-sm/6")
194
+ }
195
+
196
+ export interface ColorMapping {
197
+ tailwindClass: string; // e.g., "bg-primary"
198
+ tokenName: string; // e.g., "primary"
199
+ property: 'background' | 'text' | 'border' | 'ring';
200
+ }
201
+
202
+ export interface SizeMapping {
203
+ tailwindClass: string; // e.g., "h-9"
204
+ property: 'height' | 'width' | 'padding' | 'gap';
205
+ value: number; // Value in pixels
206
+ }
207
+
208
+ // ============================================================================
209
+ // Scanner Configuration
210
+ // ============================================================================
211
+
212
+ export interface ScannerConfig {
213
+ /** Paths to scan for components (relative to project root) */
214
+ componentPaths: string[];
215
+
216
+ /** Glob pattern for component files */
217
+ filePattern: string;
218
+
219
+ /** Components to exclude from scanning */
220
+ exclude: string[];
221
+
222
+ /** Design tokens file path */
223
+ tokensPath?: string;
224
+
225
+ /** If true, only include components that have a co-located *.stories.tsx file */
226
+ onlyWithStories?: boolean;
227
+
228
+ /**
229
+ * npm package names (or prefixes ending with '/') to treat as icon libraries.
230
+ * Imports from these packages are collected and rendered to SVG for the icon registry.
231
+ * Prefix matching: 'react-icons/' matches 'react-icons/hi', 'react-icons/fa', etc.
232
+ * Exact matching: 'lucide-react' matches only 'lucide-react'.
233
+ * Defaults to ['lucide-react', 'react-icons/'].
234
+ */
235
+ iconPackages?: string[];
236
+ }
237
+
238
+ // ============================================================================
239
+ // Generated Output (for Figma plugin)
240
+ // ============================================================================
241
+
242
+ export interface ComponentDefinitions {
243
+ /** Contract/schema version for plugin compatibility checks. */
244
+ schemaVersion: number;
245
+
246
+ /** Version for cache invalidation */
247
+ version: string;
248
+
249
+ /** When the definitions were generated */
250
+ generatedAt: string;
251
+
252
+ /** All analyzed components */
253
+ components: EnrichedComponentAnalysis[];
254
+
255
+ /** Tailwind spacing scale (px values) */
256
+ spacingScale: Record<string, number>;
257
+
258
+ /** Color token names used in components */
259
+ colorTokens: string[];
260
+
261
+ /** Palette token -> color value map (e.g. "green-600" -> "#16a34a") */
262
+ paletteTokens?: Record<string, string>;
263
+
264
+ /** React icon name -> svg mapping */
265
+ iconRegistry?: Record<string, IconRegistryEntry>;
266
+
267
+ /** Tailwind class -> CSS declaration map */
268
+ styleMap?: Record<string, StyleRuleEntry[]>;
269
+ }
270
+
271
+ export interface StyleRuleEntry {
272
+ declarations: Record<string, string>;
273
+ media?: string;
274
+ }
275
+
276
+ export interface IconImportSpec {
277
+ module: string;
278
+ exportName: string;
279
+ }
280
+
281
+ export interface IconRegistryEntry extends IconImportSpec {
282
+ svg: string;
283
+ }
284
+
285
+ /**
286
+ * Enriched component analysis with layout, responsive, and color-scheme info.
287
+ * Produced by the CLI after scanning.
288
+ */
289
+ export interface EnrichedComponentAnalysis {
290
+ /** The base analysis */
291
+ analysis: ComponentAnalysis;
292
+ /** Inferred layout from base classes */
293
+ layout: LayoutInfo;
294
+ /** Responsive breakpoint groupings */
295
+ responsive: ResponsiveInfo;
296
+ /** Light/dark mode class split */
297
+ colorScheme: ColorSchemeInfo;
298
+ }
@@ -0,0 +1,111 @@
1
+ export type BlobDimensions = {
2
+ vectorWidth: number;
3
+ vectorHeight: number;
4
+ };
5
+
6
+ export type BlobPlacement = {
7
+ layoutLeft: number;
8
+ desiredCenterX: number;
9
+ desiredCenterY: number;
10
+ cssRotateDeg: number;
11
+ topOffset: number;
12
+ hasTranslateXHalf: boolean;
13
+ };
14
+
15
+ const LEFT_CALC_REGEX = /^left-\[calc\(50%([+-]\d+(?:\.\d+)?rem)?\)\]$/;
16
+ const WIDTH_REGEX = /^w-(\d+(?:\.\d+)?)$/;
17
+ const ASPECT_REGEX = /^aspect-(\d+)\/(\d+)$/;
18
+ const TOP_REGEX = /^(-?)top-(\d+(?:\.\d+)?)$/;
19
+ const ROTATE_SIMPLE_REGEX = /^(-?)rotate-(\d+(?:\.\d+)?)$/;
20
+ const ROTATE_ARBITRARY_REGEX = /^(-?)rotate-\[(-?\d+(?:\.\d+)?)deg\]$/;
21
+
22
+ function findLastClass(classes: string[], predicate: (cls: string) => boolean): string | null {
23
+ for (let i = classes.length - 1; i >= 0; i--) {
24
+ if (predicate(classes[i])) return classes[i];
25
+ }
26
+ return null;
27
+ }
28
+
29
+ export function parseRotateDegreesFromClasses(classes: string[]): number {
30
+ for (let i = classes.length - 1; i >= 0; i--) {
31
+ const cls = classes[i];
32
+ const simple = cls.match(ROTATE_SIMPLE_REGEX);
33
+ if (simple) {
34
+ const sign = simple[1] === '-' ? -1 : 1;
35
+ return sign * parseFloat(simple[2]);
36
+ }
37
+ const arbitrary = cls.match(ROTATE_ARBITRARY_REGEX);
38
+ if (arbitrary) {
39
+ const sign = arbitrary[1] === '-' ? -1 : 1;
40
+ return sign * parseFloat(arbitrary[2]);
41
+ }
42
+ }
43
+ return 0;
44
+ }
45
+
46
+ export function resolveBlobDimensions(
47
+ activeClasses: string[],
48
+ defaults: BlobDimensions = { vectorWidth: 578, vectorHeight: 340 },
49
+ ): BlobDimensions {
50
+ let vectorWidth = defaults.vectorWidth;
51
+ let vectorHeight = defaults.vectorHeight;
52
+
53
+ const widthClass = findLastClass(activeClasses, cls => WIDTH_REGEX.test(cls));
54
+ if (widthClass) {
55
+ const widthMatch = widthClass.match(WIDTH_REGEX);
56
+ if (widthMatch) {
57
+ vectorWidth = parseFloat(widthMatch[1]) * 4;
58
+ }
59
+ }
60
+
61
+ const aspectClass = findLastClass(activeClasses, cls => ASPECT_REGEX.test(cls));
62
+ if (aspectClass) {
63
+ const aspectMatch = aspectClass.match(ASPECT_REGEX);
64
+ if (aspectMatch) {
65
+ const ratio = parseFloat(aspectMatch[1]) / parseFloat(aspectMatch[2]);
66
+ vectorHeight = vectorWidth / ratio;
67
+ }
68
+ }
69
+
70
+ return { vectorWidth, vectorHeight };
71
+ }
72
+
73
+ export function resolveBlobPlacement(
74
+ activeClasses: string[],
75
+ containerWidth: number,
76
+ vectorWidth: number,
77
+ vectorHeight: number,
78
+ ): BlobPlacement {
79
+ const hasTranslateXHalf = activeClasses.includes('-translate-x-1/2');
80
+
81
+ let layoutLeft = 0;
82
+ const leftClass = findLastClass(activeClasses, cls => LEFT_CALC_REGEX.test(cls));
83
+ if (leftClass) {
84
+ const leftMatch = leftClass.match(LEFT_CALC_REGEX);
85
+ const remOffset = leftMatch && leftMatch[1] ? parseFloat(leftMatch[1]) * 16 : 0;
86
+ layoutLeft = containerWidth * 0.5 + remOffset;
87
+ }
88
+
89
+ let topOffset = 0;
90
+ const topClass = findLastClass(activeClasses, cls => TOP_REGEX.test(cls));
91
+ if (topClass) {
92
+ const topMatch = topClass.match(TOP_REGEX);
93
+ if (topMatch) {
94
+ const sign = topMatch[1] === '-' ? -1 : 1;
95
+ topOffset = parseFloat(topMatch[2]) * 4 * sign;
96
+ }
97
+ }
98
+
99
+ const desiredCenterX = hasTranslateXHalf ? layoutLeft : (layoutLeft + vectorWidth / 2);
100
+ const desiredCenterY = vectorHeight / 2;
101
+ const cssRotateDeg = parseRotateDegreesFromClasses(activeClasses);
102
+
103
+ return {
104
+ layoutLeft,
105
+ desiredCenterX,
106
+ desiredCenterY,
107
+ cssRotateDeg,
108
+ topOffset,
109
+ hasTranslateXHalf,
110
+ };
111
+ }
@@ -0,0 +1,204 @@
1
+ import { COMPONENT_DEFS } from './tokens';
2
+ import { extractFrameProperties, propsToTailwindClasses } from './tailwind';
3
+
4
+ interface ComponentFrame {
5
+ frame: any;
6
+ componentName: string;
7
+ variant: string;
8
+ state: string;
9
+ props: any;
10
+ }
11
+
12
+ interface ChangeDetail {
13
+ property: string;
14
+ figma: string;
15
+ code: string;
16
+ newClasses: string[];
17
+ }
18
+
19
+ interface ComponentChange {
20
+ name: string;
21
+ type: string;
22
+ file: string;
23
+ changes: ChangeDetail[];
24
+ figmaProps: any;
25
+ suggestedClasses: string[];
26
+ }
27
+
28
+ interface ClassUpdate {
29
+ property: string;
30
+ remove: string[];
31
+ add: string[];
32
+ }
33
+
34
+ interface ComponentPatch {
35
+ component: string;
36
+ file: string;
37
+ classUpdates: ClassUpdate[];
38
+ }
39
+
40
+ export function findComponentFrames(): ComponentFrame[] {
41
+ let dsPage: any = null;
42
+ for (let i = 0; i < (figma as any).root.children.length; i++) {
43
+ if ((figma as any).root.children[i].name === 'Design System') {
44
+ dsPage = (figma as any).root.children[i];
45
+ break;
46
+ }
47
+ }
48
+
49
+ if (!dsPage) {
50
+ return [];
51
+ }
52
+
53
+ const components: ComponentFrame[] = [];
54
+
55
+ // Find component frames by name pattern: "ComponentName/variant/state" or "ComponentName States"
56
+ function searchFrames(node: any, depth: number): void {
57
+ if (depth > 5) return; // Limit depth
58
+
59
+ if (node.type === 'FRAME' || node.type === 'COMPONENT') {
60
+ // Check if this looks like a component instance
61
+ const nameParts = node.name.split('/');
62
+ if (nameParts.length >= 2) {
63
+ // This is a variant frame like "Button/default/hover"
64
+ components.push({
65
+ frame: node,
66
+ componentName: nameParts[0],
67
+ variant: nameParts[1],
68
+ state: nameParts[2] || 'default',
69
+ props: extractFrameProperties(node)
70
+ });
71
+ }
72
+ }
73
+
74
+ if ('children' in node) {
75
+ for (let i = 0; i < node.children.length; i++) {
76
+ searchFrames(node.children[i], depth + 1);
77
+ }
78
+ }
79
+ }
80
+
81
+ searchFrames(dsPage, 0);
82
+ return components;
83
+ }
84
+
85
+ /**
86
+ * Compare Figma component properties with code definitions
87
+ * Returns list of changed components with their differences
88
+ */
89
+ export function detectComponentChanges(): { changes?: ComponentChange[]; error?: string } {
90
+ const figmaComponents = findComponentFrames();
91
+ const changes: ComponentChange[] = [];
92
+
93
+ // Group Figma frames by component name
94
+ const figmaByComponent: Record<string, ComponentFrame[]> = {};
95
+ for (let i = 0; i < figmaComponents.length; i++) {
96
+ const comp = figmaComponents[i];
97
+ if (!figmaByComponent[comp.componentName]) {
98
+ figmaByComponent[comp.componentName] = [];
99
+ }
100
+ figmaByComponent[comp.componentName].push(comp);
101
+ }
102
+
103
+ // Compare with code definitions
104
+ if (!COMPONENT_DEFS || !(COMPONENT_DEFS as any).components) {
105
+ return { error: 'No component definitions loaded. Start dev server and refresh.' };
106
+ }
107
+
108
+ for (let j = 0; j < (COMPONENT_DEFS as any).components.length; j++) {
109
+ const codeDef = (COMPONENT_DEFS as any).components[j];
110
+ const figmaFrames = figmaByComponent[codeDef.name] || [];
111
+
112
+ if (figmaFrames.length === 0) {
113
+ continue; // Component not found in Figma
114
+ }
115
+
116
+ // Get the first frame as representative (usually default state)
117
+ const figmaFrame = figmaFrames[0];
118
+ const figmaProps = figmaFrame.props;
119
+ const figmaClasses = propsToTailwindClasses(figmaProps);
120
+
121
+ // Get code classes
122
+ let codeClasses: string[] = [];
123
+ if (codeDef.type === 'cva' && codeDef.baseClasses) {
124
+ codeClasses = codeDef.baseClasses;
125
+ } else if (codeDef.type === 'state' && codeDef.baseClasses) {
126
+ codeClasses = codeDef.baseClasses;
127
+ } else if (codeDef.type === 'simple' && codeDef.classes) {
128
+ codeClasses = codeDef.classes;
129
+ }
130
+
131
+ // Find spacing/radius classes in code
132
+ const codeSpacingClasses = codeClasses.filter(function(cls: string) {
133
+ return /^(p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|rounded)/.test(cls);
134
+ });
135
+
136
+ // Compare
137
+ let hasChanges = false;
138
+ const changeDetails: ChangeDetail[] = [];
139
+
140
+ // Check padding changes
141
+ const figmaPadding = figmaClasses.filter(function(c: string) { return c.startsWith('p'); });
142
+ const codePadding = codeSpacingClasses.filter(function(c: string) { return c.startsWith('p'); });
143
+
144
+ if (figmaPadding.join(' ') !== codePadding.join(' ')) {
145
+ hasChanges = true;
146
+ changeDetails.push({
147
+ property: 'padding',
148
+ figma: figmaPadding.join(' ') || 'none',
149
+ code: codePadding.join(' ') || 'none',
150
+ newClasses: figmaPadding
151
+ });
152
+ }
153
+
154
+ // Check radius changes
155
+ const figmaRadius = figmaClasses.filter(function(c: string) { return c.startsWith('rounded'); });
156
+ const codeRadius = codeSpacingClasses.filter(function(c: string) { return c.startsWith('rounded'); });
157
+
158
+ if (figmaRadius.join(' ') !== codeRadius.join(' ')) {
159
+ hasChanges = true;
160
+ changeDetails.push({
161
+ property: 'borderRadius',
162
+ figma: figmaRadius.join(' ') || 'none',
163
+ code: codeRadius.join(' ') || 'none',
164
+ newClasses: figmaRadius
165
+ });
166
+ }
167
+
168
+ if (hasChanges) {
169
+ changes.push({
170
+ name: codeDef.name,
171
+ type: codeDef.type,
172
+ file: codeDef.file,
173
+ changes: changeDetails,
174
+ figmaProps: figmaProps,
175
+ suggestedClasses: figmaClasses
176
+ });
177
+ }
178
+ }
179
+
180
+ return { changes: changes };
181
+ }
182
+
183
+ /**
184
+ * Generate code patch for a component
185
+ * Returns the class changes that need to be made
186
+ */
187
+ export function generateComponentPatch(componentChange: ComponentChange): ComponentPatch {
188
+ const patch: ComponentPatch = {
189
+ component: componentChange.name,
190
+ file: componentChange.file,
191
+ classUpdates: []
192
+ };
193
+
194
+ for (let i = 0; i < componentChange.changes.length; i++) {
195
+ const change = componentChange.changes[i];
196
+ patch.classUpdates.push({
197
+ property: change.property,
198
+ remove: change.code.split(' ').filter(Boolean),
199
+ add: change.newClasses
200
+ });
201
+ }
202
+
203
+ return patch;
204
+ }