Skip to main content
Glama
style-processor.ts14.4 kB
import type { SimplifiedNode } from "../types/figma.js"; import type { FlatElement } from "../types/generator.js"; import { formatRGBAColor, parsePaint } from "../utils/color-utils.js"; /** * Resolves visual appearance properties of a node. * Extracts background color, background images, opacity, borders, etc. */ export const resolveAppearance = ({ node, styles, }: { node: SimplifiedNode; styles: Record<string, any>; }): FlatElement["appearance"] | undefined => { const appearance: FlatElement["appearance"] = {}; if (node.fills && Array.isArray(node.fills) && node.fills.length > 0) { if (node.type !== "TEXT") { const visibleFill = node.fills.find((fill) => fill.visible !== false); if (visibleFill) { const parsedFill = parsePaint(visibleFill); if (parsedFill) { if (parsedFill.type === "IMAGE") { appearance.backgroundImage = `url(${parsedFill.value})`; if (parsedFill.scaleMode) { appearance.backgroundSize = parsedFill.scaleMode === "FILL" ? "cover" : parsedFill.scaleMode === "FIT" ? "contain" : parsedFill.scaleMode === "TILE" ? "repeat" : "100% 100%"; } } else { appearance.backgroundColor = parsedFill.value; } } if (visibleFill.blendMode && visibleFill.blendMode !== "NORMAL") { appearance.mixBlendMode = visibleFill.blendMode.toLowerCase(); } } } } if (node.strokes && Array.isArray(node.strokes) && node.strokes.length > 0) { const visibleStroke = node.strokes.find( (stroke) => stroke.visible !== false ); if (visibleStroke) { const parsedStroke = parsePaint(visibleStroke); if (parsedStroke) { appearance.borderColor = parsedStroke.value; } if (node.strokeWeight !== undefined) { appearance.borderWidth = `${node.strokeWeight}px`; } if (node.strokeAlign) { appearance.borderStyle = "solid"; appearance.strokeAlign = node.strokeAlign.toLowerCase(); } if (node.strokeDashes && node.strokeDashes.length > 0) { appearance.borderStyle = "dashed"; appearance.borderDash = node.strokeDashes.join(" "); } } } if (node.effects && Array.isArray(node.effects) && node.effects.length > 0) { const shadowEffects = node.effects.filter( (effect) => (effect.type === "DROP_SHADOW" || effect.type === "INNER_SHADOW") && effect.visible !== false ); if (shadowEffects.length > 0) { const shadowStrings = shadowEffects.map((effect) => { const shadowEffect = effect as any; const rgba = shadowEffect.color ? formatRGBAColor({ color: shadowEffect.color, opacity: shadowEffect.opacity, }) : "rgba(0, 0, 0, 0.1)"; const inset = effect.type === "INNER_SHADOW" ? "inset " : ""; const spread = shadowEffect.spread !== undefined ? ` ${shadowEffect.spread}px` : " 0px"; return `${inset}${shadowEffect.offset?.x || 0}px ${shadowEffect.offset?.y || 0}px ${shadowEffect.radius || 0}px${spread} ${rgba}`; }); appearance.boxShadow = shadowStrings.join(", "); } const blurEffects = node.effects.filter( (effect) => (effect.type === "LAYER_BLUR" || effect.type === "BACKGROUND_BLUR") && effect.visible !== false ); if (blurEffects.length > 0) { const blurEffect = blurEffects[0]; if (blurEffect) { const blurData = blurEffect as any; if (blurEffect.type === "LAYER_BLUR") { appearance.filter = `blur(${blurData.radius || 0}px)`; } else if (blurEffect.type === "BACKGROUND_BLUR") { appearance.backdropFilter = `blur(${blurData.radius || 0}px)`; } } } } if (node.borderRadius !== undefined) { if (Array.isArray(node.borderRadius)) { appearance.borderRadius = node.borderRadius .map((r) => `${r}px`) .join(" "); } else if (typeof node.borderRadius === "string") { appearance.borderRadius = node.borderRadius; } else { appearance.borderRadius = `${node.borderRadius}px`; } } else if ( node.rectangleCornerRadii && Array.isArray(node.rectangleCornerRadii) ) { appearance.borderRadius = node.rectangleCornerRadii .map((r: number) => `${r}px`) .join(" "); } if (typeof node.opacity === "number") { appearance.opacity = node.opacity; } if (node.individualStrokeWeights) { const { top, right, bottom, left } = node.individualStrokeWeights; appearance.borderTopWidth = `${top}px`; appearance.borderRightWidth = `${right}px`; appearance.borderBottomWidth = `${bottom}px`; appearance.borderLeftWidth = `${left}px`; delete appearance.borderWidth; } if (node.cornerSmoothing && node.cornerSmoothing > 0) { appearance.borderRadius = appearance.borderRadius || "0px"; } return Object.keys(appearance).length > 0 ? appearance : undefined; }; /** * Resolves text-related properties of a node. * Extracts text content, font styles, alignment, etc. */ export const resolveText = ({ node, styles, }: { node: SimplifiedNode; styles: Record<string, any>; }): FlatElement["text"] | undefined => { if (node.type !== "TEXT" && !node.characters && !node.style) return undefined; const text: FlatElement["text"] = {}; if (node.characters) { text.content = node.characters; } else if (node.text) { text.content = node.text; } const textStyle = node.style || node.textStyle; if (textStyle) { if (textStyle.fontFamily) { text.fontFamily = textStyle.fontFamily; } else if (textStyle.fontPostScriptName) { const fontName = textStyle.fontPostScriptName.split("-")[0]; if (fontName) { text.fontFamily = fontName; } } if (textStyle.fontSize) { text.fontSize = textStyle.fontSize; } if (textStyle.fontWeight) { text.fontWeight = textStyle.fontWeight; } if (textStyle.textAlignHorizontal) { text.alignment = textStyle.textAlignHorizontal.toLowerCase(); } if (textStyle.lineHeightPx) { text.lineHeight = `${textStyle.lineHeightPx}px`; } else if (textStyle.lineHeightPercentFontSize) { text.lineHeight = `${textStyle.lineHeightPercentFontSize}%`; } if (textStyle.letterSpacing !== undefined) { text.letterSpacing = `${textStyle.letterSpacing}px`; } if (textStyle.textDecoration) { switch (textStyle.textDecoration) { case "UNDERLINE": text.textDecoration = "underline"; break; case "STRIKETHROUGH": text.textDecoration = "line-through"; break; } } if (textStyle.textCase) { switch (textStyle.textCase) { case "UPPER": text.textTransform = "uppercase"; break; case "LOWER": text.textTransform = "lowercase"; break; case "TITLE": text.textTransform = "capitalize"; break; case "SMALL_CAPS": case "SMALL_CAPS_FORCED": text.fontVariant = "small-caps"; break; } } if (textStyle.italic || textStyle.fontStyle === "Italic") { text.fontStyle = "italic"; } } if (node.fills && Array.isArray(node.fills) && node.fills.length > 0) { const visibleFill = node.fills.find((fill) => fill.visible !== false); if (visibleFill) { const parsed = parsePaint(visibleFill); if (parsed) { text.color = parsed.value; } } } if (textStyle && textStyle.paragraphSpacing) { text.paragraphSpacing = `${textStyle.paragraphSpacing}px`; } if (textStyle && textStyle.paragraphIndent) { text.textIndent = `${textStyle.paragraphIndent}px`; } if (textStyle && textStyle.textTruncation === "ENDING") { text.overflow = "ellipsis"; if (textStyle.maxLines) { text.lineClamp = textStyle.maxLines; } } return Object.keys(text).length > 0 ? text : undefined; }; /** * Resolves layout properties of a node. * Extracts flex direction, justification, alignment, etc. */ export const resolveLayout = ({ node, styles, }: { node: SimplifiedNode; styles: Record<string, any>; }): FlatElement["layout"] | undefined => { const layout: FlatElement["layout"] = {}; if (node.layoutMode) { layout.direction = node.layoutMode === "HORIZONTAL" ? "row" : node.layoutMode === "VERTICAL" ? "column" : "none"; } if (node.primaryAxisAlignItems) { switch (node.primaryAxisAlignItems) { case "MIN": layout.justifyContent = "flex-start"; break; case "CENTER": layout.justifyContent = "center"; break; case "MAX": layout.justifyContent = "flex-end"; break; case "SPACE_BETWEEN": layout.justifyContent = "space-between"; break; } } if (node.counterAxisAlignItems) { switch (node.counterAxisAlignItems) { case "MIN": layout.alignItems = "flex-start"; break; case "CENTER": layout.alignItems = "center"; break; case "MAX": layout.alignItems = "flex-end"; break; case "BASELINE": layout.alignItems = "baseline"; break; } } if (node.itemSpacing !== undefined) { layout.gap = node.itemSpacing; } const hasPadding = node.paddingLeft !== undefined || node.paddingRight !== undefined || node.paddingTop !== undefined || node.paddingBottom !== undefined; if (hasPadding) { const top = node.paddingTop !== undefined ? `${node.paddingTop}px` : "0px"; const right = node.paddingRight !== undefined ? `${node.paddingRight}px` : "0px"; const bottom = node.paddingBottom !== undefined ? `${node.paddingBottom}px` : "0px"; const left = node.paddingLeft !== undefined ? `${node.paddingLeft}px` : "0px"; if (top === right && right === bottom && bottom === left) { layout.padding = top; } else if (top === bottom && left === right) { layout.padding = `${top} ${right}`; } else { layout.padding = `${top} ${right} ${bottom} ${left}`; } } if (node.size) { if (node.size.width !== undefined) { layout.width = `${node.size.width}px`; } if (node.size.height !== undefined) { layout.height = `${node.size.height}px`; } } else { if (node.absoluteBoundingBox) { layout.width = `${node.absoluteBoundingBox.width}px`; layout.height = `${node.absoluteBoundingBox.height}px`; } } if (node.constraints) { layout.position = "absolute"; if (node.constraints.horizontal) { switch (node.constraints.horizontal) { case "LEFT": layout.left = "0px"; break; case "RIGHT": layout.right = "0px"; break; case "CENTER": layout.left = "50%"; layout.transform = "translateX(-50%)"; break; case "LEFT_RIGHT": layout.left = "0px"; layout.right = "0px"; break; case "SCALE": layout.left = "0px"; layout.width = "100%"; break; } } if (node.constraints.vertical) { switch (node.constraints.vertical) { case "TOP": layout.top = "0px"; break; case "BOTTOM": layout.bottom = "0px"; break; case "CENTER": layout.top = "50%"; layout.transform = layout.transform ? "translate(-50%, -50%)" : "translateY(-50%)"; break; case "TOP_BOTTOM": layout.top = "0px"; layout.bottom = "0px"; break; case "SCALE": layout.top = "0px"; layout.height = "100%"; break; } } } if (node.layoutAlign) { switch (node.layoutAlign) { case "STRETCH": layout.alignSelf = "stretch"; break; case "MIN": layout.alignSelf = "flex-start"; break; case "CENTER": layout.alignSelf = "center"; break; case "MAX": layout.alignSelf = "flex-end"; break; case "INHERIT": layout.alignSelf = "inherit"; break; } } if (node.layoutGrow !== undefined) { layout.flexGrow = node.layoutGrow; } if (node.layoutSizingHorizontal) { switch (node.layoutSizingHorizontal) { case "FIXED": layout.width = layout.width || "auto"; break; case "HUG": layout.width = "fit-content"; break; case "FILL": layout.width = "100%"; break; } } if (node.layoutSizingVertical) { switch (node.layoutSizingVertical) { case "FIXED": layout.height = layout.height || "auto"; break; case "HUG": layout.height = "fit-content"; break; case "FILL": layout.height = "100%"; break; } } if (node.minWidth !== undefined) { layout.minWidth = `${node.minWidth}px`; } if (node.maxWidth !== undefined) { layout.maxWidth = `${node.maxWidth}px`; } if (node.minHeight !== undefined) { layout.minHeight = `${node.minHeight}px`; } if (node.maxHeight !== undefined) { layout.maxHeight = `${node.maxHeight}px`; } if (node.overflowDirection) { switch (node.overflowDirection) { case "HORIZONTAL_SCROLLING": layout.overflowX = "auto"; layout.overflowY = "hidden"; break; case "VERTICAL_SCROLLING": layout.overflowX = "hidden"; layout.overflowY = "auto"; break; case "HORIZONTAL_AND_VERTICAL_SCROLLING": layout.overflow = "auto"; break; case "NONE": layout.overflow = "hidden"; break; } } else if (node.clipsContent) { layout.overflow = "hidden"; } if (node.layoutWrap === "WRAP") { layout.flexWrap = "wrap"; if (node.counterAxisSpacing !== undefined) { layout.rowGap = `${node.counterAxisSpacing}px`; } } return Object.keys(layout).length > 0 ? layout : undefined; };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/toddle-edu/figma-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server