@mcp-consultant-tools/figma 1.0.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 (61) hide show
  1. package/build/FigmaService.d.ts +47 -0
  2. package/build/FigmaService.d.ts.map +1 -0
  3. package/build/FigmaService.js +108 -0
  4. package/build/FigmaService.js.map +1 -0
  5. package/build/figma/extractors/built-in.d.ts +57 -0
  6. package/build/figma/extractors/built-in.d.ts.map +1 -0
  7. package/build/figma/extractors/built-in.js +206 -0
  8. package/build/figma/extractors/built-in.js.map +1 -0
  9. package/build/figma/extractors/design-extractor.d.ts +7 -0
  10. package/build/figma/extractors/design-extractor.d.ts.map +1 -0
  11. package/build/figma/extractors/design-extractor.js +66 -0
  12. package/build/figma/extractors/design-extractor.js.map +1 -0
  13. package/build/figma/extractors/index.d.ts +5 -0
  14. package/build/figma/extractors/index.d.ts.map +1 -0
  15. package/build/figma/extractors/index.js +11 -0
  16. package/build/figma/extractors/index.js.map +1 -0
  17. package/build/figma/extractors/node-walker.d.ts +16 -0
  18. package/build/figma/extractors/node-walker.d.ts.map +1 -0
  19. package/build/figma/extractors/node-walker.js +93 -0
  20. package/build/figma/extractors/node-walker.js.map +1 -0
  21. package/build/figma/extractors/types.d.ts +73 -0
  22. package/build/figma/extractors/types.d.ts.map +1 -0
  23. package/build/figma/extractors/types.js +2 -0
  24. package/build/figma/extractors/types.js.map +1 -0
  25. package/build/figma/transformers/component.d.ts +27 -0
  26. package/build/figma/transformers/component.d.ts.map +1 -0
  27. package/build/figma/transformers/component.js +29 -0
  28. package/build/figma/transformers/component.js.map +1 -0
  29. package/build/figma/transformers/effects.d.ts +9 -0
  30. package/build/figma/transformers/effects.d.ts.map +1 -0
  31. package/build/figma/transformers/effects.js +50 -0
  32. package/build/figma/transformers/effects.js.map +1 -0
  33. package/build/figma/transformers/layout.d.ts +27 -0
  34. package/build/figma/transformers/layout.d.ts.map +1 -0
  35. package/build/figma/transformers/layout.js +203 -0
  36. package/build/figma/transformers/layout.js.map +1 -0
  37. package/build/figma/transformers/style.d.ts +120 -0
  38. package/build/figma/transformers/style.d.ts.map +1 -0
  39. package/build/figma/transformers/style.js +539 -0
  40. package/build/figma/transformers/style.js.map +1 -0
  41. package/build/figma/transformers/text.d.ts +31 -0
  42. package/build/figma/transformers/text.d.ts.map +1 -0
  43. package/build/figma/transformers/text.js +34 -0
  44. package/build/figma/transformers/text.js.map +1 -0
  45. package/build/figma/utils/common.d.ts +70 -0
  46. package/build/figma/utils/common.d.ts.map +1 -0
  47. package/build/figma/utils/common.js +167 -0
  48. package/build/figma/utils/common.js.map +1 -0
  49. package/build/figma/utils/fetch-with-retry.d.ts +13 -0
  50. package/build/figma/utils/fetch-with-retry.d.ts.map +1 -0
  51. package/build/figma/utils/fetch-with-retry.js +70 -0
  52. package/build/figma/utils/fetch-with-retry.js.map +1 -0
  53. package/build/figma/utils/identity.d.ts +23 -0
  54. package/build/figma/utils/identity.d.ts.map +1 -0
  55. package/build/figma/utils/identity.js +71 -0
  56. package/build/figma/utils/identity.js.map +1 -0
  57. package/build/index.d.ts +20 -0
  58. package/build/index.d.ts.map +1 -0
  59. package/build/index.js +88 -0
  60. package/build/index.js.map +1 -0
  61. package/package.json +54 -0
