@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.
- package/build/FigmaService.d.ts +47 -0
- package/build/FigmaService.d.ts.map +1 -0
- package/build/FigmaService.js +108 -0
- package/build/FigmaService.js.map +1 -0
- package/build/figma/extractors/built-in.d.ts +57 -0
- package/build/figma/extractors/built-in.d.ts.map +1 -0
- package/build/figma/extractors/built-in.js +206 -0
- package/build/figma/extractors/built-in.js.map +1 -0
- package/build/figma/extractors/design-extractor.d.ts +7 -0
- package/build/figma/extractors/design-extractor.d.ts.map +1 -0
- package/build/figma/extractors/design-extractor.js +66 -0
- package/build/figma/extractors/design-extractor.js.map +1 -0
- package/build/figma/extractors/index.d.ts +5 -0
- package/build/figma/extractors/index.d.ts.map +1 -0
- package/build/figma/extractors/index.js +11 -0
- package/build/figma/extractors/index.js.map +1 -0
- package/build/figma/extractors/node-walker.d.ts +16 -0
- package/build/figma/extractors/node-walker.d.ts.map +1 -0
- package/build/figma/extractors/node-walker.js +93 -0
- package/build/figma/extractors/node-walker.js.map +1 -0
- package/build/figma/extractors/types.d.ts +73 -0
- package/build/figma/extractors/types.d.ts.map +1 -0
- package/build/figma/extractors/types.js +2 -0
- package/build/figma/extractors/types.js.map +1 -0
- package/build/figma/transformers/component.d.ts +27 -0
- package/build/figma/transformers/component.d.ts.map +1 -0
- package/build/figma/transformers/component.js +29 -0
- package/build/figma/transformers/component.js.map +1 -0
- package/build/figma/transformers/effects.d.ts +9 -0
- package/build/figma/transformers/effects.d.ts.map +1 -0
- package/build/figma/transformers/effects.js +50 -0
- package/build/figma/transformers/effects.js.map +1 -0
- package/build/figma/transformers/layout.d.ts +27 -0
- package/build/figma/transformers/layout.d.ts.map +1 -0
- package/build/figma/transformers/layout.js +203 -0
- package/build/figma/transformers/layout.js.map +1 -0
- package/build/figma/transformers/style.d.ts +120 -0
- package/build/figma/transformers/style.d.ts.map +1 -0
- package/build/figma/transformers/style.js +539 -0
- package/build/figma/transformers/style.js.map +1 -0
- package/build/figma/transformers/text.d.ts +31 -0
- package/build/figma/transformers/text.d.ts.map +1 -0
- package/build/figma/transformers/text.js +34 -0
- package/build/figma/transformers/text.js.map +1 -0
- package/build/figma/utils/common.d.ts +70 -0
- package/build/figma/utils/common.d.ts.map +1 -0
- package/build/figma/utils/common.js +167 -0
- package/build/figma/utils/common.js.map +1 -0
- package/build/figma/utils/fetch-with-retry.d.ts +13 -0
- package/build/figma/utils/fetch-with-retry.d.ts.map +1 -0
- package/build/figma/utils/fetch-with-retry.js +70 -0
- package/build/figma/utils/fetch-with-retry.js.map +1 -0
- package/build/figma/utils/identity.d.ts +23 -0
- package/build/figma/utils/identity.d.ts.map +1 -0
- package/build/figma/utils/identity.js +71 -0
- package/build/figma/utils/identity.js.map +1 -0
- package/build/index.d.ts +20 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +88 -0
- package/build/index.js.map +1 -0
- 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
|