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.
- package/README.md +201 -0
- package/bin/inkhouse.mjs +171 -0
- package/code.js +11802 -0
- package/manifest.json +30 -0
- package/package.json +45 -0
- package/scanner/blob-placement-regression.ts +132 -0
- package/scanner/class-collector.ts +69 -0
- package/scanner/cli.ts +336 -0
- package/scanner/component-scanner.ts +2876 -0
- package/scanner/css-patch-regression.ts +112 -0
- package/scanner/css-token-reader-regression.ts +92 -0
- package/scanner/css-token-reader.ts +477 -0
- package/scanner/font-style-resolver-regression.ts +32 -0
- package/scanner/index.ts +9 -0
- package/scanner/radial-gradient-regression.ts +53 -0
- package/scanner/style-map.ts +145 -0
- package/scanner/tailwind-parser.ts +644 -0
- package/scanner/transform-math-regression.ts +42 -0
- package/scanner/types.ts +298 -0
- package/src/blob-placement.ts +111 -0
- package/src/change-detection.ts +204 -0
- package/src/class-utils.ts +105 -0
- package/src/clip-path-decorative.ts +194 -0
- package/src/color-resolver.ts +98 -0
- package/src/colors.ts +196 -0
- package/src/component-defs.ts +54 -0
- package/src/component-gen.ts +561 -0
- package/src/component-lookup.ts +82 -0
- package/src/config.ts +115 -0
- package/src/design-system.ts +59 -0
- package/src/dev-server.ts +173 -0
- package/src/figma-globals.d.ts +3 -0
- package/src/font-style-resolver.ts +171 -0
- package/src/github.ts +1465 -0
- package/src/icon-builder.ts +607 -0
- package/src/image-cache.ts +22 -0
- package/src/inline-text.ts +271 -0
- package/src/layout-parser.ts +667 -0
- package/src/layout-utils.ts +155 -0
- package/src/main.ts +687 -0
- package/src/node-ir.ts +595 -0
- package/src/pack-provider.ts +148 -0
- package/src/packs.ts +126 -0
- package/src/radial-gradient.ts +84 -0
- package/src/render-context.ts +138 -0
- package/src/responsive-analyzer.ts +139 -0
- package/src/state-analyzer.ts +143 -0
- package/src/story-builder.ts +1706 -0
- package/src/story-layout.ts +38 -0
- package/src/tailwind.ts +2379 -0
- package/src/text-builder.ts +116 -0
- package/src/text-line.ts +42 -0
- package/src/token-source.ts +43 -0
- package/src/tokens.ts +717 -0
- package/src/transform-math.ts +44 -0
- package/src/ui-builder.ts +1996 -0
- package/src/utility-resolver.ts +125 -0
- package/src/variables.ts +1042 -0
- package/src/width-solver.ts +466 -0
- package/templates/patch-tokens-route.ts +165 -0
- package/templates/scan-components-route.ts +57 -0
- package/ui.html +1222 -0
package/scanner/types.ts
ADDED
|
@@ -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
|
+
}
|