# Directory Structure ``` ├── .env.example ├── .eslintrc ├── .gitignore ├── .nvmrc ├── .prettierrc ├── Dockerfile ├── LICENSE ├── package.json ├── pnpm-lock.yaml ├── README.en.md ├── README.md ├── smithery.yaml ├── src │ ├── config.ts │ ├── index.ts │ ├── server.ts │ ├── services │ │ ├── figma.ts │ │ └── simplify-node-response.ts │ ├── transformers │ │ ├── effects.ts │ │ ├── layout-optimizer.ts │ │ ├── layout.ts │ │ ├── node.ts │ │ └── style.ts │ └── utils │ ├── common.ts │ ├── file.ts │ ├── identity.ts │ ├── spatial-projection.ts │ └── svg.ts ├── test │ ├── run-simplify-test.sh │ ├── test-output │ │ ├── real-node-data.json │ │ ├── simplified-node-data.json │ │ └── viewer.html │ └── test-simplify.ts ├── tsconfig.json └── tsup.config.ts ``` # Files -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- ``` v20 ``` -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- ``` { "semi": true, "trailingComma": "all", "singleQuote": false, "printWidth": 100, "tabWidth": 2, "useTabs": false } ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Dependencies node_modules .pnpm-store # Build output dist # Environment variables .env .env.local .env.*.local # IDE .vscode/* !.vscode/extensions.json !.vscode/settings.json .idea *.suo *.ntvs* *.njsproj *.sln *.sw? # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # Testing coverage # OS .DS_Store Thumbs.db ``` -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- ``` { "parser": "@typescript-eslint/parser", "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier" ], "plugins": ["@typescript-eslint"], "parserOptions": { "ecmaVersion": 2022, "sourceType": "module" }, "rules": { "@typescript-eslint/explicit-function-return-type": "warn", "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], "@typescript-eslint/no-explicit-any": "warn" } } ``` -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- ```markdown # Figma MCP Server > This project is an improved version of the open-source [Figma-Context-MCP](https://github.com/GLips/Figma-Context-MCP), with optimized data structures and conversion logic. English | [中文版](./README.md) This is a server based on the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) that enables seamless integration of Figma design files with AI coding tools like [Cursor](https://cursor.sh/), [Windsurf](https://codeium.com/windsurf), [Cline](https://cline.bot/), and more. When AI tools can access Figma design data, they can generate code that accurately matches designs in a single pass, performing much better than traditional methods like screenshots. ## Features - Convert Figma design data into AI model-friendly formats - Support retrieving layout and style information for Figma files, artboards, or components - Support downloading images and icon resources from Figma - Reduce the context provided to models, improving AI response accuracy and relevance ## Key Differences from Original Version ### Design Data Return Format ```json { // Design file basic information "name": "Design file name", "lastModified": "Last modification time", "thumbnailUrl": "Thumbnail URL", // Node array containing all page elements "nodes": [ { // Node basic information "id": "Node ID, e.g. 1:156", "name": "Node name", "type": "Node type, such as FRAME, TEXT, RECTANGLE, GROUP, etc.", // Text content (only for text nodes) "text": "Content of text node", // CSS style object containing all style properties "cssStyles": { // Dimensions and position "width": "100px", "height": "50px", "position": "absolute", "left": "10px", "top": "20px", // Text styles (mainly for TEXT nodes) "fontFamily": "Inter", "fontSize": "16px", "fontWeight": 500, "textAlign": "center", "lineHeight": "24px", "color": "#333333", // Background and borders "backgroundColor": "#ffffff", "borderRadius": "8px", "border": "1px solid #eeeeee", // Effects "boxShadow": "0px 4px 8px rgba(0, 0, 0, 0.1)", // Other CSS properties... }, // Fill information (gradients, images, etc.) "fills": [ { "type": "SOLID", "color": "#ff0000", "opacity": 0.5 } ], // Export information (for image and SVG nodes) "exportInfo": { "type": "IMAGE", "format": "PNG", "nodeId": "Node ID", "fileName": "suggested-file-name.png" }, // Child nodes "children": [ // Recursive node objects... ] } ] } ``` ### Data Structure Description #### SimplifiedDesign The top-level structure of the design file, containing basic information and all visible nodes. #### SimplifiedNode Represents an element in the design, which can be an artboard, frame, text, or shape. Key fields include: - `id`: Unique node identifier - `name`: Node name in Figma - `type`: Node type (FRAME, TEXT, RECTANGLE, etc.) - `text`: Text content (text nodes only) - `cssStyles`: CSS style object containing all style properties - `fills`: Fill information array - `exportInfo`: Export information (image and SVG nodes) - `children`: Array of child nodes ### CSSStyle Contains CSS style properties converted to web standards, such as fonts, colors, borders, shadows, etc. ### ExportInfo Export information for image and SVG nodes, including: - `type`: Export type (IMAGE or IMAGE_GROUP) - `format`: Recommended export format (PNG, JPG, SVG) - `nodeId`: Node ID for API calls - `fileName`: Suggested file name ## Installation and Usage ### Local Development and Packaging 1. Clone this repository 2. Install dependencies: `pnpm install` 3. Copy `.env.example` to `.env` and fill in your [Figma API access token](https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens) 4. Local development: `pnpm run dev` 5. Build project: `pnpm run build` 6. Local packaging: `pnpm run publish:local` After packaging, a `.tgz` file will be generated in the project root directory, like `figma-mcp-server-1.0.0.tgz` ### Local Installation and Usage There are three ways to use this service: #### Method 1: Install from NPM (Recommended) ```bash # Global installation npm install -g @yhy2001/figma-mcp-server # Start the service figma-mcp --figma-api-key=<your-figma-api-key> ``` #### Method 2: Install from Local Package ```bash # Global installation of local package npm install -g ./figma-mcp-server-1.0.0.tgz # Start the service figma-mcp --figma-api-key=<your-figma-api-key> ``` #### Method 3: Use in a Project ```bash # Install in project npm install @yhy2001/figma-mcp-server --save # Add to package.json scripts # "start-figma-mcp": "figma-mcp --figma-api-key=<your-figma-api-key>" # Or run directly npx figma-mcp --figma-api-key=<your-figma-api-key> ``` ### Command Line Arguments - `--version`: Show version number - `--figma-api-key`: Your Figma API access token (required) - `--port`: Port for the server to run on (default: 3333) - `--stdio`: Run server in command mode instead of default HTTP/SSE mode - `--help`: Show help menu ## Connecting with AI Tools ### Using in Configuration Files Many tools like Cursor, Windsurf, and Claude Desktop use configuration files to start MCP servers. You can add the following to your configuration file: ```json # Use in MCP Client { "mcpServers": { "Figma MCP": { "command": "npx", "args": ["figma-mcp", "--figma-api-key=<your-figma-api-key>", "--stdio"] } } } # Use in Local { "mcpServers": { "Figma MCP": { "url": "http://localhost:3333/sse", "env": { "API_KEY": "<your-figma-api-key>" } } } } ``` ### Connecting with Cursor 1. Start the server: `figma-mcp --figma-api-key=<your-figma-api-key>` 2. Connect MCP server in Cursor's Settings → Features tab: `http://localhost:3333` 3. After confirming successful connection, use Composer in Agent mode 4. Paste Figma file link and ask Cursor to implement the design ## Available Tools The server provides the following MCP tools: ### get_figma_data Get information about a Figma file or specific node. Parameters: - `fileKey`: The key of the Figma file - `nodeId`: Node ID (strongly recommended) - `depth`: How deep to traverse the node tree ### download_figma_images Download image and icon resources from a Figma file. Parameters: - `fileKey`: The key of the Figma file containing the node - `nodes`: Array of image nodes to fetch - `localPath`: Directory path in the project where images are stored ## License MIT ``` -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- ```typescript import { defineConfig } from "tsup"; const isDev = process.env.npm_lifecycle_event === "dev"; export default defineConfig({ clean: true, entry: ["src/index.ts"], format: ["esm"], minify: !isDev, target: "esnext", outDir: "dist", outExtension: ({ format }) => ({ js: ".js", }), onSuccess: isDev ? "node dist/index.js" : undefined, }); ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile FROM node:lts-alpine # Install pnpm globally RUN npm install -g pnpm # Set working directory WORKDIR /app # Copy package files and install dependencies (cache layer) COPY package.json pnpm-lock.yaml ./ RUN pnpm install # Copy all source files COPY . . # Build the project RUN pnpm run build # Install this package globally so that the 'figma-mcp' command is available RUN npm install -g . # Expose the port (default 3333) EXPOSE 3333 # Default command to run the MCP server CMD [ "figma-mcp", "--stdio" ] ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "baseUrl": "./", "paths": { "~/*": ["./src/*"] }, "target": "ES2020", "lib": ["ES2021", "DOM"], "module": "NodeNext", "moduleResolution": "NodeNext", "resolveJsonModule": true, "allowJs": true, "checkJs": true, /* EMIT RULES */ "outDir": "./dist", "declaration": true, "declarationMap": true, "sourceMap": true, "removeComments": true, "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*", "test-*.ts", "test/test-simplify.ts", "script/custom/test-figma-api.ts", "script/custom/test-figma-comparison.ts", "script/custom/test-figma-data.ts"] } ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml startCommand: type: stdio configSchema: # JSON Schema defining the configuration options for the MCP. type: object required: - figmaApiKey properties: figmaApiKey: type: string description: Your Figma API access token port: type: number default: 3333 description: Port for the server to run on (default 3333) commandFunction: # A JS function that produces the CLI command based on the given config to start the MCP on stdio. |- (config) => ({ command: 'figma-mcp', args: [`--figma-api-key=${config.figmaApiKey}`, '--stdio', `--port=${config.port}`], env: {} }) exampleConfig: figmaApiKey: dummy-figma-api-key port: 3333 ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { FigmaMcpServer } from "./server.js"; import { getServerConfig } from "./config.js"; import { resolve } from "path"; import { config } from "dotenv"; import { fileURLToPath } from "url"; // Load .env from the current working directory config({ path: resolve(process.cwd(), ".env") }); export async function startServer(): Promise<void> { // Check if we're running in stdio mode (e.g., via CLI) const isStdioMode = process.env.NODE_ENV === "cli" || process.argv.includes("--stdio"); const config = getServerConfig(isStdioMode); const server = new FigmaMcpServer(config.figmaApiKey); if (isStdioMode) { const transport = new StdioServerTransport(); await server.connect(transport); } else { console.log(`Initializing Figma MCP Server in HTTP mode on port ${config.port}...`); await server.startHttpServer(config.port); } } startServer().catch((error) => { console.error("Failed to start server:", error); process.exit(1); }); ``` -------------------------------------------------------------------------------- /src/transformers/style.ts: -------------------------------------------------------------------------------- ```typescript import { Node as FigmaDocumentNode } from "@figma/rest-api-spec"; import { SimplifiedFill } from "~/services/simplify-node-response.js"; import { generateCSSShorthand, isVisible, parsePaint } from "~/utils/common.js"; import { hasValue, isStrokeWeights } from "~/utils/identity.js"; export type SimplifiedStroke = { colors: SimplifiedFill[]; strokeWeight?: string; strokeDashes?: number[]; strokeWeights?: string; }; export function buildSimplifiedStrokes(n: FigmaDocumentNode): SimplifiedStroke { let strokes: SimplifiedStroke = { colors: [] }; if (hasValue("strokes", n) && Array.isArray(n.strokes) && n.strokes.length) { strokes.colors = n.strokes.filter(isVisible).map(parsePaint); } if (hasValue("strokeWeight", n) && typeof n.strokeWeight === "number" && n.strokeWeight > 0) { strokes.strokeWeight = `${n.strokeWeight}px`; } if (hasValue("strokeDashes", n) && Array.isArray(n.strokeDashes) && n.strokeDashes.length) { strokes.strokeDashes = n.strokeDashes; } if (hasValue("individualStrokeWeights", n, isStrokeWeights)) { strokes.strokeWeight = generateCSSShorthand(n.individualStrokeWeights); } return strokes; } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "@yhy2001/figma-mcp-server", "version": "1.0.1", "description": "本地MCP服务器,用于Figma设计与AI编码工具集成", "type": "module", "main": "dist/index.js", "bin": { "figma-mcp": "dist/index.js" }, "files": [ "dist", "README.md" ], "scripts": { "dev": "cross-env NODE_ENV=development tsup --watch", "build": "tsup", "test:figma": "tsx test-figma-data.ts", "prepublishOnly": "npm run build", "start": "node dist/index.js", "inspect": "pnpx @modelcontextprotocol/inspector", "mcp-test": "pnpm start -- --stdio", "type-check": "tsc --noEmit", "start:cli": "cross-env NODE_ENV=cli node dist/index.js", "start:http": "node dist/index.js", "dev:cli": "cross-env NODE_ENV=development tsup --watch -- --stdio", "lint": "eslint . --ext .ts", "format": "prettier --write \"src/**/*.ts\"", "pub:release": "pnpm build && npm publish --access public", "publish:local": "pnpm build && npm pack" }, "engines": { "node": ">=18.0.0" }, "repository": { "type": "git", "url": "git+https://github.com/1yhy/figma-mcp-server.git" }, "keywords": [ "figma", "mcp", "typescript", "ai", "design" ], "author": "", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.6.1", "@types/yargs": "^17.0.33", "cross-env": "^7.0.3", "dotenv": "^16.4.7", "express": "^4.21.2", "remeda": "^2.20.1", "yargs": "^17.7.2", "zod": "^3.24.2" }, "devDependencies": { "@figma/rest-api-spec": "^0.24.0", "@types/express": "^5.0.0", "@types/jest": "^29.5.11", "@types/node": "^20.17.0", "@typescript-eslint/eslint-plugin": "^8.24.0", "@typescript-eslint/parser": "^8.24.0", "eslint": "^9.20.1", "eslint-config-prettier": "^10.0.1", "jest": "^29.7.0", "prettier": "^3.5.0", "ts-jest": "^29.2.5", "tsup": "^8.4.0", "tsx": "^4.19.2", "typescript": "^5.7.3" } } ``` -------------------------------------------------------------------------------- /src/transformers/effects.ts: -------------------------------------------------------------------------------- ```typescript import { DropShadowEffect, InnerShadowEffect, BlurEffect, Node as FigmaDocumentNode, } from "@figma/rest-api-spec"; import { formatRGBAColor } from "~/utils/common.js"; import { hasValue } from "~/utils/identity.js"; export type SimplifiedEffects = { boxShadow?: string; filter?: string; backdropFilter?: string; }; export function buildSimplifiedEffects(n: FigmaDocumentNode): SimplifiedEffects { if (!hasValue("effects", n)) return {}; const effects = n.effects.filter((e) => e.visible); // Handle drop and inner shadows (both go into CSS box-shadow) const dropShadows = effects .filter((e): e is DropShadowEffect => e.type === "DROP_SHADOW") .map(simplifyDropShadow); const innerShadows = effects .filter((e): e is InnerShadowEffect => e.type === "INNER_SHADOW") .map(simplifyInnerShadow); const boxShadow = [...dropShadows, ...innerShadows].join(", "); // Handle blur effects - separate by CSS property // Layer blurs use the CSS 'filter' property const filterBlurValues = effects .filter((e): e is BlurEffect => e.type === "LAYER_BLUR") .map(simplifyBlur) .join(" "); // Background blurs use the CSS 'backdrop-filter' property const backdropFilterValues = effects .filter((e): e is BlurEffect => e.type === "BACKGROUND_BLUR") .map(simplifyBlur) .join(" "); const result: SimplifiedEffects = {}; if (boxShadow) result.boxShadow = boxShadow; if (filterBlurValues) result.filter = filterBlurValues; if (backdropFilterValues) result.backdropFilter = backdropFilterValues; return result; } function simplifyDropShadow(effect: DropShadowEffect) { return `${effect.offset.x}px ${effect.offset.y}px ${effect.radius}px ${effect.spread ?? 0}px ${formatRGBAColor(effect.color)}`; } function simplifyInnerShadow(effect: InnerShadowEffect) { return `inset ${effect.offset.x}px ${effect.offset.y}px ${effect.radius}px ${effect.spread ?? 0}px ${formatRGBAColor(effect.color)}`; } function simplifyBlur(effect: BlurEffect) { return `blur(${effect.radius}px)`; } ``` -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- ```typescript import { config } from "dotenv"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; // Load environment variables from .env file config(); interface ServerConfig { figmaApiKey: string; port: number; configSources: { figmaApiKey: "cli" | "env"; port: "cli" | "env" | "default"; }; } function maskApiKey(key: string): string { if (key.length <= 4) return "****"; return `****${key.slice(-4)}`; } interface CliArgs { "figma-api-key"?: string; port?: number; } export function getServerConfig(isStdioMode: boolean): ServerConfig { // Parse command line arguments const argv = yargs(hideBin(process.argv)) .options({ "figma-api-key": { type: "string", description: "Figma API key", }, port: { type: "number", description: "Port to run the server on", }, }) .help() .version("0.1.12") .parseSync() as CliArgs; const config: ServerConfig = { figmaApiKey: "", port: 3333, configSources: { figmaApiKey: "env", port: "default", }, }; // Handle FIGMA_API_KEY if (argv["figma-api-key"]) { config.figmaApiKey = argv["figma-api-key"]; config.configSources.figmaApiKey = "cli"; } else if (process.env.FIGMA_API_KEY) { config.figmaApiKey = process.env.FIGMA_API_KEY; config.configSources.figmaApiKey = "env"; } // Handle PORT if (argv.port) { config.port = argv.port; config.configSources.port = "cli"; } else if (process.env.PORT) { config.port = parseInt(process.env.PORT, 10); config.configSources.port = "env"; } // Validate configuration if (!config.figmaApiKey) { console.error("FIGMA_API_KEY is required (via CLI argument --figma-api-key or .env file)"); process.exit(1); } // Log configuration sources if (!isStdioMode) { console.log("\nConfiguration:"); console.log( `- FIGMA_API_KEY: ${maskApiKey(config.figmaApiKey)} (source: ${config.configSources.figmaApiKey})`, ); console.log(`- PORT: ${config.port} (source: ${config.configSources.port})`); console.log(); // Empty line for better readability } return config; } ``` -------------------------------------------------------------------------------- /src/utils/identity.ts: -------------------------------------------------------------------------------- ```typescript import type { Rectangle, HasLayoutTrait, StrokeWeights, HasFramePropertiesTrait, } from "@figma/rest-api-spec"; import { isTruthy } from "remeda"; import { CSSHexColor, CSSRGBAColor } from "~/services/simplify-node-response.js"; export { isTruthy }; export function hasValue<K extends PropertyKey, T>( key: K, obj: unknown, typeGuard?: (val: unknown) => val is T, ): obj is Record<K, T> { const isObject = typeof obj === "object" && obj !== null; if (!isObject || !(key in obj)) return false; const val = (obj as Record<K, unknown>)[key]; return typeGuard ? typeGuard(val) : val !== undefined; } export function isFrame(val: unknown): val is HasFramePropertiesTrait { return ( typeof val === "object" && !!val && "clipsContent" in val && typeof val.clipsContent === "boolean" ); } export function isLayout(val: unknown): val is HasLayoutTrait { return ( typeof val === "object" && !!val && "absoluteBoundingBox" in val && typeof val.absoluteBoundingBox === "object" && !!val.absoluteBoundingBox && "x" in val.absoluteBoundingBox && "y" in val.absoluteBoundingBox && "width" in val.absoluteBoundingBox && "height" in val.absoluteBoundingBox ); } export function isStrokeWeights(val: unknown): val is StrokeWeights { return ( typeof val === "object" && val !== null && "top" in val && "right" in val && "bottom" in val && "left" in val ); } export function isRectangle<T, K extends string>( key: K, obj: T, ): obj is T & { [P in K]: Rectangle } { const recordObj = obj as Record<K, unknown>; return ( typeof obj === "object" && !!obj && key in recordObj && typeof recordObj[key] === "object" && !!recordObj[key] && "x" in recordObj[key] && "y" in recordObj[key] && "width" in recordObj[key] && "height" in recordObj[key] ); } export function isRectangleCornerRadii(val: unknown): val is number[] { return Array.isArray(val) && val.length === 4 && val.every((v) => typeof v === "number"); } export function isCSSColorValue(val: unknown): val is CSSRGBAColor | CSSHexColor { return typeof val === "string" && (val.startsWith("#") || val.startsWith("rgba")); } ``` -------------------------------------------------------------------------------- /src/services/figma.ts: -------------------------------------------------------------------------------- ```typescript import fs from "fs"; import { parseFigmaResponse, SimplifiedDesign } from "./simplify-node-response.js"; import type { GetImagesResponse, GetFileResponse, GetFileNodesResponse, GetImageFillsResponse, } from "@figma/rest-api-spec"; import { downloadFigmaImage } from "~/utils/common.js"; import { Logger } from "~/server.js"; export interface FigmaError { status: number; err: string; } type FetchImageParams = { /** * The Node in Figma that will either be rendered or have its background image downloaded */ nodeId: string; /** * The local file name to save the image */ fileName: string; /** * The file mimetype for the image */ fileType: "png" | "svg"; }; type FetchImageFillParams = Omit<FetchImageParams, "fileType"> & { /** * Required to grab the background image when an image is used as a fill */ imageRef: string; }; export class FigmaService { private readonly apiKey: string; private readonly baseUrl = "https://api.figma.com/v1"; constructor(apiKey: string) { this.apiKey = apiKey; } private async request<T>(endpoint: string): Promise<T> { if (typeof fetch !== "function") { throw new Error( "The MCP server is being run in a Node.js environment where `fetch` is not available. You won't be able to access any data related to the Figma file.\n\nAs the coding AI assistant, PLEASE ABORT THE CURRENT REQUEST. No alternate approaches will work. Help the user fix this issue so you can proceed by letting them know that they need to run the MCP server with Node.js version 18 or higher.", ); } try { Logger.log(`Calling ${this.baseUrl}${endpoint}`); const response = await fetch(`${this.baseUrl}${endpoint}`, { headers: { "X-Figma-Token": this.apiKey, }, }); if (!response.ok) { throw { status: response.status, err: response.statusText || "Unknown error", } as FigmaError; } return await response.json(); } catch (error) { if ((error as FigmaError).status) { throw error; } if (error instanceof Error) { throw new Error(`Failed to make request to Figma API: ${error.message}`); } throw new Error(`Failed to make request to Figma API: ${error}`); } } async getImageFills( fileKey: string, nodes: FetchImageFillParams[], localPath: string, ): Promise<string[]> { if (nodes.length === 0) return []; let promises: Promise<string>[] = []; const endpoint = `/files/${fileKey}/images`; const file = await this.request<GetImageFillsResponse>(endpoint); const { images = {} } = file.meta; promises = nodes.map(async ({ imageRef, fileName }) => { const imageUrl = images[imageRef]; if (!imageUrl) { return ""; } return downloadFigmaImage(fileName, localPath, imageUrl); }); return Promise.all(promises); } async getImages( fileKey: string, nodes: FetchImageParams[], localPath: string, ): Promise<string[]> { const pngIds = nodes.filter(({ fileType }) => fileType === "png").map(({ nodeId }) => nodeId); const pngFiles = pngIds.length > 0 ? this.request<GetImagesResponse>( `/images/${fileKey}?ids=${pngIds.join(",")}&scale=2&format=png`, ).then(({ images = {} }) => images) : ({} as GetImagesResponse["images"]); const svgIds = nodes.filter(({ fileType }) => fileType === "svg").map(({ nodeId }) => nodeId); const svgFiles = svgIds.length > 0 ? this.request<GetImagesResponse>( `/images/${fileKey}?ids=${svgIds.join(",")}&scale=2&format=svg`, ).then(({ images = {} }) => images) : ({} as GetImagesResponse["images"]); const files = await Promise.all([pngFiles, svgFiles]).then(([f, l]) => ({ ...f, ...l })); const downloads = nodes .map(({ nodeId, fileName }) => { const imageUrl = files[nodeId]; if (imageUrl) { return downloadFigmaImage(fileName, localPath, imageUrl); } return false; }) .filter((url) => !!url); return Promise.all(downloads); } async getFile(fileKey: string, depth?: number): Promise<SimplifiedDesign> { try { const endpoint = `/files/${fileKey}${depth ? `?depth=${depth}` : ""}`; Logger.log(`Retrieving Figma file: ${fileKey} (depth: ${depth ?? "default"})`); const response = await this.request<GetFileResponse>(endpoint); Logger.log("Got response"); const simplifiedResponse = parseFigmaResponse(response); writeLogs("figma-raw.json", response); writeLogs("figma-simplified.json", simplifiedResponse); return simplifiedResponse; } catch (e) { console.error("Failed to get file:", e); throw e; } } async getNode(fileKey: string, nodeId: string, depth?: number): Promise<SimplifiedDesign> { const endpoint = `/files/${fileKey}/nodes?ids=${nodeId}${depth ? `&depth=${depth}` : ""}`; const response = await this.request<GetFileNodesResponse>(endpoint); Logger.log("Got response from getNode, now parsing."); writeLogs("figma-raw.json", response); const simplifiedResponse = parseFigmaResponse(response); writeLogs("figma-simplified.json", simplifiedResponse); return simplifiedResponse; } } function writeLogs(name: string, value: any) { try { if (process.env.NODE_ENV !== "development") return; const logsDir = "logs"; try { fs.accessSync(process.cwd(), fs.constants.W_OK); } catch (error) { Logger.log("Failed to write logs:", error); return; } if (!fs.existsSync(logsDir)) { fs.mkdirSync(logsDir); } fs.writeFileSync(`${logsDir}/${name}`, JSON.stringify(value, null, 2)); } catch (error) { console.debug("Failed to write logs:", error); } } ``` -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { FigmaService } from "./services/figma.js"; import express, { Request, Response } from "express"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { IncomingMessage, ServerResponse } from "http"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { SimplifiedDesign } from "./services/simplify-node-response.js"; export const Logger = { log: (...args: any[]) => {}, error: (...args: any[]) => {}, }; export class FigmaMcpServer { private readonly server: McpServer; private readonly figmaService: FigmaService; private sseTransport: SSEServerTransport | null = null; constructor(figmaApiKey: string) { this.figmaService = new FigmaService(figmaApiKey); this.server = new McpServer( { name: "Figma MCP Server", version: "0.1.12", }, { capabilities: { logging: {}, tools: {}, }, }, ); this.registerTools(); } private registerTools(): void { // Tool to get file information this.server.tool( "get_figma_data", "When the nodeId cannot be obtained, obtain the layout information about the entire Figma file", { fileKey: z .string() .describe( "The key of the Figma file to fetch, often found in a provided URL like figma.com/(file|design)/<fileKey>/...", ), nodeId: z .string() .optional() .describe( "The ID of the node to fetch, often found as URL parameter node-id=<nodeId>, always use if provided", ), depth: z .number() .optional() .describe( "How many levels deep to traverse the node tree, only use if explicitly requested by the user", ), }, async ({ fileKey, nodeId, depth }) => { try { Logger.log( `Fetching ${ depth ? `${depth} layers deep` : "all layers" } of ${nodeId ? `node ${nodeId} from file` : `full file`} ${fileKey}`, ); let file: SimplifiedDesign; if (nodeId) { file = await this.figmaService.getNode(fileKey, nodeId, depth); } else { file = await this.figmaService.getFile(fileKey, depth); } Logger.log(`Successfully fetched file: ${file.name}`); const { nodes, ...metadata } = file; // Stringify each node individually to try to avoid max string length error with big files const nodesJson = `[${nodes.map((node) => JSON.stringify(node, null, 2)).join(",")}]`; const metadataJson = JSON.stringify(metadata, null, 2); const resultJson = `{ "metadata": ${metadataJson}, "nodes": ${nodesJson} }`; return { content: [{ type: "text", text: resultJson }], }; } catch (error) { Logger.error(`Error fetching file ${fileKey}:`, error); return { isError: true, content: [{ type: "text", text: `Error fetching file: ${error}` }], }; } }, ); // TODO: Clean up all image download related code, particularly getImages in Figma service // Tool to download images this.server.tool( "download_figma_images", "Download SVG and PNG images used in a Figma file based on the IDs of image or icon nodes", { fileKey: z.string().describe("The key of the Figma file containing the node"), nodes: z .object({ nodeId: z .string() .describe("The ID of the Figma image node to fetch, formatted as 1234:5678"), imageRef: z .string() .optional() .describe( "If a node has an imageRef fill, you must include this variable. Leave blank when downloading Vector SVG images.", ), fileName: z.string().describe("The local name for saving the fetched file"), }) .array() .describe("The nodes to fetch as images"), localPath: z .string() .describe( "The absolute path to the directory where images are stored in the project. Automatically creates directories if needed.", ), }, async ({ fileKey, nodes, localPath }) => { try { const imageFills = nodes.filter(({ imageRef }) => !!imageRef) as { nodeId: string; imageRef: string; fileName: string; }[]; const fillDownloads = this.figmaService.getImageFills(fileKey, imageFills, localPath); const renderRequests = nodes .filter(({ imageRef }) => !imageRef) .map(({ nodeId, fileName }) => ({ nodeId, fileName, fileType: fileName.endsWith(".svg") ? ("svg" as const) : ("png" as const), })); const renderDownloads = this.figmaService.getImages(fileKey, renderRequests, localPath); const downloads = await Promise.all([fillDownloads, renderDownloads]).then(([f, r]) => [ ...f, ...r, ]); // If any download fails, return false const saveSuccess = !downloads.find((success) => !success); return { content: [ { type: "text", text: saveSuccess ? `Success, ${downloads.length} images downloaded: ${downloads.join(", ")}` : "Failed", }, ], }; } catch (error) { Logger.error(`Error downloading images from file ${fileKey}:`, error); return { isError: true, content: [{ type: "text", text: `Error downloading images: ${error}` }], }; } }, ); } async connect(transport: Transport): Promise<void> { // Logger.log("Connecting to transport..."); await this.server.connect(transport); Logger.log = (...args: any[]) => { this.server.server.sendLoggingMessage({ level: "info", data: args, }); }; Logger.error = (...args: any[]) => { this.server.server.sendLoggingMessage({ level: "error", data: args, }); }; Logger.log("Server connected and ready to process requests"); } async startHttpServer(port: number): Promise<void> { const app = express(); app.get("/sse", async (req: Request, res: Response) => { console.log("New SSE connection established"); this.sseTransport = new SSEServerTransport( "/messages", res as unknown as ServerResponse<IncomingMessage>, ); await this.server.connect(this.sseTransport); }); app.post("/messages", async (req: Request, res: Response) => { if (!this.sseTransport) { res.sendStatus(400); return; } await this.sseTransport.handlePostMessage( req as unknown as IncomingMessage, res as unknown as ServerResponse<IncomingMessage>, ); }); Logger.log = console.log; Logger.error = console.error; app.listen(port, () => { Logger.log(`HTTP server listening on port ${port}`); Logger.log(`SSE endpoint available at http://localhost:${port}/sse`); Logger.log(`Message endpoint available at http://localhost:${port}/messages`); }); } } ``` -------------------------------------------------------------------------------- /src/transformers/layout.ts: -------------------------------------------------------------------------------- ```typescript import { isFrame, isLayout, isRectangle } from "~/utils/identity.js"; import type { Node as FigmaDocumentNode, HasFramePropertiesTrait, HasLayoutTrait, } from "@figma/rest-api-spec"; import { generateCSSShorthand } from "~/utils/common.js"; export interface SimplifiedLayout { mode: "none" | "row" | "column"; justifyContent?: "flex-start" | "flex-end" | "center" | "space-between" | "baseline" | "stretch"; alignItems?: "flex-start" | "flex-end" | "center" | "space-between" | "baseline" | "stretch"; alignSelf?: "flex-start" | "flex-end" | "center" | "stretch"; wrap?: boolean; gap?: string; locationRelativeToParent?: { x: number; y: number; }; dimensions?: { width?: number; height?: number; aspectRatio?: number; }; padding?: string; sizing?: { horizontal?: "fixed" | "fill" | "hug"; vertical?: "fixed" | "fill" | "hug"; }; overflowScroll?: ("x" | "y")[]; position?: "absolute"; } // Convert Figma's layout config into a more typical flex-like schema export function buildSimplifiedLayout( n: FigmaDocumentNode, parent?: FigmaDocumentNode, ): SimplifiedLayout { const frameValues = buildSimplifiedFrameValues(n); const layoutValues = buildSimplifiedLayoutValues(n, parent, frameValues.mode) || {}; return { ...frameValues, ...layoutValues }; } // For flex layouts, process alignment and sizing function convertAlign( axisAlign?: | HasFramePropertiesTrait["primaryAxisAlignItems"] | HasFramePropertiesTrait["counterAxisAlignItems"], stretch?: { children: FigmaDocumentNode[]; axis: "primary" | "counter"; mode: "row" | "column" | "none"; }, ) { if (stretch && stretch.mode !== "none") { const { children, mode, axis } = stretch; // Compute whether to check horizontally or vertically based on axis and direction const direction = getDirection(axis, mode); const shouldStretch = children.length > 0 && children.reduce((shouldStretch, c) => { if (!shouldStretch) return false; if ("layoutPositioning" in c && c.layoutPositioning === "ABSOLUTE") return true; if (direction === "horizontal") { return "layoutSizingHorizontal" in c && c.layoutSizingHorizontal === "FILL"; } else if (direction === "vertical") { return "layoutSizingVertical" in c && c.layoutSizingVertical === "FILL"; } return false; }, true); if (shouldStretch) return "stretch"; } switch (axisAlign) { case "MIN": // MIN, AKA flex-start, is the default alignment return undefined; case "MAX": return "flex-end"; case "CENTER": return "center"; case "SPACE_BETWEEN": return "space-between"; case "BASELINE": return "baseline"; default: return undefined; } } function convertSelfAlign(align?: HasLayoutTrait["layoutAlign"]) { switch (align) { case "MIN": // MIN, AKA flex-start, is the default alignment return undefined; case "MAX": return "flex-end"; case "CENTER": return "center"; case "STRETCH": return "stretch"; default: return undefined; } } // interpret sizing function convertSizing( s?: HasLayoutTrait["layoutSizingHorizontal"] | HasLayoutTrait["layoutSizingVertical"], ) { if (s === "FIXED") return "fixed"; if (s === "FILL") return "fill"; if (s === "HUG") return "hug"; return undefined; } function getDirection( axis: "primary" | "counter", mode: "row" | "column", ): "horizontal" | "vertical" { switch (axis) { case "primary": switch (mode) { case "row": return "horizontal"; case "column": return "vertical"; } case "counter": switch (mode) { case "row": return "horizontal"; case "column": return "vertical"; } } } function buildSimplifiedFrameValues(n: FigmaDocumentNode): SimplifiedLayout | { mode: "none" } { if (!isFrame(n)) { return { mode: "none" }; } const frameValues: SimplifiedLayout = { mode: !n.layoutMode || n.layoutMode === "NONE" ? "none" : n.layoutMode === "HORIZONTAL" ? "row" : "column", }; const overflowScroll: SimplifiedLayout["overflowScroll"] = []; if (n.overflowDirection?.includes("HORIZONTAL")) overflowScroll.push("x"); if (n.overflowDirection?.includes("VERTICAL")) overflowScroll.push("y"); if (overflowScroll.length > 0) frameValues.overflowScroll = overflowScroll; if (frameValues.mode === "none") { return frameValues; } // TODO: convertAlign should be two functions, one for justifyContent and one for alignItems frameValues.justifyContent = convertAlign(n.primaryAxisAlignItems ?? "MIN", { children: n.children, axis: "primary", mode: frameValues.mode, }); frameValues.alignItems = convertAlign(n.counterAxisAlignItems ?? "MIN", { children: n.children, axis: "counter", mode: frameValues.mode, }); frameValues.alignSelf = convertSelfAlign(n.layoutAlign); // Only include wrap if it's set to WRAP, since flex layouts don't default to wrapping frameValues.wrap = n.layoutWrap === "WRAP" ? true : undefined; frameValues.gap = n.itemSpacing ? `${n.itemSpacing ?? 0}px` : undefined; // gather padding if (n.paddingTop || n.paddingBottom || n.paddingLeft || n.paddingRight) { frameValues.padding = generateCSSShorthand({ top: n.paddingTop ?? 0, right: n.paddingRight ?? 0, bottom: n.paddingBottom ?? 0, left: n.paddingLeft ?? 0, }); } return frameValues; } function buildSimplifiedLayoutValues( n: FigmaDocumentNode, parent: FigmaDocumentNode | undefined, mode: "row" | "column" | "none", ): SimplifiedLayout | undefined { if (!isLayout(n)) return undefined; const layoutValues: SimplifiedLayout = { mode }; layoutValues.sizing = { horizontal: convertSizing(n.layoutSizingHorizontal), vertical: convertSizing(n.layoutSizingVertical), }; // Only include positioning-related properties if parent layout isn't flex or if the node is absolute if (isFrame(parent) && (parent?.layoutMode === "NONE" || n.layoutPositioning === "ABSOLUTE")) { if (n.layoutPositioning === "ABSOLUTE") { layoutValues.position = "absolute"; } if (n.absoluteBoundingBox && parent.absoluteBoundingBox) { layoutValues.locationRelativeToParent = { x: n.absoluteBoundingBox.x - (parent?.absoluteBoundingBox?.x ?? n.absoluteBoundingBox.x), y: n.absoluteBoundingBox.y - (parent?.absoluteBoundingBox?.y ?? n.absoluteBoundingBox.y), }; } return layoutValues; } // Handle dimensions based on layout growth and alignment if (isRectangle("absoluteBoundingBox", n) && isRectangle("absoluteBoundingBox", parent)) { const dimensions: { width?: number; height?: number; aspectRatio?: number } = {}; // Only include dimensions that aren't meant to stretch if (mode === "row") { if (!n.layoutGrow && n.layoutSizingHorizontal == "FIXED") dimensions.width = n.absoluteBoundingBox.width; if (n.layoutAlign !== "STRETCH" && n.layoutSizingVertical == "FIXED") dimensions.height = n.absoluteBoundingBox.height; } else if (mode === "column") { // column if (n.layoutAlign !== "STRETCH" && n.layoutSizingHorizontal == "FIXED") dimensions.width = n.absoluteBoundingBox.width; if (!n.layoutGrow && n.layoutSizingVertical == "FIXED") dimensions.height = n.absoluteBoundingBox.height; if (n.preserveRatio) { dimensions.aspectRatio = n.absoluteBoundingBox?.width / n.absoluteBoundingBox?.height; } } if (Object.keys(dimensions).length > 0) { layoutValues.dimensions = dimensions; } } return layoutValues; } ``` -------------------------------------------------------------------------------- /src/utils/common.ts: -------------------------------------------------------------------------------- ```typescript import fs from "fs"; import path from "path"; import type { Paint, RGBA } from "@figma/rest-api-spec"; import { CSSHexColor, CSSRGBAColor, SimplifiedFill } from "~/services/simplify-node-response.js"; export type StyleId = `${string}_${string}` & { __brand: "StyleId" }; export interface ColorValue { hex: CSSHexColor; opacity: number; } /** * Download Figma image and save it locally * @param fileName - The filename to save as * @param localPath - The local path to save to * @param imageUrl - Image URL (images[nodeId]) * @returns A Promise that resolves to the full file path where the image was saved * @throws Error if download fails */ export async function downloadFigmaImage( fileName: string, localPath: string, imageUrl: string, ): Promise<string> { try { // Ensure local path exists if (!fs.existsSync(localPath)) { fs.mkdirSync(localPath, { recursive: true }); } // Build the complete file path const fullPath = path.join(localPath, fileName); // Use fetch to download the image const response = await fetch(imageUrl, { method: "GET", }); if (!response.ok) { throw new Error(`Failed to download image: ${response.statusText}`); } // Create write stream const writer = fs.createWriteStream(fullPath); // Get the response as a readable stream and pipe it to the file const reader = response.body?.getReader(); if (!reader) { throw new Error("Failed to get response body"); } return new Promise((resolve, reject) => { // Process stream const processStream = async () => { try { while (true) { const { done, value } = await reader.read(); if (done) { writer.end(); break; } writer.write(value); } resolve(fullPath); } catch (err) { writer.end(); fs.unlink(fullPath, () => {}); reject(err); } }; writer.on("error", (err) => { reader.cancel(); fs.unlink(fullPath, () => {}); reject(new Error(`Failed to write image: ${err.message}`)); }); processStream(); }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Error downloading image: ${errorMessage}`); } } /** * Remove keys with empty arrays or empty objects from an object. * @param input - The input object or value. * @returns The processed object or the original value. */ export function removeEmptyKeys<T>(input: T): T { // If not an object type or null, return directly if (typeof input !== "object" || input === null) { return input; } // Handle array type if (Array.isArray(input)) { return input.map((item) => removeEmptyKeys(item)) as T; } // Handle object type const result = {} as T; for (const key in input) { if (Object.prototype.hasOwnProperty.call(input, key)) { const value = input[key]; // Recursively process nested objects const cleanedValue = removeEmptyKeys(value); // Skip empty arrays and empty objects if ( cleanedValue !== undefined && !(Array.isArray(cleanedValue) && cleanedValue.length === 0) && !( typeof cleanedValue === "object" && cleanedValue !== null && Object.keys(cleanedValue).length === 0 ) ) { result[key] = cleanedValue; } } } return result; } /** * Convert hex color value and opacity to rgba format * @param hex - Hexadecimal color value (e.g., "#FF0000" or "#F00") * @param opacity - Opacity value (0-1) * @returns Color string in rgba format */ export function hexToRgba(hex: string, opacity: number = 1): string { // Remove possible # prefix hex = hex.replace("#", ""); // Handle shorthand hex values (e.g., #FFF) if (hex.length === 3) { hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; } // Convert hex to RGB values const r = parseInt(hex.substring(0, 2), 16); const g = parseInt(hex.substring(2, 4), 16); const b = parseInt(hex.substring(4, 6), 16); // Ensure opacity is in the 0-1 range const validOpacity = Math.min(Math.max(opacity, 0), 1); return `rgba(${r}, ${g}, ${b}, ${validOpacity})`; } /** * Convert color from RGBA to { hex, opacity } * * @param color - The color to convert, including alpha channel * @param opacity - The opacity of the color, if not included in alpha channel * @returns The converted color **/ export function convertColor(color: RGBA, opacity = 1): ColorValue { const r = Math.round(color.r * 255); const g = Math.round(color.g * 255); const b = Math.round(color.b * 255); // Alpha channel defaults to 1. If opacity and alpha are both and < 1, their effects are multiplicative const a = Math.round(opacity * color.a * 100) / 100; const hex = ("#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()) as CSSHexColor; return { hex, opacity: a }; } /** * Convert color from Figma RGBA to rgba(#, #, #, #) CSS format * * @param color - The color to convert, including alpha channel * @param opacity - The opacity of the color, if not included in alpha channel * @returns The converted color **/ export function formatRGBAColor(color: RGBA, opacity = 1): CSSRGBAColor { const r = Math.round(color.r * 255); const g = Math.round(color.g * 255); const b = Math.round(color.b * 255); // Alpha channel defaults to 1. If opacity and alpha are both and < 1, their effects are multiplicative const a = Math.round(opacity * color.a * 100) / 100; return `rgba(${r}, ${g}, ${b}, ${a})`; } /** * Generate a 6-character random variable ID * @param prefix - ID prefix * @returns A 6-character random ID string with prefix */ export function generateVarId(prefix: string = "var"): StyleId { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; let result = ""; for (let i = 0; i < 6; i++) { const randomIndex = Math.floor(Math.random() * chars.length); result += chars[randomIndex]; } return `${prefix}_${result}` as StyleId; } /** * Generate a CSS shorthand for values that come with top, right, bottom, and left * * input: { top: 10, right: 10, bottom: 10, left: 10 } * output: "10px" * * input: { top: 10, right: 20, bottom: 10, left: 20 } * output: "10px 20px" * * input: { top: 10, right: 20, bottom: 30, left: 40 } * output: "10px 20px 30px 40px" * * @param values - The values to generate the shorthand for * @returns The generated shorthand */ export function generateCSSShorthand( values: { top: number; right: number; bottom: number; left: number; }, { ignoreZero = true, suffix = "px", }: { /** * If true and all values are 0, return undefined. Defaults to true. */ ignoreZero?: boolean; /** * The suffix to add to the shorthand. Defaults to "px". */ suffix?: string; } = {}, ) { const { top, right, bottom, left } = values; if (ignoreZero && top === 0 && right === 0 && bottom === 0 && left === 0) { return undefined; } if (top === right && right === bottom && bottom === left) { return `${top}${suffix}`; } if (right === left) { if (top === bottom) { return `${top}${suffix} ${right}${suffix}`; } return `${top}${suffix} ${right}${suffix} ${bottom}${suffix}`; } return `${top}${suffix} ${right}${suffix} ${bottom}${suffix} ${left}${suffix}`; } /** * Convert a Figma paint (solid, image, gradient) to a SimplifiedFill * @param raw - The Figma paint to convert * @returns The converted SimplifiedFill */ export function parsePaint(raw: Paint): SimplifiedFill { if (raw.type === "IMAGE") { return { type: "IMAGE", imageRef: raw.imageRef, scaleMode: raw.scaleMode, }; } else if (raw.type === "SOLID") { // treat as SOLID const { hex, opacity } = convertColor(raw.color!, raw.opacity); if (opacity === 1) { return hex; } else { return formatRGBAColor(raw.color!, opacity); } } else if ( ["GRADIENT_LINEAR", "GRADIENT_RADIAL", "GRADIENT_ANGULAR", "GRADIENT_DIAMOND"].includes( raw.type, ) ) { // treat as GRADIENT_LINEAR return { type: raw.type, gradientHandlePositions: raw.gradientHandlePositions, gradientStops: raw.gradientStops.map(({ position, color }) => ({ position, color: convertColor(color), })), }; } else { throw new Error(`Unknown paint type: ${raw.type}`); } } /** * 检查元素是否可见 * @param element - 要检查的元素 * @returns 如果元素可见则返回true,否则返回false */ export function isVisible(element: { visible?: boolean; opacity?: number; absoluteBoundingBox?: { x: number; y: number; width: number; height: number }; absoluteRenderBounds?: { x: number; y: number; width: number; height: number } | null; }): boolean { // 1. 显式可见性检查 if (element.visible === false) { return false; } // 2. 透明度检查 if (element.opacity === 0) { return false; } // 3. 渲染边界检查 - 如果明确没有渲染边界,则不可见 if (element.absoluteRenderBounds === null) { return false; } // 默认为可见 return true; } /** * 检查元素在父容器中是否可见 * @param element - 要检查的元素 * @param parent - 父元素信息 * @returns 如果元素可见则返回true,否则返回false */ export function isVisibleInParent( element: { visible?: boolean; opacity?: number; absoluteBoundingBox?: { x: number; y: number; width: number; height: number }; absoluteRenderBounds?: { x: number; y: number; width: number; height: number } | null; }, parent: { clipsContent?: boolean; absoluteBoundingBox?: { x: number; y: number; width: number; height: number }; } ): boolean { // 先检查元素本身是否可见 if (!isVisible(element)) { return false; } // 父容器裁剪检查 if (parent && parent.clipsContent === true && element.absoluteBoundingBox && parent.absoluteBoundingBox) { const elementBox = element.absoluteBoundingBox; const parentBox = parent.absoluteBoundingBox; // 检查元素是否完全在父容器外部 const outsideParent = elementBox.x >= parentBox.x + parentBox.width || // 右侧超出 elementBox.x + elementBox.width <= parentBox.x || // 左侧超出 elementBox.y >= parentBox.y + parentBox.height || // 底部超出 elementBox.y + elementBox.height <= parentBox.y; // 顶部超出 if (outsideParent) { return false; } } // 通过所有检查,认为元素可见 return true; } ``` -------------------------------------------------------------------------------- /test/test-output/simplified-node-data.json: -------------------------------------------------------------------------------- ```json { "name": "test", "lastModified": "2025-03-25T08:22:10Z", "thumbnailUrl": "https://s3-alpha.figma.com/thumbnails/473a648c-4039-40c2-8046-1e1a2030aa81?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAQ4GOSFWCT3MRUCDU%2F20250323%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20250323T120000Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=016073092878d75d2d8b7e2fc2425fd97579e7f97d0d33fd89cd770cd30cc65a", "nodes": [ { "id": "409:3528", "name": "Frame", "type": "FRAME", "cssStyles": { "width": "322px", "height": "523px", "position": "absolute", "left": "13338px", "top": "12889px", "borderColor": "rgba(217, 217, 217, 0.4)", "borderWidth": "1px", "borderStyle": "solid", "borderRadius": "10px" }, "children": [ { "id": "409:3529", "name": "Group", "type": "GROUP", "cssStyles": { "width": "275.9183044433594px", "height": "275.53729248046875px", "position": "absolute", "left": "26.6875px", "top": "230.3779296875px" }, "children": [ { "id": "409:3530", "name": "Group", "type": "GROUP", "cssStyles": { "width": "275.9183044433594px", "height": "275.53729248046875px", "position": "absolute", "left": "0px", "top": "0px", "display": "flex", "flexDirection": "column", "gap": "9px", "justifyContent": "space-between" }, "children": [ { "id": "409:3565", "name": "Group 1410104421", "type": "GROUP", "cssStyles": { "width": "47.074562072753906px", "height": "47.074562072753906px", "position": "absolute", "left": "228.84375px", "top": "0px" }, "exportInfo": { "type": "IMAGE", "format": "SVG", "nodeId": "409:3565", "fileName": "group_1410104421.svg" } }, { "id": "409:3555", "name": "Vector", "type": "VECTOR", "cssStyles": { "width": "28.830785751342773px", "height": "24.948415756225586px", "position": "absolute", "left": "237.98828125px", "top": "66.38671875px", "backgroundColor": "#FFFFFF" }, "exportInfo": { "type": "IMAGE", "format": "SVG", "nodeId": "409:3555" } }, { "id": "409:3539", "name": "500 K", "type": "TEXT", "cssStyles": { "width": "27px", "height": "11px", "position": "absolute", "left": "239.251953125px", "top": "97.736328125px", "color": "#FFFFFF", "fontFamily": "Montserrat", "fontSize": "8.899900436401367px", "fontWeight": 600, "textAlign": "left", "verticalAlign": "top", "lineHeight": "10.848978042602539px" }, "text": "500 K" }, { "id": "409:3550", "name": "Group", "type": "GROUP", "cssStyles": { "width": "28.695743560791016px", "height": "29.134611129760742px", "position": "absolute", "left": "238.0390625px", "top": "120.115234375px" }, "exportInfo": { "type": "IMAGE", "format": "SVG", "nodeId": "409:3550", "fileName": "group.svg" } }, { "id": "409:3537", "name": "400", "type": "TEXT", "cssStyles": { "width": "19px", "height": "11px", "position": "absolute", "left": "243.90625px", "top": "155.279296875px", "color": "#FFFFFF", "fontFamily": "Montserrat", "fontSize": "8.899900436401367px", "fontWeight": 600, "textAlign": "left", "verticalAlign": "top", "lineHeight": "10.848978042602539px" }, "text": "400" }, { "id": "409:3549", "name": "Vector", "type": "VECTOR", "cssStyles": { "width": "25.04969024658203px", "height": "25.30290412902832px", "position": "absolute", "left": "239.87890625px", "top": "175.970703125px", "backgroundColor": "#FFFFFF" }, "exportInfo": { "type": "IMAGE", "format": "SVG", "nodeId": "409:3549" } }, { "id": "409:3562", "name": "Vector", "type": "VECTOR", "cssStyles": { "width": "32.74707794189453px", "height": "14.19596004486084px", "position": "absolute", "left": "2.17578125px", "top": "204.7001953125px", "backgroundColor": "#F7214E" }, "exportInfo": { "type": "IMAGE", "format": "SVG", "nodeId": "409:3562" } }, { "id": "409:3563", "name": "HOT", "type": "TEXT", "cssStyles": { "width": "21px", "height": "11px", "position": "absolute", "left": "7.625px", "top": "205.7548828125px", "color": "#FFFFFF", "fontFamily": "Montserrat", "fontSize": "8.823772430419922px", "fontWeight": 800, "textAlign": "left", "verticalAlign": "top", "lineHeight": "10.756178855895996px" }, "text": "HOT" }, { "id": "409:3535", "name": "Share", "type": "TEXT", "cssStyles": { "width": "27px", "height": "11px", "position": "absolute", "left": "239.251953125px", "top": "206.9052734375px", "color": "#FFFFFF", "fontFamily": "Montserrat", "fontSize": "8.899900436401367px", "fontWeight": 600, "textAlign": "left", "verticalAlign": "top", "lineHeight": "10.848978042602539px" }, "text": "Share" }, { "id": "409:3533", "name": "@LoremIpsum", "type": "TEXT", "cssStyles": { "width": "86px", "height": "14px", "position": "absolute", "left": "0px", "top": "223.7900390625px", "color": "#FFFFFF", "fontFamily": "Montserrat", "fontSize": "11.237085342407227px", "fontWeight": 600, "textAlign": "left", "verticalAlign": "top", "lineHeight": "13.698006629943848px" }, "text": "@LoremIpsum" }, { "id": "409:3542", "name": "Group", "type": "GROUP", "cssStyles": { "width": "34.25025939941406px", "height": "34.249176025390625px", "position": "absolute", "left": "235.26953125px", "top": "237.2109375px" }, "exportInfo": { "type": "IMAGE", "format": "SVG", "nodeId": "409:3542", "fileName": "group.svg" } }, { "id": "409:3541", "name": "#lorem#Ipsum#loremipsum", "type": "TEXT", "cssStyles": { "width": "165px", "height": "14px", "position": "absolute", "left": "0px", "top": "238.474609375px", "color": "#FFFFFF", "fontFamily": "Montserrat", "fontSize": "11.237085342407227px", "fontWeight": 600, "textAlign": "left", "verticalAlign": "top", "lineHeight": "13.698006629943848px" }, "text": "#lorem#Ipsum#loremipsum" }, { "id": "409:3531", "name": "Song Title - Singer", "type": "TEXT", "cssStyles": { "width": "106px", "height": "14px", "position": "absolute", "left": "27.716796875px", "top": "261.537109375px", "color": "#FFFFFF", "fontFamily": "Montserrat", "fontSize": "11.237085342407227px", "fontWeight": 600, "textAlign": "left", "verticalAlign": "top", "lineHeight": "13.698006629943848px" }, "text": "Song Title - Singer" }, { "id": "409:3558", "name": "Vector", "type": "VECTOR", "cssStyles": { "width": "12.102851867675781px", "height": "12.069082260131836px", "position": "absolute", "left": "4.490234375px", "top": "262.564453125px", "backgroundColor": "#FFFFFF" }, "exportInfo": { "type": "IMAGE", "format": "SVG", "nodeId": "409:3558" } } ] } ] } ] } ] } ``` -------------------------------------------------------------------------------- /test/test-output/viewer.html: -------------------------------------------------------------------------------- ```html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Figma 节点 CSS 样式查看器</title> <style> :root { --primary-color: #1E88E5; --secondary-color: #757575; --background-color: #FAFAFA; --card-background: #FFFFFF; --border-color: #E0E0E0; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; margin: 0; padding: 20px; background-color: var(--background-color); color: #333; } .container { max-width: 1200px; margin: 0 auto; background-color: var(--card-background); border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); padding: 20px; } h1 { color: var(--primary-color); border-bottom: 1px solid var(--border-color); padding-bottom: 10px; margin-top: 0; } .info-box { background-color: #E3F2FD; border-left: 4px solid var(--primary-color); padding: 10px 15px; margin-bottom: 20px; border-radius: 4px; } .file-input-container { display: flex; margin-bottom: 20px; align-items: center; flex-wrap: wrap; gap: 10px; } input[type="file"] { flex: 1; min-width: 300px; padding: 8px; border: 1px solid var(--border-color); border-radius: 4px; } button { background-color: var(--primary-color); color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; } button:hover { background-color: #1565C0; } .tabs { display: flex; margin-bottom: 20px; border-bottom: 1px solid var(--border-color); } .tab { padding: 10px 20px; cursor: pointer; border-bottom: 2px solid transparent; } .tab.active { color: var(--primary-color); border-bottom: 2px solid var(--primary-color); font-weight: 500; } .tab-content { display: none; } .tab-content.active { display: block; } .nodes { font-family: monospace; white-space: pre-wrap; padding: 15px; background-color: #F5F5F5; border-radius: 4px; overflow: auto; max-height: 600px; } .css-styles { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; } .style-card { border: 1px solid var(--border-color); border-radius: 6px; overflow: hidden; } .style-preview { height: 120px; display: flex; justify-content: center; align-items: center; } .style-info { padding: 15px; border-top: 1px solid var(--border-color); background-color: #F5F5F5; } .style-name { font-weight: 500; margin-bottom: 5px; } .style-properties { font-family: monospace; font-size: 13px; } .property { margin: 3px 0; } .color-box { display: inline-block; width: 16px; height: 16px; border-radius: 3px; margin-right: 6px; vertical-align: middle; border: 1px solid rgba(0, 0, 0, 0.1); } .search-container { margin-bottom: 15px; } #nodeSearch { width: 100%; padding: 8px; border: 1px solid var(--border-color); border-radius: 4px; margin-bottom: 10px; } .tree-view { font-family: monospace; line-height: 1.5; } .tree-item { margin: 2px 0; cursor: pointer; } .tree-toggle { display: inline-block; width: 16px; text-align: center; user-select: none; } .tree-content { padding-left: 20px; display: none; } .tree-content.expanded { display: block; } .selected { background-color: #E3F2FD; border-radius: 3px; } #nodeDetails { margin-top: 20px; padding: 15px; background-color: #F5F5F5; border-radius: 4px; display: none; } .detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; } @media (max-width: 768px) { .detail-grid { grid-template-columns: 1fr; } } .detail-section { border: 1px solid var(--border-color); border-radius: 4px; padding: 10px; background-color: white; } .detail-title { font-weight: 500; margin-bottom: 10px; color: var(--primary-color); } .detail-content { max-height: 300px; overflow: auto; } .css-preview { border: 1px solid #ddd; padding: 15px; margin-top: 10px; border-radius: 4px; } </style> </head> <body> <div class="container"> <h1>Figma 节点 CSS 样式查看器</h1> <div class="info-box"> 此工具用于查看Figma节点数据及其CSS样式转换结果。您可以上传JSON文件或加载示例数据。 </div> <div class="file-input-container"> <input type="file" id="fileInput" accept=".json"> <button id="loadFile">加载文件</button> <button id="loadSample">加载示例数据</button> </div> <div class="tabs"> <div class="tab active" data-tab="nodeTree">节点树</div> <div class="tab" data-tab="cssStyles">CSS 样式</div> </div> <div id="nodeTree" class="tab-content active"> <div class="search-container"> <input type="text" id="nodeSearch" placeholder="搜索节点名称..."> </div> <div class="tree-view" id="nodeTreeView"></div> <div id="nodeDetails"> <h3>节点详情</h3> <div class="detail-grid"> <div class="detail-section"> <div class="detail-title">基本信息</div> <div class="detail-content" id="nodeBasicInfo"></div> </div> <div class="detail-section"> <div class="detail-title">CSS 样式</div> <div class="detail-content" id="nodeCssStyles"></div> <div class="css-preview" id="cssPreview"></div> </div> </div> </div> </div> <div id="cssStyles" class="tab-content"> <div id="cssStylesContent" class="css-styles"></div> </div> </div> <script> let figmaData = null // DOM元素 const fileInput = document.getElementById('fileInput') const loadFileBtn = document.getElementById('loadFile') const loadSampleBtn = document.getElementById('loadSample') const tabs = document.querySelectorAll('.tab') const tabContents = document.querySelectorAll('.tab-content') const nodeTreeView = document.getElementById('nodeTreeView') const nodeSearch = document.getElementById('nodeSearch') const nodeDetails = document.getElementById('nodeDetails') const nodeBasicInfo = document.getElementById('nodeBasicInfo') const nodeCssStyles = document.getElementById('nodeCssStyles') const cssPreview = document.getElementById('cssPreview') const cssStylesContent = document.getElementById('cssStylesContent') // 初始化 document.addEventListener('DOMContentLoaded', () => { // 加载示例数据(如果在同目录下存在) try { fetch('./simplified-with-css.json') .then(response => { if (!response.ok) throw new Error('示例数据未找到') return response.json() }) .then(data => { figmaData = data renderData() }) .catch(err => console.log('未找到示例数据,请上传文件')) } catch (e) { console.log('未找到示例数据,请上传文件') } }) // 事件监听器 loadFileBtn.addEventListener('click', () => { if (fileInput.files.length > 0) { const file = fileInput.files[0] const reader = new FileReader() reader.onload = (e) => { try { figmaData = JSON.parse(e.target.result) renderData() } catch (err) { alert('JSON解析错误: ' + err.message) } } reader.readAsText(file) } else { alert('请选择一个JSON文件') } }) loadSampleBtn.addEventListener('click', async () => { try { const response = await fetch('./simplified-with-css.json') if (!response.ok) throw new Error('示例数据未找到') figmaData = await response.json() renderData() } catch (err) { alert('加载示例数据失败: ' + err.message) } }) // 标签切换 tabs.forEach(tab => { tab.addEventListener('click', () => { const tabId = tab.getAttribute('data-tab') tabs.forEach(t => t.classList.remove('active')) tabContents.forEach(tc => tc.classList.remove('active')) tab.classList.add('active') document.getElementById(tabId).classList.add('active') }) }) // 搜索功能 nodeSearch.addEventListener('input', () => { const searchTerm = nodeSearch.value.toLowerCase() const treeItems = document.querySelectorAll('.tree-item') treeItems.forEach(item => { const text = item.textContent.toLowerCase() if (text.includes(searchTerm)) { item.style.display = 'block' // 展开父级 let parent = item.parentElement while (parent && parent.classList.contains('tree-content')) { parent.classList.add('expanded') parent = parent.parentElement.parentElement } } else { item.style.display = 'none' } }) }) // 渲染数据 function renderData() { if (!figmaData) return // 渲染节点树 renderNodeTree() // 渲染CSS样式 renderCssStyles() } // 渲染节点树 function renderNodeTree() { nodeTreeView.innerHTML = '' if (figmaData.nodes && Array.isArray(figmaData.nodes)) { figmaData.nodes.forEach(node => { nodeTreeView.appendChild(createTreeItem(node)) }) } } // 创建树项 function createTreeItem(node, level = 0) { const item = document.createElement('div') item.className = 'tree-item' item.dataset.nodeId = node.id || '' const hasChildren = node.children && node.children.length > 0 const toggle = document.createElement('span') toggle.className = 'tree-toggle' toggle.textContent = hasChildren ? '▶' : ' ' const label = document.createElement('span') label.className = 'tree-label' label.textContent = `${node.name || 'Unnamed'} (${node.type || 'Unknown'})` item.appendChild(toggle) item.appendChild(label) if (hasChildren) { const content = document.createElement('div') content.className = 'tree-content' node.children.forEach(child => { content.appendChild(createTreeItem(child, level + 1)) }) toggle.addEventListener('click', () => { toggle.textContent = content.classList.toggle('expanded') ? '▼' : '▶' }) item.appendChild(content) } // 点击查看节点详情 item.addEventListener('click', (e) => { if (e.target !== toggle) { document.querySelectorAll('.tree-item').forEach(i => i.classList.remove('selected')) item.classList.add('selected') showNodeDetails(node) } e.stopPropagation() }) return item } // 显示节点详情 function showNodeDetails(node) { nodeDetails.style.display = 'block' // 基本信息 nodeBasicInfo.innerHTML = ` <div><strong>ID:</strong> ${node.id || 'N/A'}</div> <div><strong>名称:</strong> ${node.name || 'Unnamed'}</div> <div><strong>类型:</strong> ${node.type || 'Unknown'}</div> ${node.boundingBox ? ` <div><strong>位置:</strong> X: ${node.boundingBox.x.toFixed(2)}, Y: ${node.boundingBox.y.toFixed(2)}</div> <div><strong>尺寸:</strong> W: ${node.boundingBox.width.toFixed(2)}, H: ${node.boundingBox.height.toFixed(2)}</div> ` : ''} ` // CSS样式 if (node.cssStyles && Object.keys(node.cssStyles).length > 0) { let cssStylesHtml = '<div class="properties">' for (const [property, value] of Object.entries(node.cssStyles)) { cssStylesHtml += ` <div class="property"> ${property.includes('color') || property.includes('background') ? `<span class="color-box" style="background-color: ${value}"></span>` : ''} <strong>${property}:</strong> ${value} </div> ` } cssStylesHtml += '</div>' nodeCssStyles.innerHTML = cssStylesHtml // CSS预览 let styles = '' for (const [property, value] of Object.entries(node.cssStyles)) { styles += `${property}: ${value};\n` } cssPreview.innerHTML = ` <div class="detail-title">预览</div> <div style="${styles} border: 1px dashed #ccc; min-height: 50px; display: flex; align-items: center; justify-content: center;"> ${node.type === 'TEXT' && node.characters ? node.characters : 'CSS样式预览'} </div> <pre style="margin-top: 10px;">${styles}</pre> ` cssPreview.style.display = 'block' } else { nodeCssStyles.innerHTML = '<div>该节点没有CSS样式</div>' cssPreview.style.display = 'none' } } // 渲染CSS样式 function renderCssStyles() { cssStylesContent.innerHTML = '' if (!figmaData.nodes) return // 收集所有样式 const stylesMap = new Map() function collectStyles(nodes) { if (!Array.isArray(nodes)) return nodes.forEach(node => { if (node.cssStyles && Object.keys(node.cssStyles).length > 0) { const styleKey = JSON.stringify(node.cssStyles) if (!stylesMap.has(styleKey)) { stylesMap.set(styleKey, { styles: node.cssStyles, count: 1, nodeName: node.name, nodeType: node.type }) } else { const info = stylesMap.get(styleKey) info.count++ } } if (node.children) { collectStyles(node.children) } }) } collectStyles(figmaData.nodes) // 按使用频率排序并仅显示前50个样式 const sortedStyles = Array.from(stylesMap.entries()) .sort((a, b) => b[1].count - a[1].count) .slice(0, 50) // 创建样式卡片 sortedStyles.forEach(([styleKey, info]) => { const { styles, count, nodeName, nodeType } = info const card = document.createElement('div') card.className = 'style-card' let preview = '' if (styles.backgroundColor) { preview = `background-color: ${styles.backgroundColor};` } else if (styles.color) { preview = `color: ${styles.color}; background-color: #f0f0f0;` } let stylesStr = '' for (const [property, value] of Object.entries(styles)) { stylesStr += `${property}: ${value};\n` } card.innerHTML = ` <div class="style-preview" style="${preview}"> <div style="${Object.entries(styles).map(([p, v]) => `${p}: ${v}`).join('; ')}"> ${nodeType === 'TEXT' ? '文本样式示例' : '样式预览'} </div> </div> <div class="style-info"> <div class="style-name">${nodeName || 'Unnamed'} (${nodeType || 'Unknown'}) - 使用 ${count} 次</div> <div class="style-properties"> ${Object.entries(styles).map(([property, value]) => ` <div class="property"> ${property.includes('color') || property.includes('background') ? `<span class="color-box" style="background-color: ${value}"></span>` : ''} <strong>${property}:</strong> ${value} </div> `).join('')} </div> </div> ` cssStylesContent.appendChild(card) }) } </script> </body> </html> ``` -------------------------------------------------------------------------------- /test/test-output/real-node-data.json: -------------------------------------------------------------------------------- ```json { "name": "test", "lastModified": "2025-03-25T08:22:10Z", "thumbnailUrl": "https://s3-alpha.figma.com/thumbnails/473a648c-4039-40c2-8046-1e1a2030aa81?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAQ4GOSFWCT3MRUCDU%2F20250323%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20250323T120000Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=016073092878d75d2d8b7e2fc2425fd97579e7f97d0d33fd89cd770cd30cc65a", "version": "2199364251293339646", "role": "owner", "editorType": "figma", "linkAccess": "view", "nodes": { "409:3528": { "document": { "id": "409:3528", "name": "Frame", "type": "FRAME", "scrollBehavior": "SCROLLS", "children": [ { "id": "409:3529", "name": "Group", "type": "GROUP", "scrollBehavior": "SCROLLS", "children": [ { "id": "409:3530", "name": "Group", "type": "GROUP", "scrollBehavior": "SCROLLS", "children": [ { "id": "409:3531", "name": "Song Title - Singer", "type": "TEXT", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13392.404296875, "y": 13380.9150390625, "width": 106, "height": 14 }, "absoluteRenderBounds": { "x": 13392.7978515625, "y": 13383.240234375, "width": 105.2763671875, "height": 10.93359375 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "characters": "Song Title - Singer", "characterStyleOverrides": [], "styleOverrideTable": {}, "lineTypes": [ "NONE" ], "lineIndentations": [ 0 ], "style": { "fontFamily": "Montserrat", "fontPostScriptName": "Montserrat-SemiBold", "fontStyle": "SemiBold", "fontWeight": 600, "textAutoResize": "WIDTH_AND_HEIGHT", "fontSize": 11.237085342407227, "textAlignHorizontal": "LEFT", "textAlignVertical": "TOP", "letterSpacing": 0, "lineHeightPx": 13.698006629943848, "lineHeightPercent": 100, "lineHeightUnit": "INTRINSIC_%" }, "layoutVersion": 4, "effects": [], "interactions": [] }, { "id": "409:3533", "name": "@LoremIpsum", "type": "TEXT", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13364.6875, "y": 13343.16796875, "width": 86, "height": 14 }, "absoluteRenderBounds": { "x": 13365.181640625, "y": 13346.201171875, "width": 84.150390625, "height": 10.2587890625 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "characters": "@LoremIpsum", "characterStyleOverrides": [], "styleOverrideTable": {}, "lineTypes": [ "NONE" ], "lineIndentations": [ 0 ], "style": { "fontFamily": "Montserrat", "fontPostScriptName": "Montserrat-SemiBold", "fontStyle": "SemiBold", "fontWeight": 600, "textAutoResize": "WIDTH_AND_HEIGHT", "fontSize": 11.237085342407227, "textAlignHorizontal": "LEFT", "textAlignVertical": "TOP", "letterSpacing": 0, "lineHeightPx": 13.698006629943848, "lineHeightPercent": 100, "lineHeightUnit": "INTRINSIC_%" }, "layoutVersion": 4, "effects": [], "interactions": [] }, { "id": "409:3535", "name": "Share", "type": "TEXT", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13603.939453125, "y": 13326.283203125, "width": 27, "height": 11 }, "absoluteRenderBounds": { "x": 13604.2509765625, "y": 13328.6796875, "width": 25.6318359375, "height": 6.6923828125 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "characters": "Share", "characterStyleOverrides": [], "styleOverrideTable": {}, "lineTypes": [ "NONE" ], "lineIndentations": [ 0 ], "style": { "fontFamily": "Montserrat", "fontPostScriptName": "Montserrat-SemiBold", "fontStyle": "SemiBold", "fontWeight": 600, "textAutoResize": "WIDTH_AND_HEIGHT", "fontSize": 8.899900436401367, "textAlignHorizontal": "LEFT", "textAlignVertical": "TOP", "letterSpacing": 0, "lineHeightPx": 10.848978042602539, "lineHeightPercent": 100, "lineHeightUnit": "INTRINSIC_%" }, "layoutVersion": 4, "effects": [], "interactions": [] }, { "id": "409:3537", "name": "400", "type": "TEXT", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13608.59375, "y": 13274.6572265625, "width": 19, "height": 11 }, "absoluteRenderBounds": { "x": 13608.896484375, "y": 13277.337890625, "width": 17.32421875, "height": 6.408203125 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "characters": "400", "characterStyleOverrides": [], "styleOverrideTable": {}, "lineTypes": [ "NONE" ], "lineIndentations": [ 0 ], "style": { "fontFamily": "Montserrat", "fontPostScriptName": "Montserrat-SemiBold", "fontStyle": "SemiBold", "fontWeight": 600, "textAutoResize": "WIDTH_AND_HEIGHT", "fontSize": 8.899900436401367, "textAlignHorizontal": "LEFT", "textAlignVertical": "TOP", "letterSpacing": 0, "lineHeightPx": 10.848978042602539, "lineHeightPercent": 100, "lineHeightUnit": "INTRINSIC_%" }, "layoutVersion": 4, "effects": [], "interactions": [] }, { "id": "409:3539", "name": "500 K", "type": "TEXT", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13603.939453125, "y": 13217.1142578125, "width": 27, "height": 11 }, "absoluteRenderBounds": { "x": 13604.0546875, "y": 13219.794921875, "width": 26.05078125, "height": 6.408203125 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "characters": "500 K", "characterStyleOverrides": [], "styleOverrideTable": {}, "lineTypes": [ "NONE" ], "lineIndentations": [ 0 ], "style": { "fontFamily": "Montserrat", "fontPostScriptName": "Montserrat-SemiBold", "fontStyle": "SemiBold", "fontWeight": 600, "textAutoResize": "WIDTH_AND_HEIGHT", "fontSize": 8.899900436401367, "textAlignHorizontal": "LEFT", "textAlignVertical": "TOP", "letterSpacing": 0, "lineHeightPx": 10.848978042602539, "lineHeightPercent": 100, "lineHeightUnit": "INTRINSIC_%" }, "layoutVersion": 4, "effects": [], "interactions": [] }, { "id": "409:3541", "name": "#lorem#Ipsum#loremipsum", "type": "TEXT", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13364.6875, "y": 13357.8525390625, "width": 165, "height": 14 }, "absoluteRenderBounds": { "x": 13364.9462890625, "y": 13360.177734375, "width": 163.4404296875, "height": 10.8544921875 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "characters": "#lorem#Ipsum#loremipsum", "characterStyleOverrides": [], "styleOverrideTable": {}, "lineTypes": [ "NONE" ], "lineIndentations": [ 0 ], "style": { "fontFamily": "Montserrat", "fontPostScriptName": "Montserrat-SemiBold", "fontStyle": "SemiBold", "fontWeight": 600, "textAutoResize": "WIDTH_AND_HEIGHT", "fontSize": 11.237085342407227, "textAlignHorizontal": "LEFT", "textAlignVertical": "TOP", "letterSpacing": 0, "lineHeightPx": 13.698006629943848, "lineHeightPercent": 100, "lineHeightUnit": "INTRINSIC_%" }, "layoutVersion": 4, "effects": [], "interactions": [] }, { "id": "409:3542", "name": "Group", "type": "GROUP", "scrollBehavior": "SCROLLS", "children": [ { "id": "409:3543", "name": "Vector", "type": "VECTOR", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 0.13725490868091583, "g": 0.12156862765550613, "b": 0.125490203499794, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13599.95703125, "y": 13356.5888671875, "width": 34.249202728271484, "height": 34.249176025390625 }, "absoluteRenderBounds": { "x": 13599.95703125, "y": 13356.5888671875, "width": 34.2490234375, "height": 34.2490234375 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "effects": [], "interactions": [] }, { "id": "409:3544", "name": "Vector", "type": "VECTOR", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 0.2549019753932953, "g": 0.250980406999588, "b": 0.25882354378700256, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13617.07421875, "y": 13362.8515625, "width": 17.133037567138672, "height": 21.707462310791016 }, "absoluteRenderBounds": { "x": 13617.07421875, "y": 13362.8515625, "width": 17.1328125, "height": 21.70703125 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "effects": [], "interactions": [] }, { "id": "409:3545", "name": "Vector", "type": "VECTOR", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 0.2549019753932953, "g": 0.250980406999588, "b": 0.25882354378700256, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13599.95703125, "y": 13362.8515625, "width": 17.116167068481445, "height": 21.72435760498047 }, "absoluteRenderBounds": { "x": 13599.95703125, "y": 13362.8515625, "width": 17.1162109375, "height": 21.724609375 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "effects": [], "interactions": [] }, { "id": "409:3546", "name": "Vector", "type": "VECTOR", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 0, "g": 0, "b": 0, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13608.59765625, "y": 13365.248046875, "width": 16.93048667907715, "height": 16.93048667907715 }, "absoluteRenderBounds": { "x": 13608.59765625, "y": 13365.248046875, "width": 16.9306640625, "height": 16.9306640625 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "effects": [], "interactions": [] }, { "id": "409:3547", "name": "Vector", "type": "VECTOR", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13610.490234375, "y": 13374.26171875, "width": 13.166287422180176, "height": 7.933498382568359 }, "absoluteRenderBounds": { "x": 13610.490234375, "y": 13374.26171875, "width": 13.166015625, "height": 7.93359375 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "effects": [], "interactions": [] }, { "id": "409:3548", "name": "Vector", "type": "VECTOR", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13613.81640625, "y": 13366.7333984375, "width": 6.515623092651367, "height": 6.51564884185791 }, "absoluteRenderBounds": { "x": 13613.81640625, "y": 13366.7333984375, "width": 6.515625, "height": 6.515625 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "effects": [], "interactions": [] } ], "blendMode": "PASS_THROUGH", "clipsContent": false, "background": [], "fills": [], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "backgroundColor": { "r": 0, "g": 0, "b": 0, "a": 0 }, "absoluteBoundingBox": { "x": 13599.95703125, "y": 13356.5888671875, "width": 34.25025939941406, "height": 34.249176025390625 }, "absoluteRenderBounds": { "x": 13599.95703125, "y": 13356.5888671875, "width": 34.25025939941406, "height": 34.249176025390625 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "effects": [], "interactions": [] }, { "id": "409:3549", "name": "Vector", "type": "VECTOR", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13604.56640625, "y": 13295.3486328125, "width": 25.04969024658203, "height": 25.30290412902832 }, "absoluteRenderBounds": { "x": 13604.56640625, "y": 13295.3486328125, "width": 25.0498046875, "height": 25.302734375 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "exportSettings": [ { "suffix": "", "format": "SVG", "constraint": { "type": "SCALE", "value": 1 } } ], "effects": [], "interactions": [] }, { "id": "409:3550", "name": "Group", "type": "GROUP", "scrollBehavior": "SCROLLS", "children": [ { "id": "409:3551", "name": "Vector", "type": "VECTOR", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13602.7265625, "y": 13239.4931640625, "width": 28.695743560791016, "height": 29.134611129760742 }, "absoluteRenderBounds": { "x": 13602.7265625, "y": 13239.4931640625, "width": 28.6953125, "height": 29.134765625 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "effects": [], "interactions": [] }, { "id": "409:3552", "name": "Vector", "type": "VECTOR", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 0, "g": 0, "b": 0, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13608.212890625, "y": 13249.384765625, "width": 3.949878692626953, "height": 3.949878692626953 }, "absoluteRenderBounds": { "x": 13608.212890625, "y": 13249.384765625, "width": 3.9501953125, "height": 3.9501953125 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "effects": [], "interactions": [] }, { "id": "409:3553", "name": "Vector", "type": "VECTOR", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 0, "g": 0, "b": 0, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13615.099609375, "y": 13249.384765625, "width": 3.949878692626953, "height": 3.949878692626953 }, "absoluteRenderBounds": { "x": 13615.099609375, "y": 13249.384765625, "width": 3.9501953125, "height": 3.9501953125 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "effects": [], "interactions": [] }, { "id": "409:3554", "name": "Vector", "type": "VECTOR", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 0, "g": 0, "b": 0, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13622.00390625, "y": 13249.384765625, "width": 3.949878692626953, "height": 3.949878692626953 }, "absoluteRenderBounds": { "x": 13622.00390625, "y": 13249.384765625, "width": 3.9501953125, "height": 3.9501953125 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "effects": [], "interactions": [] } ], "blendMode": "PASS_THROUGH", "clipsContent": false, "background": [], "fills": [], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "backgroundColor": { "r": 0, "g": 0, "b": 0, "a": 0 }, "absoluteBoundingBox": { "x": 13602.7265625, "y": 13239.4931640625, "width": 28.695743560791016, "height": 29.134611129760742 }, "absoluteRenderBounds": { "x": 13602.7265625, "y": 13239.4931640625, "width": 28.695743560791016, "height": 29.134765625 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "exportSettings": [ { "suffix": "", "format": "SVG", "constraint": { "type": "SCALE", "value": 1 } } ], "effects": [], "interactions": [] }, { "id": "409:3555", "name": "Vector", "type": "VECTOR", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13602.67578125, "y": 13185.7646484375, "width": 28.830785751342773, "height": 24.948415756225586 }, "absoluteRenderBounds": { "x": 13602.67578125, "y": 13185.7646484375, "width": 28.8310546875, "height": 24.9482421875 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "exportSettings": [ { "suffix": "", "format": "SVG", "constraint": { "type": "SCALE", "value": 1 } } ], "effects": [], "interactions": [] }, { "id": "409:3565", "name": "Group 1410104421", "type": "GROUP", "scrollBehavior": "SCROLLS", "children": [ { "id": "409:3556", "name": "Vector", "type": "VECTOR", "scrollBehavior": "SCROLLS", "rotation": -0.7853981633974483, "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 0.9686274528503418, "g": 0.12941177189350128, "b": 0.30588236451148987, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13593.53125, "y": 13119.377757252198, "width": 47.07456362060657, "height": 47.07456362060475 }, "absoluteRenderBounds": { "x": 13600.4248046875, "y": 13126.271484375, "width": 33.287109375, "height": 33.287109375 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "effects": [], "interactions": [] }, { "id": "409:3557", "name": "Vector", "type": "VECTOR", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13607.166015625, "y": 13133.015625, "width": 19.816926956176758, "height": 19.816951751708984 }, "absoluteRenderBounds": { "x": 13607.166015625, "y": 13133.015625, "width": 19.8173828125, "height": 19.8173828125 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "effects": [], "interactions": [] } ], "blendMode": "PASS_THROUGH", "clipsContent": false, "background": [], "fills": [], "strokes": [], "strokeWeight": 1, "strokeAlign": "INSIDE", "backgroundColor": { "r": 0, "g": 0, "b": 0, "a": 0 }, "absoluteBoundingBox": { "x": 13593.53125, "y": 13119.3779296875, "width": 47.074562072753906, "height": 47.074562072753906 }, "absoluteRenderBounds": { "x": 13593.53125, "y": 13119.3779296875, "width": 47.074562072753906, "height": 47.074562072753906 }, "constraints": { "vertical": "TOP", "horizontal": "LEFT" }, "exportSettings": [ { "suffix": "", "format": "SVG", "constraint": { "type": "SCALE", "value": 1 } } ], "effects": [], "interactions": [] }, { "id": "409:3558", "name": "Vector", "type": "VECTOR", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13369.177734375, "y": 13381.9423828125, "width": 12.102851867675781, "height": 12.069082260131836 }, "absoluteRenderBounds": { "x": 13369.177734375, "y": 13381.9423828125, "width": 12.1025390625, "height": 12.0693359375 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "exportSettings": [ { "suffix": "", "format": "SVG", "constraint": { "type": "SCALE", "value": 1 } } ], "effects": [], "interactions": [] }, { "id": "409:3562", "name": "Vector", "type": "VECTOR", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 0.9686274528503418, "g": 0.12941177189350128, "b": 0.30588236451148987, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13366.86328125, "y": 13324.078125, "width": 32.74707794189453, "height": 14.19596004486084 }, "absoluteRenderBounds": { "x": 13366.86328125, "y": 13324.078125, "width": 32.7470703125, "height": 14.1962890625 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "effects": [], "interactions": [] }, { "id": "409:3563", "name": "HOT", "type": "TEXT", "scrollBehavior": "SCROLLS", "blendMode": "PASS_THROUGH", "fills": [ { "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 1, "g": 1, "b": 1, "a": 1 } } ], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "absoluteBoundingBox": { "x": 13372.3125, "y": 13325.1328125, "width": 21, "height": 11 }, "absoluteRenderBounds": { "x": 13372.9296875, "y": 13327.8330078125, "width": 19.435546875, "height": 6.4228515625 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "characters": "HOT", "characterStyleOverrides": [], "styleOverrideTable": {}, "lineTypes": [ "NONE" ], "lineIndentations": [ 0 ], "style": { "fontFamily": "Montserrat", "fontPostScriptName": "Montserrat-ExtraBold", "fontStyle": "ExtraBold", "fontWeight": 800, "textAutoResize": "WIDTH_AND_HEIGHT", "fontSize": 8.823772430419922, "textAlignHorizontal": "LEFT", "textAlignVertical": "TOP", "letterSpacing": 0, "lineHeightPx": 10.756178855895996, "lineHeightPercent": 100, "lineHeightUnit": "INTRINSIC_%" }, "layoutVersion": 4, "effects": [], "interactions": [] } ], "blendMode": "PASS_THROUGH", "clipsContent": false, "background": [], "fills": [], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "backgroundColor": { "r": 0, "g": 0, "b": 0, "a": 0 }, "absoluteBoundingBox": { "x": 13364.6875, "y": 13119.3779296875, "width": 275.9183044433594, "height": 275.53729248046875 }, "absoluteRenderBounds": { "x": 13364.6875, "y": 13119.3779296875, "width": 275.9183044433594, "height": 275.53729248046875 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "effects": [], "interactions": [] } ], "blendMode": "PASS_THROUGH", "clipsContent": false, "background": [], "fills": [], "strokes": [], "strokeWeight": 1.6879849433898926, "strokeAlign": "INSIDE", "backgroundColor": { "r": 0, "g": 0, "b": 0, "a": 0 }, "absoluteBoundingBox": { "x": 13364.6875, "y": 13119.3779296875, "width": 275.9183044433594, "height": 275.53729248046875 }, "absoluteRenderBounds": { "x": 13364.6875, "y": 13119.3779296875, "width": 275.9183044433594, "height": 275.53729248046875 }, "constraints": { "vertical": "SCALE", "horizontal": "SCALE" }, "effects": [], "interactions": [] } ], "blendMode": "PASS_THROUGH", "clipsContent": true, "background": [], "fills": [], "strokes": [ { "opacity": 0.4000000059604645, "blendMode": "NORMAL", "type": "SOLID", "color": { "r": 0.8509804010391235, "g": 0.8509804010391235, "b": 0.8509804010391235, "a": 1 } } ], "cornerRadius": 10, "cornerSmoothing": 0, "strokeWeight": 1, "strokeAlign": "INSIDE", "backgroundColor": { "r": 0, "g": 0, "b": 0, "a": 0 }, "absoluteBoundingBox": { "x": 13338, "y": 12889, "width": 322, "height": 523 }, "absoluteRenderBounds": { "x": 13338, "y": 12889, "width": 322, "height": 523 }, "constraints": { "vertical": "TOP", "horizontal": "LEFT" }, "exportSettings": [ { "suffix": "", "format": "PNG", "constraint": { "type": "SCALE", "value": 1 } } ], "effects": [], "interactions": [] }, "components": {}, "componentSets": {}, "schemaVersion": 0, "styles": {} } } } ```