@@ -0,0 +1,120 @@
1
+ import type { Node as FigmaDocumentNode, Paint, RGBA, Transform } from "@figma/rest-api-spec";
2
+ export type CSSRGBAColor = `rgba(${number}, ${number}, ${number}, ${number})`;
3
+ export type CSSHexColor = `#${string}`;
4
+ export interface ColorValue {
5
+ hex: CSSHexColor;
6
+ opacity: number;
7
+ }
8
+ /**
9
+ * Simplified image fill with CSS properties and processing metadata
10
+ *
11
+ * This type represents an image fill that can be used as either:
12
+ * - background-image (when parent node has children)
13
+ * - <img> tag (when parent node has no children)
14
+ *
15
+ * The CSS properties are mutually exclusive based on usage context.
16
+ */
17
+ export type SimplifiedImageFill = {
18
+ type: "IMAGE";
19
+ imageRef: string;
20
+ scaleMode: "FILL" | "FIT" | "TILE" | "STRETCH";
21
+ /**
22
+ * For TILE mode, the scaling factor relative to original image size
23
+ */
24
+ scalingFactor?: number;
25
+ backgroundSize?: string;
26
+ backgroundRepeat?: string;
27
+ isBackground?: boolean;
28
+ objectFit?: string;
29
+ imageDownloadArguments?: {
30
+ /**
31
+ * Whether image needs cropping based on transform
32
+ */
33
+ needsCropping: boolean;
34
+ /**
35
+ * Whether CSS variables for dimensions are needed to calculate the background size for TILE mode
36
+ *
37
+ * Figma bases scalingFactor on the image's original size. In CSS, background size (as a percentage)
38
+ * is calculated based on the size of the container. We need to pass back the original dimensions
39
+ * after processing to calculate the intended background size when translated to code.
40
+ */
41
+ requiresImageDimensions: boolean;
42
+ /**
43
+ * Figma's transform matrix for Sharp processing
44
+ */
45
+ cropTransform?: Transform;
46
+ /**
47
+ * Suggested filename suffix to make cropped images unique
48
+ * When the same imageRef is used multiple times with different crops,
49
+ * this helps avoid overwriting conflicts
50
+ */
51
+ filenameSuffix?: string;
52
+ };
53
+ };
54
+ export type SimplifiedGradientFill = {
55
+ type: "GRADIENT_LINEAR" | "GRADIENT_RADIAL" | "GRADIENT_ANGULAR" | "GRADIENT_DIAMOND";
56
+ gradient: string;
57
+ };
58
+ export type SimplifiedPatternFill = {
59
+ type: "PATTERN";
60
+ patternSource: {
61
+ /**
62
+ * Hardcode to expect PNG for now, consider SVG detection in the future.
63
+ *
64
+ * SVG detection is a bit challenging because the nodeId in question isn't
65
+ * guaranteed to be included in the response we're working with. No guaranteed
66
+ * way to look into it and see if it's only composed of vector shapes.
67
+ */
68
+ type: "IMAGE-PNG";
69
+ nodeId: string;
70
+ };
71
+ backgroundRepeat: string;
72
+ backgroundSize: string;
73
+ backgroundPosition: string;
74
+ };
75
+ export type SimplifiedFill = SimplifiedImageFill | SimplifiedGradientFill | SimplifiedPatternFill | CSSRGBAColor | CSSHexColor;
76
+ export type SimplifiedStroke = {
77
+ colors: SimplifiedFill[];
78
+ strokeWeight?: string;
79
+ strokeDashes?: number[];
80
+ strokeWeights?: string;
81
+ };
82
+ /**
83
+ * Build simplified stroke information from a Figma node
84
+ *
85
+ * @param n - The Figma node to extract stroke information from
86
+ * @param hasChildren - Whether the node has children (affects paint processing)
87
+ * @returns Simplified stroke object with colors and properties
88
+ */
89
+ export declare function buildSimplifiedStrokes(n: FigmaDocumentNode, hasChildren?: boolean): SimplifiedStroke;
90
+ /**
91
+ * Convert a Figma paint (solid, image, gradient) to a SimplifiedFill
92
+ * @param raw - The Figma paint to convert
93
+ * @param hasChildren - Whether the node has children (determines CSS properties)
94
+ * @returns The converted SimplifiedFill
95
+ */
96
+ export declare function parsePaint(raw: Paint, hasChildren?: boolean): SimplifiedFill;
97
+ /**
98
+ * Convert hex color value and opacity to rgba format
99
+ * @param hex - Hexadecimal color value (e.g., "#FF0000" or "#F00")
100
+ * @param opacity - Opacity value (0-1)
101
+ * @returns Color string in rgba format
102
+ */
103
+ export declare function hexToRgba(hex: string, opacity?: number): string;
104
+ /**
105
+ * Convert color from RGBA to { hex, opacity }
106
+ *
107
+ * @param color - The color to convert, including alpha channel
108
+ * @param opacity - The opacity of the color, if not included in alpha channel
109
+ * @returns The converted color
110
+ **/
111
+ export declare function convertColor(color: RGBA, opacity?: number): ColorValue;
112
+ /**
113
+ * Convert color from Figma RGBA to rgba(#, #, #, #) CSS format
114
+ *
115
+ * @param color - The color to convert, including alpha channel
116
+ * @param opacity - The opacity of the color, if not included in alpha channel
117
+ * @returns The converted color
118
+ **/
119
+ export declare function formatRGBAColor(color: RGBA, opacity?: number): CSSRGBAColor;
120
+ //# sourceMappingURL=style.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"style.d.ts","sourceRoot":"","sources":["../../../src/figma/transformers/style.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,IAAI,iBAAiB,EACzB,KAAK,EAEL,IAAI,EACJ,SAAS,EACV,MAAM,sBAAsB,CAAC;AAI9B,MAAM,MAAM,YAAY,GAAG,QAAQ,MAAM,KAAK,MAAM,KAAK,MAAM,KAAK,MAAM,GAAG,CAAC;AAC9E,MAAM,MAAM,WAAW,GAAG,IAAI,MAAM,EAAE,CAAC;AACvC,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,WAAW,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC;IAC/C;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAGvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAG1B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IAInB,sBAAsB,CAAC,EAAE;QACvB;;WAEG;QACH,aAAa,EAAE,OAAO,CAAC;QACvB;;;;;;WAMG;QACH,uBAAuB,EAAE,OAAO,CAAC;QACjC;;WAEG;QACH,aAAa,CAAC,EAAE,SAAS,CAAC;QAC1B;;;;WAIG;QACH,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EAAE,iBAAiB,GAAG,iBAAiB,GAAG,kBAAkB,GAAG,kBAAkB,CAAC;IACtF,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,SAAS,CAAC;IAChB,aAAa,EAAE;QACb;;;;;;WAMG;QACH,IAAI,EAAE,WAAW,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,cAAc,GACtB,mBAAmB,GACnB,sBAAsB,GACtB,qBAAqB,GACrB,YAAY,GACZ,WAAW,CAAC;AAEhB,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AA+GF;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,CAAC,EAAE,iBAAiB,EACpB,WAAW,GAAE,OAAe,GAC3B,gBAAgB,CAmBlB;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,KAAK,EAAE,WAAW,GAAE,OAAe,GAAG,cAAc,CA+DnF;AA4DD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,MAAU,GAAG,MAAM,CAkBlE;AAED;;;;;;IAMI;AACJ,wBAAgB,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,SAAI,GAAG,UAAU,CAYjE;AAED;;;;;;IAMI;AACJ,wBAAgB,eAAe,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,SAAI,GAAG,YAAY,CAQtE"}
@@ -0,0 +1,539 @@
1
+ import { generateCSSShorthand, isVisible } from "../utils/common.js";
2
+ import { hasValue, isStrokeWeights } from "../utils/identity.js";
3
+ /**
4
+ * Translate Figma scale modes to CSS properties based on usage context
5
+ *
6
+ * @param scaleMode - The Figma scale mode (FILL, FIT, TILE, STRETCH)
7
+ * @param isBackground - Whether this image will be used as background-image (true) or <img> tag (false)
8
+ * @param scalingFactor - For TILE mode, the scaling factor relative to original image size
9
+ * @returns Object containing CSS properties and processing metadata
10
+ */
11
+ function translateScaleMode(scaleMode, hasChildren, scalingFactor) {
12
+ const isBackground = hasChildren;
13
+ switch (scaleMode) {
14
+ case "FILL":
15
+ // Image covers entire container, may be cropped
16
+ return {
17
+ css: isBackground
18
+ ? { backgroundSize: "cover", backgroundRepeat: "no-repeat", isBackground: true }
19
+ : { objectFit: "cover", isBackground: false },
20
+ processing: { needsCropping: false, requiresImageDimensions: false },
21
+ };
22
+ case "FIT":
23
+ // Image fits entirely within container, may have empty space
24
+ return {
25
+ css: isBackground
26
+ ? { backgroundSize: "contain", backgroundRepeat: "no-repeat", isBackground: true }
27
+ : { objectFit: "contain", isBackground: false },
28
+ processing: { needsCropping: false, requiresImageDimensions: false },
29
+ };
30
+ case "TILE":
31
+ // Image repeats to fill container at specified scale
32
+ // Always treat as background image (can't tile an <img> tag)
33
+ return {
34
+ css: {
35
+ backgroundRepeat: "repeat",
36
+ backgroundSize: scalingFactor
37
+ ? `calc(var(--original-width) * ${scalingFactor}) calc(var(--original-height) * ${scalingFactor})`
38
+ : "auto",
39
+ isBackground: true,
40
+ },
41
+ processing: { needsCropping: false, requiresImageDimensions: true },
42
+ };
43
+ case "STRETCH":
44
+ // Figma calls crop "STRETCH" in its API.
45
+ return {
46
+ css: isBackground
47
+ ? { backgroundSize: "100% 100%", backgroundRepeat: "no-repeat", isBackground: true }
48
+ : { objectFit: "fill", isBackground: false },
49
+ processing: { needsCropping: false, requiresImageDimensions: false },
50
+ };
51
+ default:
52
+ return {
53
+ css: {},
54
+ processing: { needsCropping: false, requiresImageDimensions: false },
55
+ };
56
+ }
57
+ }
58
+ /**
59
+ * Generate a short hash from a transform matrix to create unique filenames
60
+ * @param transform - The transform matrix to hash
61
+ * @returns Short hash string for filename suffix
62
+ */
63
+ function generateTransformHash(transform) {
64
+ const values = transform.flat();
65
+ const hash = values.reduce((acc, val) => {
66
+ // Simple hash function - convert to string and create checksum
67
+ const str = val.toString();
68
+ for (let i = 0; i < str.length; i++) {
69
+ acc = ((acc << 5) - acc + str.charCodeAt(i)) & 0xffffffff;
70
+ }
71
+ return acc;
72
+ }, 0);
73
+ // Convert to positive hex string, take first 6 chars
74
+ return Math.abs(hash).toString(16).substring(0, 6);
75
+ }
76
+ /**
77
+ * Handle imageTransform for post-processing (not CSS translation)
78
+ *
79
+ * When Figma includes an imageTransform matrix, it means the image is cropped/transformed.
80
+ * This function converts the transform into processing instructions for Sharp.
81
+ *
82
+ * @param imageTransform - Figma's 2x3 transform matrix [[scaleX, skewX, translateX], [skewY, scaleY, translateY]]
83
+ * @returns Processing metadata for image cropping
84
+ */
85
+ function handleImageTransform(imageTransform) {
86
+ const transformHash = generateTransformHash(imageTransform);
87
+ return {
88
+ needsCropping: true,
89
+ requiresImageDimensions: false,
90
+ cropTransform: imageTransform,
91
+ filenameSuffix: `${transformHash}`,
92
+ };
93
+ }
94
+ /**
95
+ * Build simplified stroke information from a Figma node
96
+ *
97
+ * @param n - The Figma node to extract stroke information from
98
+ * @param hasChildren - Whether the node has children (affects paint processing)
99
+ * @returns Simplified stroke object with colors and properties
100
+ */
101
+ export function buildSimplifiedStrokes(n, hasChildren = false) {
102
+ let strokes = { colors: [] };
103
+ if (hasValue("strokes", n) && Array.isArray(n.strokes) && n.strokes.length) {
104
+ strokes.colors = n.strokes.filter(isVisible).map((stroke) => parsePaint(stroke, hasChildren));
105
+ }
106
+ if (hasValue("strokeWeight", n) && typeof n.strokeWeight === "number" && n.strokeWeight > 0) {
107
+ strokes.strokeWeight = `${n.strokeWeight}px`;
108
+ }
109
+ if (hasValue("strokeDashes", n) && Array.isArray(n.strokeDashes) && n.strokeDashes.length) {
110
+ strokes.strokeDashes = n.strokeDashes;
111
+ }
112
+ if (hasValue("individualStrokeWeights", n, isStrokeWeights)) {
113
+ strokes.strokeWeight = generateCSSShorthand(n.individualStrokeWeights);
114
+ }
115
+ return strokes;
116
+ }
117
+ /**
118
+ * Convert a Figma paint (solid, image, gradient) to a SimplifiedFill
119
+ * @param raw - The Figma paint to convert
120
+ * @param hasChildren - Whether the node has children (determines CSS properties)
121
+ * @returns The converted SimplifiedFill
122
+ */
123
+ export function parsePaint(raw, hasChildren = false) {
124
+ if (raw.type === "IMAGE") {
125
+ const baseImageFill = {
126
+ type: "IMAGE",
127
+ imageRef: raw.imageRef,
128
+ scaleMode: raw.scaleMode,
129
+ scalingFactor: raw.scalingFactor,
130
+ };
131
+ // Get CSS properties and processing metadata from scale mode
132
+ // TILE mode always needs to be treated as background image (can't tile an <img> tag)
133
+ const isBackground = hasChildren || baseImageFill.scaleMode === "TILE";
134
+ const { css, processing } = translateScaleMode(baseImageFill.scaleMode, isBackground, raw.scalingFactor);
135
+ // Combine scale mode processing with transform processing if needed
136
+ // Transform processing (cropping) takes precedence over scale mode processing
137
+ let finalProcessing = processing;
138
+ if (raw.imageTransform) {
139
+ const transformProcessing = handleImageTransform(raw.imageTransform);
140
+ finalProcessing = {
141
+ ...processing,
142
+ ...transformProcessing,
143
+ // Keep requiresImageDimensions from scale mode (needed for TILE)
144
+ requiresImageDimensions: processing.requiresImageDimensions || transformProcessing.requiresImageDimensions,
145
+ };
146
+ }
147
+ return {
148
+ ...baseImageFill,
149
+ ...css,
150
+ imageDownloadArguments: finalProcessing,
151
+ };
152
+ }
153
+ else if (raw.type === "SOLID") {
154
+ // treat as SOLID
155
+ const { hex, opacity } = convertColor(raw.color, raw.opacity);
156
+ if (opacity === 1) {
157
+ return hex;
158
+ }
159
+ else {
160
+ return formatRGBAColor(raw.color, opacity);
161
+ }
162
+ }
163
+ else if (raw.type === "PATTERN") {
164
+ return parsePatternPaint(raw);
165
+ }
166
+ else if (["GRADIENT_LINEAR", "GRADIENT_RADIAL", "GRADIENT_ANGULAR", "GRADIENT_DIAMOND"].includes(raw.type)) {
167
+ return {
168
+ type: raw.type,
169
+ gradient: convertGradientToCss(raw),
170
+ };
171
+ }
172
+ else {
173
+ throw new Error(`Unknown paint type: ${raw.type}`);
174
+ }
175
+ }
176
+ /**
177
+ * Convert a Figma PatternPaint to a CSS-like pattern fill.
178
+ *
179
+ * Ignores `tileType` and `spacing` from the Figma API currently as there's
180
+ * no great way to translate them to CSS.
181
+ *
182
+ * @param raw - The Figma PatternPaint to convert
183
+ * @returns The converted pattern SimplifiedFill
184
+ */
185
+ function parsePatternPaint(raw) {
186
+ /**
187
+ * The only CSS-like repeat value supported by Figma is repeat.
188
+ *
189
+ * They also have hexagonal horizontal and vertical repeats, but
190
+ * those aren't easy to pull off in CSS, so we just use repeat.
191
+ */
192
+ let backgroundRepeat = "repeat";
193
+ let horizontal = "left";
194
+ switch (raw.horizontalAlignment) {
195
+ case "START":
196
+ horizontal = "left";
197
+ break;
198
+ case "CENTER":
199
+ horizontal = "center";
200
+ break;
201
+ case "END":
202
+ horizontal = "right";
203
+ break;
204
+ }
205
+ let vertical = "top";
206
+ switch (raw.verticalAlignment) {
207
+ case "START":
208
+ vertical = "top";
209
+ break;
210
+ case "CENTER":
211
+ vertical = "center";
212
+ break;
213
+ case "END":
214
+ vertical = "bottom";
215
+ break;
216
+ }
217
+ return {
218
+ type: raw.type,
219
+ patternSource: {
220
+ type: "IMAGE-PNG",
221
+ nodeId: raw.sourceNodeId,
222
+ },
223
+ backgroundRepeat,
224
+ backgroundSize: `${Math.round(raw.scalingFactor * 100)}%`,
225
+ backgroundPosition: `${horizontal} ${vertical}`,
226
+ };
227
+ }
228
+ /**
229
+ * Convert hex color value and opacity to rgba format
230
+ * @param hex - Hexadecimal color value (e.g., "#FF0000" or "#F00")
231
+ * @param opacity - Opacity value (0-1)
232
+ * @returns Color string in rgba format
233
+ */
234
+ export function hexToRgba(hex, opacity = 1) {
235
+ // Remove possible # prefix
236
+ hex = hex.replace("#", "");
237
+ // Handle shorthand hex values (e.g., #FFF)
238
+ if (hex.length === 3) {
239
+ hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
240
+ }
241
+ // Convert hex to RGB values
242
+ const r = parseInt(hex.substring(0, 2), 16);
243
+ const g = parseInt(hex.substring(2, 4), 16);
244
+ const b = parseInt(hex.substring(4, 6), 16);
245
+ // Ensure opacity is in the 0-1 range
246
+ const validOpacity = Math.min(Math.max(opacity, 0), 1);
247
+ return `rgba(${r}, ${g}, ${b}, ${validOpacity})`;
248
+ }
249
+ /**
250
+ * Convert color from RGBA to { hex, opacity }
251
+ *
252
+ * @param color - The color to convert, including alpha channel
253
+ * @param opacity - The opacity of the color, if not included in alpha channel
254
+ * @returns The converted color
255
+ **/
256
+ export function convertColor(color, opacity = 1) {
257
+ const r = Math.round(color.r * 255);
258
+ const g = Math.round(color.g * 255);
259
+ const b = Math.round(color.b * 255);
260
+ // Alpha channel defaults to 1. If opacity and alpha are both and < 1, their effects are multiplicative
261
+ const a = Math.round(opacity * color.a * 100) / 100;
262
+ const hex = ("#" +
263
+ ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase());
264
+ return { hex, opacity: a };
265
+ }
266
+ /**
267
+ * Convert color from Figma RGBA to rgba(#, #, #, #) CSS format
268
+ *
269
+ * @param color - The color to convert, including alpha channel
270
+ * @param opacity - The opacity of the color, if not included in alpha channel
271
+ * @returns The converted color
272
+ **/
273
+ export function formatRGBAColor(color, opacity = 1) {
274
+ const r = Math.round(color.r * 255);
275
+ const g = Math.round(color.g * 255);
276
+ const b = Math.round(color.b * 255);
277
+ // Alpha channel defaults to 1. If opacity and alpha are both and < 1, their effects are multiplicative
278
+ const a = Math.round(opacity * color.a * 100) / 100;
279
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
280
+ }
281
+ /**
282
+ * Map gradient stops from Figma's handle-based coordinate system to CSS percentages
283
+ */
284
+ function mapGradientStops(gradient, elementBounds = { width: 1, height: 1 }) {
285
+ const handles = gradient.gradientHandlePositions;
286
+ if (!handles || handles.length < 2) {
287
+ const stops = gradient.gradientStops
288
+ .map(({ position, color }) => {
289
+ const cssColor = formatRGBAColor(color, 1);
290
+ return `${cssColor} ${Math.round(position * 100)}%`;
291
+ })
292
+ .join(", ");
293
+ return { stops, cssGeometry: "0deg" };
294
+ }
295
+ const [handle1, handle2, handle3] = handles;
296
+ switch (gradient.type) {
297
+ case "GRADIENT_LINEAR": {
298
+ return mapLinearGradient(gradient.gradientStops, handle1, handle2, elementBounds);
299
+ }
300
+ case "GRADIENT_RADIAL": {
301
+ return mapRadialGradient(gradient.gradientStops, handle1, handle2, handle3, elementBounds);
302
+ }
303
+ case "GRADIENT_ANGULAR": {
304
+ return mapAngularGradient(gradient.gradientStops, handle1, handle2, handle3, elementBounds);
305
+ }
306
+ case "GRADIENT_DIAMOND": {
307
+ return mapDiamondGradient(gradient.gradientStops, handle1, handle2, handle3, elementBounds);
308
+ }
309
+ default: {
310
+ const stops = gradient.gradientStops
311
+ .map(({ position, color }) => {
312
+ const cssColor = formatRGBAColor(color, 1);
313
+ return `${cssColor} ${Math.round(position * 100)}%`;
314
+ })
315
+ .join(", ");
316
+ return { stops, cssGeometry: "0deg" };
317
+ }
318
+ }
319
+ }
320
+ /**
321
+ * Map linear gradient from Figma handles to CSS
322
+ */
323
+ function mapLinearGradient(gradientStops, start, end, elementBounds) {
324
+ // Calculate the gradient line in element space
325
+ const dx = end.x - start.x;
326
+ const dy = end.y - start.y;
327
+ const gradientLength = Math.sqrt(dx * dx + dy * dy);
328
+ // Handle degenerate case
329
+ if (gradientLength === 0) {
330
+ const stops = gradientStops
331
+ .map(({ position, color }) => {
332
+ const cssColor = formatRGBAColor(color, 1);
333
+ return `${cssColor} ${Math.round(position * 100)}%`;
334
+ })
335
+ .join(", ");
336
+ return { stops, cssGeometry: "0deg" };
337
+ }
338
+ // Calculate angle for CSS
339
+ const angle = Math.atan2(dy, dx) * (180 / Math.PI) + 90;
340
+ // Find where the extended gradient line intersects the element boundaries
341
+ const extendedIntersections = findExtendedLineIntersections(start, end);
342
+ if (extendedIntersections.length >= 2) {
343
+ // The gradient line extended to fill the element
344
+ const fullLineStart = Math.min(extendedIntersections[0], extendedIntersections[1]);
345
+ const fullLineEnd = Math.max(extendedIntersections[0], extendedIntersections[1]);
346
+ const fullLineLength = fullLineEnd - fullLineStart;
347
+ // Map gradient stops from the Figma line segment to the full CSS line
348
+ const mappedStops = gradientStops.map(({ position, color }) => {
349
+ const cssColor = formatRGBAColor(color, 1);
350
+ // Position along the Figma gradient line (0 = start handle, 1 = end handle)
351
+ const figmaLinePosition = position;
352
+ // The Figma line spans from t=0 to t=1
353
+ // The full extended line spans from fullLineStart to fullLineEnd
354
+ // Map the figma position to the extended line
355
+ const tOnExtendedLine = figmaLinePosition * (1 - 0) + 0; // This is just figmaLinePosition
356
+ const extendedPosition = (tOnExtendedLine - fullLineStart) / (fullLineEnd - fullLineStart);
357
+ const clampedPosition = Math.max(0, Math.min(1, extendedPosition));
358
+ return `${cssColor} ${Math.round(clampedPosition * 100)}%`;
359
+ });
360
+ return {
361
+ stops: mappedStops.join(", "),
362
+ cssGeometry: `${Math.round(angle)}deg`,
363
+ };
364
+ }
365
+ // Fallback to simple gradient if intersection calculation fails
366
+ const mappedStops = gradientStops.map(({ position, color }) => {
367
+ const cssColor = formatRGBAColor(color, 1);
368
+ return `${cssColor} ${Math.round(position * 100)}%`;
369
+ });
370
+ return {
371
+ stops: mappedStops.join(", "),
372
+ cssGeometry: `${Math.round(angle)}deg`,
373
+ };
374
+ }
375
+ /**
376
+ * Find where the extended gradient line intersects with the element boundaries
377
+ */
378
+ function findExtendedLineIntersections(start, end) {
379
+ const dx = end.x - start.x;
380
+ const dy = end.y - start.y;
381
+ // Handle degenerate case
382
+ if (Math.abs(dx) < 1e-10 && Math.abs(dy) < 1e-10) {
383
+ return [];
384
+ }
385
+ const intersections = [];
386
+ // Check intersection with each edge of the unit square [0,1] x [0,1]
387
+ // Top edge (y = 0)
388
+ if (Math.abs(dy) > 1e-10) {
389
+ const t = -start.y / dy;
390
+ const x = start.x + t * dx;
391
+ if (x >= 0 && x <= 1) {
392
+ intersections.push(t);
393
+ }
394
+ }
395
+ // Bottom edge (y = 1)
396
+ if (Math.abs(dy) > 1e-10) {
397
+ const t = (1 - start.y) / dy;
398
+ const x = start.x + t * dx;
399
+ if (x >= 0 && x <= 1) {
400
+ intersections.push(t);
401
+ }
402
+ }
403
+ // Left edge (x = 0)
404
+ if (Math.abs(dx) > 1e-10) {
405
+ const t = -start.x / dx;
406
+ const y = start.y + t * dy;
407
+ if (y >= 0 && y <= 1) {
408
+ intersections.push(t);
409
+ }
410
+ }
411
+ // Right edge (x = 1)
412
+ if (Math.abs(dx) > 1e-10) {
413
+ const t = (1 - start.x) / dx;
414
+ const y = start.y + t * dy;
415
+ if (y >= 0 && y <= 1) {
416
+ intersections.push(t);
417
+ }
418
+ }
419
+ // Remove duplicates and sort
420
+ const uniqueIntersections = [
421
+ ...new Set(intersections.map((t) => Math.round(t * 1000000) / 1000000)),
422
+ ];
423
+ return uniqueIntersections.sort((a, b) => a - b);
424
+ }
425
+ /**
426
+ * Find where a line intersects with the unit square (0,0) to (1,1)
427
+ */
428
+ function findLineIntersections(start, end) {
429
+ const dx = end.x - start.x;
430
+ const dy = end.y - start.y;
431
+ const intersections = [];
432
+ // Check intersection with each edge of the unit square
433
+ const edges = [
434
+ { x: 0, y: 0, dx: 1, dy: 0 }, // top edge
435
+ { x: 1, y: 0, dx: 0, dy: 1 }, // right edge
436
+ { x: 1, y: 1, dx: -1, dy: 0 }, // bottom edge
437
+ { x: 0, y: 1, dx: 0, dy: -1 }, // left edge
438
+ ];
439
+ for (const edge of edges) {
440
+ const t = lineIntersection(start, { x: dx, y: dy }, edge, { x: edge.dx, y: edge.dy });
441
+ if (t !== null && t >= 0 && t <= 1) {
442
+ intersections.push(t);
443
+ }
444
+ }
445
+ return intersections.sort((a, b) => a - b);
446
+ }
447
+ /**
448
+ * Calculate line intersection parameter
449
+ */
450
+ function lineIntersection(p1, d1, p2, d2) {
451
+ const denominator = d1.x * d2.y - d1.y * d2.x;
452
+ if (Math.abs(denominator) < 1e-10)
453
+ return null; // Lines are parallel
454
+ const dx = p2.x - p1.x;
455
+ const dy = p2.y - p1.y;
456
+ const t = (dx * d2.y - dy * d2.x) / denominator;
457
+ return t;
458
+ }
459
+ /**
460
+ * Map radial gradient from Figma handles to CSS
461
+ */
462
+ function mapRadialGradient(gradientStops, center, edge, widthHandle, elementBounds) {
463
+ const centerX = Math.round(center.x * 100);
464
+ const centerY = Math.round(center.y * 100);
465
+ const stops = gradientStops
466
+ .map(({ position, color }) => {
467
+ const cssColor = formatRGBAColor(color, 1);
468
+ return `${cssColor} ${Math.round(position * 100)}%`;
469
+ })
470
+ .join(", ");
471
+ return {
472
+ stops,
473
+ cssGeometry: `circle at ${centerX}% ${centerY}%`,
474
+ };
475
+ }
476
+ /**
477
+ * Map angular gradient from Figma handles to CSS
478
+ */
479
+ function mapAngularGradient(gradientStops, center, angleHandle, widthHandle, elementBounds) {
480
+ const centerX = Math.round(center.x * 100);
481
+ const centerY = Math.round(center.y * 100);
482
+ const angle = Math.atan2(angleHandle.y - center.y, angleHandle.x - center.x) * (180 / Math.PI) + 90;
483
+ const stops = gradientStops
484
+ .map(({ position, color }) => {
485
+ const cssColor = formatRGBAColor(color, 1);
486
+ return `${cssColor} ${Math.round(position * 100)}%`;
487
+ })
488
+ .join(", ");
489
+ return {
490
+ stops,
491
+ cssGeometry: `from ${Math.round(angle)}deg at ${centerX}% ${centerY}%`,
492
+ };
493
+ }
494
+ /**
495
+ * Map diamond gradient from Figma handles to CSS (approximate with ellipse)
496
+ */
497
+ function mapDiamondGradient(gradientStops, center, edge, widthHandle, elementBounds) {
498
+ const centerX = Math.round(center.x * 100);
499
+ const centerY = Math.round(center.y * 100);
500
+ const stops = gradientStops
501
+ .map(({ position, color }) => {
502
+ const cssColor = formatRGBAColor(color, 1);
503
+ return `${cssColor} ${Math.round(position * 100)}%`;
504
+ })
505
+ .join(", ");
506
+ return {
507
+ stops,
508
+ cssGeometry: `ellipse at ${centerX}% ${centerY}%`,
509
+ };
510
+ }
511
+ /**
512
+ * Convert a Figma gradient to CSS gradient syntax
513
+ */
514
+ function convertGradientToCss(gradient) {
515
+ // Sort stops by position to ensure proper order
516
+ const sortedGradient = {
517
+ ...gradient,
518
+ gradientStops: [...gradient.gradientStops].sort((a, b) => a.position - b.position),
519
+ };
520
+ // Map gradient stops using handle-based geometry
521
+ const { stops, cssGeometry } = mapGradientStops(sortedGradient);
522
+ switch (gradient.type) {
523
+ case "GRADIENT_LINEAR": {
524
+ return `linear-gradient(${cssGeometry}, ${stops})`;
525
+ }
526
+ case "GRADIENT_RADIAL": {
527
+ return `radial-gradient(${cssGeometry}, ${stops})`;
528
+ }
529
+ case "GRADIENT_ANGULAR": {
530
+ return `conic-gradient(${cssGeometry}, ${stops})`;
531
+ }
532
+ case "GRADIENT_DIAMOND": {
533
+ return `radial-gradient(${cssGeometry}, ${stops})`;
534
+ }
535
+ default:
536
+ return `linear-gradient(0deg, ${stops})`;
537
+ }
538
+ }
539
+ //# sourceMappingURL=style.js.map